Add support for pollingTime overrides (#2533)

* Add support for pollingTime overrides

* the current format HH:mm:ss is still supported
* add option for deviation percent (HH:mm:ss~\d{1,2}%) which allows the system to do some randomizing of the poll interval
* add support for overriding default polling time interval for devices matching some RSQL filters (in order), e.g. 01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync -> 00:05:00
* IMPORTANT: overdue time is calculated according to the default polling time. So, the overdue status might be incorrect for targets with overridden poll interval

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

* Remove min polling time from the tenant config - it is a system configuration

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

* Add support for bigger poll intervals and overdue + duration format config support

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

---------

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-07-07 16:33:55 +03:00
committed by GitHub
parent baab05f009
commit 7f97d6f441
31 changed files with 664 additions and 514 deletions

View File

@@ -35,6 +35,7 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.SoftwareModuleMetadata;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties;
import org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties.TenantConfigurationKey;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -188,38 +189,24 @@ public interface ControllerManagement {
/**
* Returns configured polling interval at which the controller polls hawkBit server.
*
* @return current {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}.
* @param target {@link Target} for which polling time is calculated (it could be overridden for a specific targets).
* @return current {@link TenantConfigurationKey#POLLING_TIME}.
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
String getPollingTime();
/**
* Returns the configured minimum polling interval.
*
* @return current {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}.
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
String getMinPollingTime();
/**
* Returns the count to be used for reducing polling interval while calling {@link ControllerManagement#getPollingTimeForAction(Action)}.
*
* @return configured value of {@link TenantConfigurationKey#MAINTENANCE_WINDOW_POLL_COUNT}.
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
int getMaintenanceWindowPollCount();
String getPollingTime(Target target);
/**
* Returns polling time based on the maintenance window for an action. Server will reduce the polling interval as the start time for
* maintenance window approaches, so that at least these many attempts are made between current polling until start of maintenance window.
* Poll time keeps reducing with MinPollingTime as lower limit {@link TenantConfigurationKey#MIN_POLLING_TIME_INTERVAL}. After the start
* of maintenance window, it resets to default {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}.
* Poll time keeps reducing with MinPollingTime as lower limit {@link ControllerPollProperties#getMinPollingTime()}. After the start
* of maintenance window, it resets to default {@link TenantConfigurationKey#POLLING_TIME}.
*
* @param target {@link Target} for which polling time is calculated
* @param action {@link Action} for which polling time is calculated based on it having maintenance window or not
* @return current {@link TenantConfigurationKey#POLLING_TIME_INTERVAL}.
* @return current {@link TenantConfigurationKey#POLLING_TIME}.
*/
@PreAuthorize(SpringEvalExpressions.IS_CONTROLLER)
String getPollingTimeForAction(Action action);
String getPollingTimeForAction(Target target, Action action);
/**
* Checks if a given target has currently or has even been assigned to the given artifact through the action history list. This can e.g.

View File

@@ -31,11 +31,10 @@ public interface TenantConfigurationManagement {
* Adds or updates a specific configuration for a specific tenant.
*
* @param configurationKeyName the key of the configuration
* @param value the configuration value which will be written into the
* database.
* @param value the configuration value which will be written into the database.
* @return the configuration value which was just written into the database.
* @throws TenantConfigurationValidatorException if the {@code propertyType} and the value in general does not
* match the expected type and format defined by the Key
* @throws TenantConfigurationValidatorException if the {@code propertyType} and the value in general does not match the expected type and
* format defined by the Key
* @throws ConversionFailedException if the property cannot be converted to the given
*/
@PreAuthorize(value = SpringEvalExpressions.HAS_AUTH_TENANT_CONFIGURATION)

View File

@@ -16,11 +16,9 @@ import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Defines global configuration for the controllers/clients on the provisioning
* targets/devices.
*
* Note: many of the controller related properties can be overridden on tenant
* level.
* Defines global configuration for the controllers/clients on the provisioning targets/devices.
* <p/>
* Note: many of the controller related properties can be overridden on tenant level.
*/
@Data
@ConfigurationProperties(prefix = "hawkbit.controller")
@@ -30,22 +28,24 @@ public class ControllerPollProperties implements Serializable {
private static final long serialVersionUID = 1L;
/**
* Maximum polling time that can be configured system-wide and by tenant in HH:MM:SS notation.
* Maximum polling time that can be configured system-wide and by tenant in HH:mm:ss notation.
*/
private String maxPollingTime = "23:59:59";
/**
* Minimum polling time that can be configured by a tenant in HH:MM:SS notation.
* Minimum polling time that can be configured by a tenant in HH:mm:ss notation.
*/
private String minPollingTime = "00:00:30";
/**
* Controller polling time that can be configured system-wide and by tenant in HH:MM:SS notation.
* Controller polling time that can be configured system-wide and by tenant in HH:mm:ss(~\d{1,2}%)? notation, plus
* followed (optionally and ordered) by a comma separated @lt;QL filter@gt; -@gt; polling time that overrides the
* default polling time for the targets that match the filter.
*/
private String pollingTime = "00:05:00";
/**
* Controller polling overdue time that can be configured system-wide and by tenant in HH:MM:SS notation.
* Controller polling overdue time that can be configured system-wide and by tenant in HH:mm:ss notation.
*/
private String pollingOverdueTime = "00:05:00";

View File

@@ -13,101 +13,78 @@ import java.time.Duration;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import lombok.NoArgsConstructor;
/**
* This class is a helper for converting a duration into a string and for the
* other way. The string is in the format expected in configuration and database
* {@link #DURATION_FORMAT}.
* This class is a helper for converting a duration into a string and for the other way. The string is in the format expected
* in configuration and database - in {@link Duration} default format or in custom format like "01:00:00" or "01:01:50:50"
* (starting with seconds, minutes, hours, days from the end).
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public final class DurationHelper {
/**
* Format of the String expected in configuration file and in the database.
*/
public static final String DURATION_FORMAT = "HH:mm:ss";
private DurationHelper() {
// utility class
}
/**
* Creates a {@link DurationRangeValidator}.
*
* @param min imum of range.
* @param max imum of range.
* @return {@link DurationRangeValidator} range.
*/
public static DurationRangeValidator durationRangeValidator(final Duration min, final Duration max) {
return new DurationRangeValidator(min, max);
}
private static final DateTimeFormatter DURATION_FORMATER = DateTimeFormatter.ofPattern("HH:mm:ss");
private static final long SECONDS_PER_DAY = 24 * 60 * 60L; // 24 hours * 60 minutes * 60 seconds
private static final Duration DAY = Duration.ofDays(1);
/**
* Converts a Duration into a formatted String
*
* @param duration duration, which will be converted into a formatted String
* @return String in the duration format, specified at
* {@link #DURATION_FORMAT}
* @return String in the duration format, specified as HH:mm:ss or d+:HH:mm:ss
*/
public static String durationToFormattedString(final Duration duration) {
public static String toString(final Duration duration) {
if (duration == null) {
return null;
}
return LocalTime.ofNanoOfDay(duration.toNanos()).format(DateTimeFormatter.ofPattern(DURATION_FORMAT));
if (duration.compareTo(DAY) < 0) { // backward compatible HH:mm:ss
return LocalTime.ofSecondOfDay(duration.toSeconds()).format(DURATION_FORMATER);
} else { // custom format d+:HH:mm:ss
return duration.toDays() + ":" + LocalTime.ofSecondOfDay(duration.toSeconds() % SECONDS_PER_DAY).format(DURATION_FORMATER);
}
}
/**
* Converts a formatted String into a Duration object.
*
* @param formattedDuration String in {@link #DURATION_FORMAT}
* @return duration
* @param durationStr String in {@link Duration} default format or in custom format like "01:00:00" or "01:01:50:50"
* (starting with seconds, minutes, hours, days from the end)
* @return duration as a {@link Duration} object
* @throws DateTimeParseException when String is in wrong format
*/
public static Duration formattedStringToDuration(final String formattedDuration) {
if (formattedDuration == null) {
public static Duration fromString(final String durationStr) {
if (durationStr == null) {
return null;
}
final TemporalAccessor ta = DateTimeFormatter.ofPattern(DURATION_FORMAT).parse(formattedDuration.trim());
return Duration.between(LocalTime.MIDNIGHT, LocalTime.from(ta));
}
/**
* converts values of time constants to a Duration object..
*
* @param hours count of hours
* @param minutes count of minutes
* @param seconds count of seconds
* @return duration
*/
public static Duration getDurationByTimeValues(final long hours, final long minutes, final long seconds) {
return Duration.ofHours(hours).plusMinutes(minutes).plusSeconds(seconds);
}
/**
* Duration validation utility class. Checks if the requested duration is in
* the defined min/max range.
*/
public static final class DurationRangeValidator {
private final Duration min;
private final Duration max;
private DurationRangeValidator(final Duration min, final Duration max) {
this.min = min;
this.max = max;
}
/**
* Checks if the requested duration is in the defined min/max range.
*
* @param duration to checked
* @return <code>true</code> if in time range
*/
public boolean isWithinRange(final Duration duration) {
return duration.compareTo(min) >= 0 && duration.compareTo(max) <= 0;
if (durationStr.charAt(0) == 'P') {
// Handle ISO-8601 format, e.g., "PT1H30M"
return Duration.parse(durationStr);
} else {
// Handle custom format, e.g., "01:00:00" or "01:01:50:50"
final String[] split = durationStr.split(":");
if (split.length == 1) { // ss
return Duration.ofSeconds(Long.parseLong(split[0]));
} else if (split.length == 2) { // mm:ss
return Duration
.ofMinutes(Long.parseLong(split[0]))
.plusSeconds(Long.parseLong(split[1]));
} else if (split.length == 3) { // HH:mm:ss
return Duration
.ofHours(Long.parseLong(split[0]))
.plusMinutes(Long.parseLong(split[1]))
.plusSeconds(Long.parseLong(split[2]));
} else if (split.length == 4) { // d:HH:mm:ss
return Duration
.ofDays(Long.parseLong(split[0]))
.plusHours(Long.parseLong(split[1]))
.plusMinutes(Long.parseLong(split[2]))
.plusSeconds(Long.parseLong(split[3]));
} else {
throw new IllegalArgumentException("No more then 4 chunks (split by ':') are allowed in duration");
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy.configuration;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.Value;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
@Value
public class PollingTime {
private static final Pattern OVERRIDE_PATTERN = Pattern.compile(
"\\s{0,5},\\s{0,5}(?<qlStr>[^,]*)\\s{0,5}->\\s{0,5}(?<pollInterval>" + PollingInterval.POLLING_INTERVALE_REGEX + ")\\s{0,5}");
PollingInterval pollingInterval;
List<Override> overrides;
public PollingTime(final String pollingTime) {
final int indexOfComma = pollingTime.indexOf(',');
if (indexOfComma == -1) { // no overrides
pollingInterval = new PollingInterval(pollingTime);
overrides = Collections.emptyList();
} else {
// Extract the main polling interval and overrides
final String pollingIntervalStr = pollingTime.substring(0, indexOfComma);
pollingInterval = new PollingInterval(pollingIntervalStr);
overrides = new ArrayList<>();
final String overridesStr = pollingTime.substring(indexOfComma).trim(); // with initial comma
final Matcher overridesMatcher = OVERRIDE_PATTERN.matcher(overridesStr);
for (int start = 0; start < overridesStr.length(); start = overridesMatcher.end()) {
if (overridesMatcher.find(start)) {
overrides.add(new Override(
overridesMatcher.group("qlStr").trim(),
new PollingInterval(overridesMatcher.group("pollInterval").trim())));
} else {
throw new TenantConfigurationValidatorException("Invalid pollingTime overrides: " + overridesStr);
}
}
}
}
@Value
public static class PollingInterval {
private static final Random RANDOM = new Random();
public static final String POLLING_INTERVALE_REGEX = "\\s{0,5}(?<pollingInterval>\\d{2}:[0-5]\\d:[0-5]\\d)\\s{0,5}(~(?<deviationPercent>\\d{1,2})%)?\\s{0,5}";
private static final Pattern POLLING_INTERVAL_PATTERN = Pattern.compile(POLLING_INTERVALE_REGEX);
Duration interval;
int deviationPercent;
public PollingInterval(final String pollingInterval) {
final Matcher matcher = POLLING_INTERVAL_PATTERN.matcher(pollingInterval);
if (matcher.matches()) {
try {
this.interval = DurationHelper.fromString(matcher.group("pollingInterval"));
} catch (final DateTimeParseException ex) {
throw new TenantConfigurationValidatorException(
"The given configuration value is expected as a string in the format HH:mm:ss(~\\d{1,2})?.");
}
this.deviationPercent = Optional.ofNullable(matcher.group("deviationPercent")).map(Integer::parseInt).orElse(0);
} else {
throw new TenantConfigurationValidatorException("Invalid pollingInterval: " + pollingInterval);
}
}
public String getFormattedIntervalWithDeviation(final Duration minPollingTime, final Duration maxPollingTime) {
if (deviationPercent > 0) {
final long millis = interval.toMillis();
final long maxDeviationMillis = (millis * deviationPercent) / 100;
final long deviation = RANDOM.nextLong(-maxDeviationMillis, maxDeviationMillis + 1);
if (deviation != 0) {
final Duration intervalWithDeviation = Duration.ofMillis(millis + deviation);
if (minPollingTime != null && intervalWithDeviation.compareTo(minPollingTime) < 0) {
return DurationHelper.toString(minPollingTime);
} else if (maxPollingTime != null && intervalWithDeviation.compareTo(maxPollingTime) > 0) {
return DurationHelper.toString(maxPollingTime);
} else {
return DurationHelper.toString(intervalWithDeviation);
}
}
}
return DurationHelper.toString(interval);
}
}
// This record holds the override information for a specific QL string and its associated polling interval.
public record Override(String qlStr, PollingInterval pollingInterval) {}
}

View File

@@ -67,50 +67,41 @@ public class TenantConfigurationProperties {
/**
* Header based authentication enabled.
*/
public static final String AUTHENTICATION_MODE_HEADER_ENABLED = "authentication.header.enabled";
public static final String AUTHENTICATION_HEADER_ENABLED = "authentication.header.enabled";
/**
* Header based authentication authority name.
*/
public static final String AUTHENTICATION_MODE_HEADER_AUTHORITY_NAME = "authentication.header.authority";
public static final String AUTHENTICATION_HEADER_AUTHORITY_NAME = "authentication.header.authority";
/**
* Target token based authentication enabled.
*/
public static final String AUTHENTICATION_MODE_TARGET_SECURITY_TOKEN_ENABLED = "authentication.targettoken.enabled";
public static final String AUTHENTICATION_TARGET_SECURITY_TOKEN_ENABLED = "authentication.targettoken.enabled";
/**
* Gateway token based authentication enabled.
*/
public static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled";
public static final String AUTHENTICATION_GATEWAY_SECURITY_TOKEN_ENABLED = "authentication.gatewaytoken.enabled";
/**
* Gateway token value.
*/
public static final String AUTHENTICATION_MODE_GATEWAY_SECURITY_TOKEN_KEY = "authentication.gatewaytoken.key";
public static final String AUTHENTICATION_GATEWAY_SECURITY_TOKEN_KEY = "authentication.gatewaytoken.key";
/**
* See system default in
* {@link ControllerPollProperties#getPollingTime()}.
* See system default in {@link ControllerPollProperties#getPollingTime()}.
*/
public static final String POLLING_TIME_INTERVAL = "pollingTime";
public static final String POLLING_TIME = "pollingTime";
/**
* See system default in
* {@link ControllerPollProperties#getMinPollingTime()}.
* See system default in {@link ControllerPollProperties#getPollingOverdueTime()}.
*/
public static final String MIN_POLLING_TIME_INTERVAL = "minPollingTime";
public static final String POLLING_OVERDUE_TIME = "pollingOverdueTime";
/**
* See system default in
* {@link ControllerPollProperties#getMaintenanceWindowPollCount()}.
* See system default in {@link ControllerPollProperties#getMaintenanceWindowPollCount()}.
*/
public static final String MAINTENANCE_WINDOW_POLL_COUNT = "maintenanceWindowPollCount";
/**
* See system default in
* {@link ControllerPollProperties#getPollingOverdueTime()}.
*/
public static final String POLLING_OVERDUE_TIME_INTERVAL = "pollingOverdueTime";
/**
* Represents setting if approval for a rollout is needed.
*/
public static final String ROLLOUT_APPROVAL_ENABLED = "rollout.approval.enabled";
/**
* Repository on autoclose mode instead of canceling in case of new DS
* assignment over active actions.
* Repository on autoclose mode instead of canceling in case of new Distribution Set assignment over active actions.
*/
public static final String REPOSITORY_ACTIONS_AUTOCLOSE_ENABLED = "repository.actions.autoclose.enabled";
/**
@@ -118,7 +109,7 @@ public class TenantConfigurationProperties {
*/
public static final String ACTION_CLEANUP_ENABLED = "action.cleanup.enabled";
/**
* Specifies the action expiry in milli-seconds.
* Specifies the action expiry in milliseconds.
*/
public static final String ACTION_CLEANUP_ACTION_EXPIRY = "action.cleanup.actionExpiry";
/**
@@ -156,11 +147,11 @@ public class TenantConfigurationProperties {
/**
* Validates if an object matches the allowed data format of the corresponding key
*
* @param context application context
* @param value which will be validated
* @param context application context
* @throws TenantConfigurationValidatorException is thrown, when object is invalid
*/
public void validate(final ApplicationContext context, final Object value) {
public void validate(final Object value, final ApplicationContext context) {
if (validator == null) {
Objects.requireNonNull(DEFAULT_TYPE_VALIDATORS.get(dataType), "No validator defined for " + keyName).validate(value);
} else {

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy.configuration.validator;
import static org.eclipse.hawkbit.tenancy.configuration.DurationHelper.fromString;
import java.time.Duration;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
/**
* This class is used to validate, that the property is a String and that it is in the correct duration format.
*/
public class TenantConfigurationDurationValidator extends TenantConfigurationStringValidator {
// Exception squid:S1166 - Hide origin exception
@SuppressWarnings({ "squid:S1166" })
@Override
public void validate(final Object tenantConfigurationObject) {
super.validate(tenantConfigurationObject);
final String tenantConfigurationString = (String) tenantConfigurationObject;
final Duration duration = fromString(tenantConfigurationString);
if (duration.isNegative()) {
throw new TenantConfigurationValidatorException("The given configuration value is not in the allowed to be negative.");
}
}
}

View File

@@ -1,67 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy.configuration.validator;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
/**
* This class is used to validate, that the property is a String and that it is in the correct duration format.
*/
public class TenantConfigurationPollingDurationValidator implements TenantConfigurationValidator {
private final Duration minDuration;
private final Duration maxDuration;
/**
* This constructor is called by {@link org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties} using
* ApplicationContext.getAutowireCapableBeanFactory().createBean(Class) to validate the polling duration configuration.
* This insures the wiring of the properties is done correctly.
*
* @param properties property accessor for poll configuration
*/
public TenantConfigurationPollingDurationValidator(final ControllerPollProperties properties) {
minDuration = DurationHelper.formattedStringToDuration(properties.getMinPollingTime());
maxDuration = DurationHelper.formattedStringToDuration(properties.getMaxPollingTime());
}
@Override
// Exception squid:S1166 - Hide origin exception
@SuppressWarnings({ "squid:S1166" })
public void validate(final Object tenantConfigurationObject) {
TenantConfigurationValidator.super.validate(tenantConfigurationObject);
final String tenantConfigurationString = (String) tenantConfigurationObject;
final Duration tenantConfigurationValue;
try {
tenantConfigurationValue = DurationHelper.formattedStringToDuration(tenantConfigurationString);
} catch (final DateTimeParseException ex) {
throw new TenantConfigurationValidatorException(
String.format("The given configuration value is expected as a string in the format %s.",
DurationHelper.DURATION_FORMAT));
}
if (!DurationHelper.durationRangeValidator(minDuration, maxDuration).isWithinRange(tenantConfigurationValue)) {
throw new TenantConfigurationValidatorException(
String.format("The given configuration value is not in the allowed range from %s to %s.",
DurationHelper.durationToFormattedString(minDuration),
DurationHelper.durationToFormattedString(maxDuration)));
}
}
@Override
public Class<?> validateToClass() {
return String.class;
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.tenancy.configuration.validator;
import static org.eclipse.hawkbit.tenancy.configuration.DurationHelper.fromString;
import java.time.Duration;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
import org.eclipse.hawkbit.tenancy.configuration.ControllerPollProperties;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
import org.eclipse.hawkbit.tenancy.configuration.PollingTime;
/**
* This class is used to validate, that the property is a String and that it is in the correct polling time format.
*/
public class TenantConfigurationPollingTimeValidator extends TenantConfigurationStringValidator {
private final Duration minPollingInterval;
private final Duration maxPollingInterval;
/**
* This constructor is called by {@link org.eclipse.hawkbit.tenancy.configuration.TenantConfigurationProperties} using
* ApplicationContext.getAutowireCapableBeanFactory().createBean(Class) to validate the polling duration configuration.
* This insures the wiring of the properties is done correctly.
*
* @param properties property accessor for poll configuration
*/
public TenantConfigurationPollingTimeValidator(final ControllerPollProperties properties) {
this.minPollingInterval = fromString(properties.getMinPollingTime());
this.maxPollingInterval = fromString(properties.getMaxPollingTime());
}
@Override
public void validate(final Object tenantConfigurationObject) {
super.validate(tenantConfigurationObject);
final String tenantConfigurationString = (String) tenantConfigurationObject;
// validate parsable
final PollingTime pollingTime = new PollingTime(tenantConfigurationString);
// validate polling interval in range
validateInRange(pollingTime.getPollingInterval().getInterval());
for (final PollingTime.Override override : pollingTime.getOverrides()) {
validateInRange(override.pollingInterval().getInterval());
}
}
private void validateInRange(final Duration pollingInterval) {
if (pollingInterval.compareTo(minPollingInterval) < 0) {
throw new TenantConfigurationValidatorException(String.format(
"The polling interval is smaller then minimum polling interval. The allowed range is [%s, %s].",
DurationHelper.toString(minPollingInterval), DurationHelper.toString(maxPollingInterval)));
}
if (pollingInterval.compareTo(maxPollingInterval) > 0) {
throw new TenantConfigurationValidatorException(String.format(
"The polling interval is bigger then minimum polling interval. The allowed range is [%s, %s].",
DurationHelper.toString(minPollingInterval), DurationHelper.toString(maxPollingInterval)));
}
}
}

View File

@@ -12,12 +12,12 @@ package org.eclipse.hawkbit.tenancy.configuration.validator;
import org.eclipse.hawkbit.repository.exception.TenantConfigurationValidatorException;
/**
* base interface for clases which can validate tenant configuration values.
* base interface for classes which can validate tenant configuration values.
*/
public interface TenantConfigurationValidator {
/**
* validates the tenant configuration value
* Validates the tenant configuration value
*
* @param tenantConfigurationValue value which will be validated.
* @throws TenantConfigurationValidatorException is thrown, when parameter is invalid.

View File

@@ -0,0 +1,64 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import org.eclipse.hawkbit.tenancy.configuration.DurationHelper;
import org.eclipse.hawkbit.tenancy.configuration.PollingTime;
import org.junit.jupiter.api.Test;
class PollingTimeTest {
@Test
void testBackwardsCompatibility() {
final PollingTime pollingTime = new PollingTime("01:00:00");
assertThat(pollingTime.getPollingInterval().getInterval()).hasToString("PT1H");
assertThat(pollingTime.getPollingInterval().getDeviationPercent()).isZero();
assertThat(pollingTime.getOverrides()).isEmpty();
}
@Test
void testDeviation() {
final PollingTime pollingTime = new PollingTime("01:00:00~10%");
final long maxDeviation = (Duration.ofHours(1).toMillis() / 10);
final long deviation = Duration.ofHours(1).toMillis()
- DurationHelper.fromString(pollingTime.getPollingInterval().getFormattedIntervalWithDeviation(null, null)).toMillis();
assertThat(deviation)
.isGreaterThanOrEqualTo(-maxDeviation)
.isLessThanOrEqualTo(maxDeviation);
}
@Test
void testComplexWithOverrides() {
assertExpectedComplexWithOverrides("01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync -> 00:05:00");
}
@Test
void testComplexWithOverridesWithWhitespaces() {
assertExpectedComplexWithOverrides("01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync ->00:05:00");
assertExpectedComplexWithOverrides(" 01:00:00~10%, group == 'eu' -> 00:02:00~15%, status != in_sync ->00:05:00 ");
assertExpectedComplexWithOverrides(" 01:00:00~10% , group == 'eu' -> 00:02:00 ~15%, status != in_sync ->00:05:00 ");
}
private static void assertExpectedComplexWithOverrides(final String pollingTimeStr) {
final PollingTime pollingTime = new PollingTime(pollingTimeStr);
assertThat(pollingTime.getPollingInterval().getInterval()).hasToString("PT1H");
assertThat(pollingTime.getPollingInterval().getDeviationPercent()).isEqualTo(10);
assertThat(pollingTime.getOverrides().get(0).qlStr()).isEqualTo("group == 'eu'");
assertThat(pollingTime.getOverrides().get(0).pollingInterval().getInterval()).hasToString("PT2M");
assertThat(pollingTime.getOverrides().get(0).pollingInterval().getDeviationPercent()).isEqualTo(15);
assertThat(pollingTime.getOverrides().get(1).qlStr()).isEqualTo("status != in_sync");
assertThat(pollingTime.getOverrides().get(1).pollingInterval().getInterval()).hasToString("PT5M");
assertThat(pollingTime.getOverrides().get(1).pollingInterval().getDeviationPercent()).isZero();
}
}