Added TimeUnit support

This commit is contained in:
dshvaika 2025-09-03 17:21:50 +03:00
parent 159d779d77
commit 3abb23780d
8 changed files with 106 additions and 20 deletions

View File

@ -60,7 +60,6 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Function; import java.util.function.Function;
@ -454,7 +453,7 @@ public class CalculatedFieldManagerMessageProcessor extends AbstractContextAware
log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId()); log.debug("[{}][{}] Dynamic arguments refresh task for CF already exists!", tenantId, cf.getId());
return; return;
} }
long refreshDynamicSourceInterval = TimeUnit.SECONDS.toMillis(scheduledCfConfig.getScheduledUpdateIntervalSec()); long refreshDynamicSourceInterval = scheduledCfConfig.getTimeUnit().toMillis(scheduledCfConfig.getScheduledUpdateInterval());
var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId()); var scheduledMsg = new CalculatedFieldDynamicArgumentsRefreshMsg(tenantId, cfCtx.getCfId());
ScheduledFuture<?> scheduledFuture = systemContext ScheduledFuture<?> scheduledFuture = systemContext

View File

@ -325,8 +325,9 @@ public class CalculatedFieldCtx {
if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig if (calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration thisConfig
&& other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) { && other.calculatedField.getConfiguration() instanceof ScheduledUpdateSupportedCalculatedFieldConfiguration otherConfig) {
boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled(); boolean refreshTriggerChanged = thisConfig.isScheduledUpdateEnabled() != otherConfig.isScheduledUpdateEnabled();
boolean refreshIntervalChanged = thisConfig.getScheduledUpdateIntervalSec() != otherConfig.getScheduledUpdateIntervalSec(); boolean refreshIntervalChanged = thisConfig.getScheduledUpdateInterval() != otherConfig.getScheduledUpdateInterval();
return refreshTriggerChanged || refreshIntervalChanged; boolean timeUnitChanged = thisConfig.getTimeUnit() != otherConfig.getTimeUnit();
return refreshTriggerChanged || refreshIntervalChanged || timeUnitChanged;
} }
return false; return false;
} }

View File

@ -799,7 +799,8 @@ public class CalculatedFieldIntegrationTest extends CalculatedFieldControllerTes
cfg.setOutput(out); cfg.setOutput(out);
// Enable scheduled refresh with a 6-second interval // Enable scheduled refresh with a 6-second interval
cfg.setScheduledUpdateIntervalSec(6); cfg.setScheduledUpdateInterval(6);
cfg.setTimeUnit(TimeUnit.SECONDS);
cf.setConfiguration(cfg); cf.setConfiguration(cfg);
CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class); CalculatedField savedCalculatedField = doPost("/api/calculatedField", cf, CalculatedField.class);

View File

@ -28,13 +28,15 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
@Data @Data
public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration { public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCalculatedFieldConfiguration, ScheduledUpdateSupportedCalculatedFieldConfiguration {
private EntityCoordinates entityCoordinates; private EntityCoordinates entityCoordinates;
private List<ZoneGroupConfiguration> zoneGroups; private List<ZoneGroupConfiguration> zoneGroups;
private int scheduledUpdateIntervalSec; private int scheduledUpdateInterval;
private TimeUnit timeUnit;
private Output output; private Output output;
@ -63,11 +65,12 @@ public class GeofencingCalculatedFieldConfiguration implements ArgumentsBasedCal
@Override @Override
public boolean isScheduledUpdateEnabled() { public boolean isScheduledUpdateEnabled() {
return scheduledUpdateIntervalSec > 0 && zoneGroups.stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource); return scheduledUpdateInterval > 0 && zoneGroups.stream().anyMatch(ZoneGroupConfiguration::hasDynamicSource);
} }
@Override @Override
public void validate() { public void validate() {
ScheduledUpdateSupportedCalculatedFieldConfiguration.super.validate();
if (entityCoordinates == null) { if (entityCoordinates == null) {
throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!"); throw new IllegalArgumentException("Geofencing calculated field entity coordinates must be specified!");
} }

View File

@ -17,12 +17,39 @@ package org.thingsboard.server.common.data.cf.configuration;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration { public interface ScheduledUpdateSupportedCalculatedFieldConfiguration extends CalculatedFieldConfiguration {
Set<TimeUnit> SUPPORTED_TIME_UNITS =
EnumSet.of(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS);
@JsonIgnore @JsonIgnore
boolean isScheduledUpdateEnabled(); boolean isScheduledUpdateEnabled();
int getScheduledUpdateIntervalSec(); int getScheduledUpdateInterval();
void setScheduledUpdateIntervalSec(int interval); void setScheduledUpdateInterval(int interval);
TimeUnit getTimeUnit();
void setTimeUnit(TimeUnit timeUnit);
@Override
default void validate() {
if (!isScheduledUpdateEnabled()) {
return;
}
var timeUnit = getTimeUnit();
if (timeUnit == null) {
throw new IllegalArgumentException("Scheduled update time unit should be specified!");
}
if (!SUPPORTED_TIME_UNITS.contains(timeUnit)) {
throw new IllegalArgumentException("Unsupported scheduled update time unit: " + timeUnit +
". Allowed: " + SUPPORTED_TIME_UNITS);
}
}
} }

View File

@ -17,6 +17,8 @@ package org.thingsboard.server.common.data.cf.configuration;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.server.common.data.AttributeScope; import org.thingsboard.server.common.data.AttributeScope;
import org.thingsboard.server.common.data.cf.CalculatedFieldType; import org.thingsboard.server.common.data.cf.CalculatedFieldType;
@ -25,6 +27,7 @@ import org.thingsboard.server.common.data.cf.configuration.geofencing.ZoneGroupC
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatCode;
@ -33,6 +36,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.thingsboard.server.common.data.cf.configuration.ScheduledUpdateSupportedCalculatedFieldConfiguration.SUPPORTED_TIME_UNITS;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LATITUDE_ARGUMENT_KEY;
import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY; import static org.thingsboard.server.common.data.cf.configuration.geofencing.EntityCoordinates.ENTITY_ID_LONGITUDE_ARGUMENT_KEY;
@ -122,10 +126,50 @@ public class GeofencingCalculatedFieldConfigurationTest {
verify(zoneGroupConfigurationB, never()).validate(); verify(zoneGroupConfigurationB, never()).validate();
} }
@Test
void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSpecified() {
var cfg = new GeofencingCalculatedFieldConfiguration();
cfg.setScheduledUpdateInterval(60);
var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class));
cfg.setZoneGroups(List.of(zg));
cfg.setTimeUnit(null);
assertThat(cfg.isScheduledUpdateEnabled()).isTrue();
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Scheduled update time unit should be specified!");
}
@ParameterizedTest
@EnumSource(TimeUnit.class)
void validateShouldThrowWhenScheduledUpdateIntervalIsSetButTimeUnitIsNotSupported(TimeUnit timeUnit) {
var cfg = new GeofencingCalculatedFieldConfiguration();
cfg.setScheduledUpdateInterval(60);
var zg = new ZoneGroupConfiguration("allowedZones", "perimeter", GeofencingReportStrategy.REPORT_TRANSITION_EVENTS_AND_PRESENCE_STATUS, false);
zg.setRefDynamicSourceConfiguration(mock(RelationQueryDynamicSourceConfiguration.class));
cfg.setZoneGroups(List.of(zg));
cfg.setEntityCoordinates(mock(EntityCoordinates.class));
cfg.setTimeUnit(timeUnit);
assertThat(cfg.isScheduledUpdateEnabled()).isTrue();
if (SUPPORTED_TIME_UNITS.contains(timeUnit)) {
assertThatCode(cfg::validate).doesNotThrowAnyException();
return;
}
assertThatThrownBy(cfg::validate)
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Unsupported scheduled update time unit: " + timeUnit +
". Allowed: " + SUPPORTED_TIME_UNITS);
}
@Test @Test
void scheduledUpdateDisabledWhenIntervalIsZero() { void scheduledUpdateDisabledWhenIntervalIsZero() {
var cfg = new GeofencingCalculatedFieldConfiguration(); var cfg = new GeofencingCalculatedFieldConfiguration();
cfg.setScheduledUpdateIntervalSec(0); cfg.setScheduledUpdateInterval(0);
assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); assertThat(cfg.isScheduledUpdateEnabled()).isFalse();
} }
@ -135,17 +179,18 @@ public class GeofencingCalculatedFieldConfigurationTest {
var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class);
when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false); when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(false);
cfg.setZoneGroups(List.of(zoneGroupConfigurationMock)); cfg.setZoneGroups(List.of(zoneGroupConfigurationMock));
cfg.setScheduledUpdateIntervalSec(60); cfg.setScheduledUpdateInterval(60);
assertThat(cfg.isScheduledUpdateEnabled()).isFalse(); assertThat(cfg.isScheduledUpdateEnabled()).isFalse();
} }
@Test @Test
void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() { void scheduledUpdateEnabledWhenIntervalIsGreaterThanZeroAndDynamicArgumentsPresent() {
var cfg = new GeofencingCalculatedFieldConfiguration(); var cfg = new GeofencingCalculatedFieldConfiguration();
cfg.setTimeUnit(TimeUnit.SECONDS);
var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class); var zoneGroupConfigurationMock = mock(ZoneGroupConfiguration.class);
when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true); when(zoneGroupConfigurationMock.hasDynamicSource()).thenReturn(true);
cfg.setZoneGroups(List.of(zoneGroupConfigurationMock)); cfg.setZoneGroups(List.of(zoneGroupConfigurationMock));
cfg.setScheduledUpdateIntervalSec(60); cfg.setScheduledUpdateInterval(60);
assertThat(cfg.isScheduledUpdateEnabled()).isTrue(); assertThat(cfg.isScheduledUpdateEnabled()).isTrue();
} }

View File

@ -38,6 +38,7 @@ import org.thingsboard.server.dao.service.DataValidator;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -98,10 +99,15 @@ public class BaseCalculatedFieldService extends AbstractEntityService implements
if (!configuration.isScheduledUpdateEnabled()) { if (!configuration.isScheduledUpdateEnabled()) {
return; return;
} }
int tenantProfileMinAllowedValue = tbTenantProfileCache.get(calculatedField.getTenantId()) TimeUnit timeUnit = configuration.getTimeUnit();
long intervalInSeconds = timeUnit.toSeconds(configuration.getScheduledUpdateInterval());
int tenantProfileMinAllowedSecValue = tbTenantProfileCache.get(calculatedField.getTenantId())
.getDefaultProfileConfiguration() .getDefaultProfileConfiguration()
.getMinAllowedScheduledUpdateIntervalInSecForCF(); .getMinAllowedScheduledUpdateIntervalInSecForCF();
configuration.setScheduledUpdateIntervalSec(Math.max(configuration.getScheduledUpdateIntervalSec(), tenantProfileMinAllowedValue)); if (intervalInSeconds < tenantProfileMinAllowedSecValue) {
configuration.setScheduledUpdateInterval(tenantProfileMinAllowedSecValue);
configuration.setTimeUnit(TimeUnit.SECONDS);
}
} }
} }

View File

@ -46,6 +46,7 @@ import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
@ -117,7 +118,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
cfg.setZoneGroups(List.of(zoneGroupConfiguration)); cfg.setZoneGroups(List.of(zoneGroupConfiguration));
// Set a scheduled interval to some value // Set a scheduled interval to some value
cfg.setScheduledUpdateIntervalSec(600); cfg.setScheduledUpdateInterval(600);
cfg.setTimeUnit(TimeUnit.SECONDS);
// Create & save Calculated Field // Create & save Calculated Field
CalculatedField cf = new CalculatedField(); CalculatedField cf = new CalculatedField();
@ -136,7 +138,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration();
// Assert: the interval is saved, but scheduling is not enabled // Assert: the interval is saved, but scheduling is not enabled
int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval();
boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled(); boolean scheduledUpdateEnabled = geofencingCalculatedFieldConfiguration.isScheduledUpdateEnabled();
assertThat(savedInterval).isEqualTo(600); assertThat(savedInterval).isEqualTo(600);
@ -167,7 +169,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
cfg.setZoneGroups(List.of(zoneGroupConfiguration)); cfg.setZoneGroups(List.of(zoneGroupConfiguration));
// Enable scheduling with an interval below tenant min // Enable scheduling with an interval below tenant min
cfg.setScheduledUpdateIntervalSec(600); cfg.setScheduledUpdateInterval(600);
cfg.setTimeUnit(TimeUnit.SECONDS);
// Create & save Calculated Field // Create & save Calculated Field
CalculatedField cf = new CalculatedField(); CalculatedField cf = new CalculatedField();
@ -186,7 +189,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration();
// Assert: the interval is clamped up to tenant profile min // Assert: the interval is clamped up to tenant profile min
int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval();
int min = tbTenantProfileCache.get(tenantId) int min = tbTenantProfileCache.get(tenantId)
.getDefaultProfileConfiguration() .getDefaultProfileConfiguration()
@ -225,7 +228,8 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
// Enable scheduling with an interval greater than tenant min // Enable scheduling with an interval greater than tenant min
int valueFromConfig = min + 100; int valueFromConfig = min + 100;
cfg.setScheduledUpdateIntervalSec(valueFromConfig); cfg.setScheduledUpdateInterval(valueFromConfig);
cfg.setTimeUnit(TimeUnit.SECONDS);
// Create & save Calculated Field // Create & save Calculated Field
CalculatedField cf = new CalculatedField(); CalculatedField cf = new CalculatedField();
@ -244,7 +248,7 @@ public class CalculatedFieldServiceTest extends AbstractServiceTest {
var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration(); var geofencingCalculatedFieldConfiguration = (GeofencingCalculatedFieldConfiguration) saved.getConfiguration();
// Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min) // Assert: the interval is clamped up to tenant profile min (or stays >= original if already >= min)
int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateIntervalSec(); int savedInterval = geofencingCalculatedFieldConfiguration.getScheduledUpdateInterval();
assertThat(savedInterval).isEqualTo(valueFromConfig); assertThat(savedInterval).isEqualTo(valueFromConfig);
calculatedFieldService.deleteCalculatedField(tenantId, saved.getId()); calculatedFieldService.deleteCalculatedField(tenantId, saved.getId());