From c01302fe6bd349f5a8524ce8e9ec3133a4d7f50f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Tue, 21 Oct 2025 18:20:45 +0300 Subject: [PATCH 1/8] added lock on entity creation to fix race condition on multiple entity creation --- .../server/dao/asset/BaseAssetService.java | 4 ++ .../dao/customer/CustomerServiceImpl.java | 2 +- .../dao/dashboard/DashboardServiceImpl.java | 4 ++ .../server/dao/device/DeviceServiceImpl.java | 4 ++ .../server/dao/edge/EdgeServiceImpl.java | 4 ++ .../dao/entity/AbstractEntityService.java | 23 ++++++++ .../server/dao/rule/BaseRuleChainService.java | 4 ++ .../server/dao/user/UserServiceImpl.java | 4 ++ .../dao/service/AbstractServiceTest.java | 6 ++ .../server/dao/service/AssetServiceTest.java | 57 +++++++++++++++++++ .../server/dao/service/DeviceServiceTest.java | 31 ++++++++++ 11 files changed, 142 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index fed201d403..3e49c6d6b3 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -148,6 +148,10 @@ public class BaseAssetService extends AbstractCachedEntityService doSaveAsset(asset, doValidate)); + } + + private Asset doSaveAsset(Asset asset, boolean doValidate) { log.trace("Executing saveAsset [{}]", asset); Asset oldAsset = null; if (doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index b06902d95e..fa1de490fa 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -139,7 +139,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true)); } private Customer saveCustomer(Customer customer, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index 1b21310237..b044f8fbe7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -157,6 +157,10 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) { + return saveLimitedEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate)); + } + + private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) { log.trace("Executing saveDashboard [{}]", dashboard); if (doValidate) { dashboardValidator.validate(dashboard, DashboardInfo::getTenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index 6d993f3e3d..cd64ddccda 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -210,6 +210,10 @@ public class DeviceServiceImpl extends CachedVersionedEntityService doSaveDeviceWithoutCredentials(device, doValidate)); + } + + private Device doSaveDeviceWithoutCredentials(Device device, boolean doValidate) { log.trace("Executing saveDevice [{}]", device); Device oldDevice = null; if (doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index 0655d05572..b71decf5f6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -201,6 +201,10 @@ public class EdgeServiceImpl extends AbstractCachedEntityService doSaveEdge(edge)); + } + + private Edge doSaveEdge(Edge edge) { log.trace("Executing saveEdge [{}]", edge); Edge oldEdge = edgeValidator.validate(edge, Edge::getTenantId); EdgeCacheEvictEvent evictEvent = new EdgeCacheEvictEvent(edge.getTenantId(), edge.getName(), oldEdge != null ? oldEdge.getName() : null); diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 7560c7fb76..8a8f74671b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -21,13 +21,16 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; +import org.springframework.util.ConcurrentReferenceHashMap; import org.thingsboard.common.util.DebugModeUtil; import org.thingsboard.server.common.data.EntityView; import org.thingsboard.server.common.data.HasDebugSettings; +import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.debug.DebugSettings; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; @@ -44,7 +47,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; @Slf4j public abstract class AbstractEntityService { @@ -52,6 +58,8 @@ public abstract class AbstractEntityService { public static final String INCORRECT_EDGE_ID = "Incorrect edgeId "; public static final String INCORRECT_PAGE_LINK = "Incorrect page link "; + private final ConcurrentMap entityCreationLocks = new ConcurrentReferenceHashMap<>(16); + @Autowired protected ApplicationEventPublisher eventPublisher; @@ -86,6 +94,21 @@ public abstract class AbstractEntityService { @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; + protected E saveLimitedEntity(E entity, Supplier saveFunction) { + log.debug("Creating limited entity: {}", entity); + if (entity.getId() == null) { + ReentrantLock lock = entityCreationLocks.computeIfAbsent(entity.getTenantId(), id -> new ReentrantLock()); + lock.lock(); + try { + return saveFunction.get(); + } finally { + lock.unlock(); + } + } else { + return saveFunction.get(); + } + } + protected void createRelation(TenantId tenantId, EntityRelation relation) { log.debug("Creating relation: {}", relation); relationService.saveRelation(tenantId, relation); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 8538bd9492..47e82f7df1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -125,6 +125,10 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { + return saveLimitedEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, true)); + } + + private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { log.trace("Executing doSaveRuleChain [{}]", ruleChain); if (doValidate) { ruleChainValidator.validate(ruleChain, RuleChain::getTenantId); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 5c94ba1891..8b92c5f8ac 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -159,6 +159,10 @@ public class UserServiceImpl extends AbstractCachedEntityService doSaveUser(tenantId, user)); + } + + private User doSaveUser(TenantId tenantId, User user) { log.trace("Executing saveUser [{}]", user); User oldUser = userValidator.validate(user, User::getTenantId); if (!userLoginCaseSensitive) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java index 25339244fd..0d2b9b5d9f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AbstractServiceTest.java @@ -48,6 +48,7 @@ import org.thingsboard.server.common.data.id.DeviceProfileId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.oauth2.MapperType; import org.thingsboard.server.common.data.oauth2.OAuth2Client; import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig; @@ -185,8 +186,13 @@ public abstract class AbstractServiceTest { } public Tenant createTenant() { + return createTenant(null); + } + + public Tenant createTenant(TenantProfileId tenantProfileId) { Tenant tenant = new Tenant(); tenant.setTitle("My tenant " + UUID.randomUUID()); + tenant.setTenantProfileId(tenantProfileId); Tenant savedTenant = tenantService.saveTenant(tenant); assertNotNull(savedTenant); return savedTenant; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index 462e7a894c..ccd0a1ca9b 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -16,17 +16,25 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.testcontainers.shaded.org.apache.commons.lang3.RandomStringUtils; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.asset.AssetInfo; import org.thingsboard.server.common.data.asset.AssetProfile; @@ -44,6 +52,8 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.asset.AssetDao; import org.thingsboard.server.dao.asset.AssetProfileService; import org.thingsboard.server.dao.asset.AssetService; @@ -51,11 +61,14 @@ import org.thingsboard.server.dao.cf.CalculatedFieldService; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.relation.RelationService; +import org.thingsboard.server.dao.tenant.TenantProfileService; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -72,6 +85,8 @@ public class AssetServiceTest extends AbstractServiceTest { @Autowired RelationService relationService; @Autowired + TenantProfileService tenantProfileService; + @Autowired private AssetProfileService assetProfileService; @Autowired private CalculatedFieldService calculatedFieldService; @@ -79,6 +94,18 @@ public class AssetServiceTest extends AbstractServiceTest { private PlatformTransactionManager platformTransactionManager; private IdComparator idComparator = new IdComparator<>(); + ListeningExecutorService executor; + private TenantId anotherTenantId; + + @Before + public void before() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); + } + + @After + public void after() { + executor.shutdownNow(); + } @Test public void testSaveAsset() { @@ -105,6 +132,36 @@ public class AssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, savedAsset.getId()); } + @Test + public void testAssetLimitOnTenantProfileLevel() { + TenantProfile tenantProfile = new TenantProfile(); + tenantProfile.setName("Test profile"); + tenantProfile.setDescription("Test"); + TenantProfileData profileData = new TenantProfileData(); + profileData.setConfiguration(DefaultTenantProfileConfiguration.builder().maxAssets(5l).build()); + tenantProfile.setProfileData(profileData); + tenantProfile.setDefault(false); + tenantProfile.setIsolatedTbRuleEngine(false); + + tenantProfile = tenantProfileService.saveTenantProfile(anotherTenantId, tenantProfile); + anotherTenantId = createTenant(tenantProfile.getId()).getId(); + + for (int i = 0; i < 20; i++) { + executor.submit(() -> { + Asset asset = new Asset(); + asset.setTenantId(anotherTenantId); + asset.setName(RandomStringUtils.randomAlphabetic(10)); + asset.setType("default"); + assetService.saveAsset(asset); + }); + } + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { + long countByTenantId = assetService.countByTenantId(anotherTenantId); + return countByTenantId == 5; + }); + } + @Test public void testShouldNotPutInCacheRolledbackAssetProfile() { AssetProfile assetProfile = new AssetProfile(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 32767043d7..257eed8232 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -16,6 +16,8 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; +import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -27,6 +29,8 @@ import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -74,6 +78,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; @@ -105,10 +111,12 @@ public class DeviceServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; + private ListeningExecutorService executor; @Before public void before() { anotherTenantId = createTenant().getId(); + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); } @After @@ -118,6 +126,7 @@ public class DeviceServiceTest extends AbstractServiceTest { tenantProfileService.deleteTenantProfiles(tenantId); tenantProfileService.deleteTenantProfiles(anotherTenantId); + executor.shutdownNow(); } @Test @@ -136,6 +145,28 @@ public class DeviceServiceTest extends AbstractServiceTest { deleteDevice(tenantId, device); } + @Test + public void testDeviceLimitOnTenantProfileLevel() { + TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); + defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(5l).build()); + tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); + + for (int i = 0; i < 20; i++) { + executor.submit(() -> { + Device device = new Device(); + device.setTenantId(tenantId); + device.setName(StringUtils.randomAlphabetic(10)); + device.setType("default"); + deviceService.saveDevice(device, true); + }); + } + + Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> { + long countByTenantId = deviceService.countByTenantId(tenantId); + return countByTenantId == 5; + }); + } + @Test public void testSaveDevicesWithMaxDeviceOutOfLimit() { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); From e8e8ca13ecf81a7d5f53a8ef8c6bd8bf52ce0d77 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 22 Oct 2025 17:24:40 +0300 Subject: [PATCH 2/8] refactoring --- .../server/dao/asset/BaseAssetService.java | 2 +- .../dao/customer/CustomerServiceImpl.java | 2 +- .../dao/dashboard/DashboardServiceImpl.java | 2 +- .../server/dao/device/DeviceServiceImpl.java | 2 +- .../server/dao/edge/EdgeServiceImpl.java | 2 +- .../dao/entity/AbstractEntityService.java | 3 +-- .../server/dao/rule/BaseRuleChainService.java | 2 +- .../server/dao/user/UserServiceImpl.java | 2 +- .../server/dao/service/AssetServiceTest.java | 17 +++++++++-------- .../server/dao/service/DeviceServiceTest.java | 16 +++++++++++++--- 10 files changed, 30 insertions(+), 20 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 3e49c6d6b3..755a39fddd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -148,7 +148,7 @@ public class BaseAssetService extends AbstractCachedEntityService doSaveAsset(asset, doValidate)); + return saveEntity(asset, () -> doSaveAsset(asset, doValidate)); } private Asset doSaveAsset(Asset asset, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index fa1de490fa..2e77c5b866 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -139,7 +139,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true)); + return saveEntity(customer, () -> saveCustomer(customer, true)); } private Customer saveCustomer(Customer customer, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java index b044f8fbe7..b41e4053eb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/dashboard/DashboardServiceImpl.java @@ -157,7 +157,7 @@ public class DashboardServiceImpl extends AbstractEntityService implements Dashb @Override public Dashboard saveDashboard(Dashboard dashboard, boolean doValidate) { - return saveLimitedEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate)); + return saveEntity(dashboard, () -> doSaveDashboard(dashboard, doValidate)); } private Dashboard doSaveDashboard(Dashboard dashboard, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java index cd64ddccda..d387ac82a0 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceServiceImpl.java @@ -210,7 +210,7 @@ public class DeviceServiceImpl extends CachedVersionedEntityService doSaveDeviceWithoutCredentials(device, doValidate)); + return saveEntity(device, () -> doSaveDeviceWithoutCredentials(device, doValidate)); } private Device doSaveDeviceWithoutCredentials(Device device, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java index b71decf5f6..ee3ac36c31 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/edge/EdgeServiceImpl.java @@ -201,7 +201,7 @@ public class EdgeServiceImpl extends AbstractCachedEntityService doSaveEdge(edge)); + return saveEntity(edge, () -> doSaveEdge(edge)); } private Edge doSaveEdge(Edge edge) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java index 8a8f74671b..3f653c673b 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/AbstractEntityService.java @@ -94,8 +94,7 @@ public abstract class AbstractEntityService { @Value("${debug.settings.default_duration:15}") private int defaultDebugDurationMinutes; - protected E saveLimitedEntity(E entity, Supplier saveFunction) { - log.debug("Creating limited entity: {}", entity); + protected E saveEntity(E entity, Supplier saveFunction) { if (entity.getId() == null) { ReentrantLock lock = entityCreationLocks.computeIfAbsent(entity.getTenantId(), id -> new ReentrantLock()); lock.lock(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java index 47e82f7df1..97a732f94a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java @@ -125,7 +125,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC @Override @Transactional public RuleChain saveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { - return saveLimitedEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, true)); + return saveEntity(ruleChain, () -> doSaveRuleChain(ruleChain, publishSaveEvent, doValidate)); } private RuleChain doSaveRuleChain(RuleChain ruleChain, boolean publishSaveEvent, boolean doValidate) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 8b92c5f8ac..70c356eb45 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -159,7 +159,7 @@ public class UserServiceImpl extends AbstractCachedEntityService doSaveUser(tenantId, user)); + return saveEntity(user, () -> doSaveUser(tenantId, user)); } private User doSaveUser(TenantId tenantId, User user) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index ccd0a1ca9b..9ad8b42c21 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -18,9 +18,9 @@ package org.thingsboard.server.dao.service; import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; -import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; @@ -93,17 +93,18 @@ public class AssetServiceTest extends AbstractServiceTest { @Autowired private PlatformTransactionManager platformTransactionManager; + private static ListeningExecutorService executor; + private IdComparator idComparator = new IdComparator<>(); - ListeningExecutorService executor; private TenantId anotherTenantId; - @Before - public void before() { - executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); + @BeforeClass + public static void before() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("AssetServiceTestScope"))); } - @After - public void after() { + @AfterClass + public static void after() { executor.shutdownNow(); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 257eed8232..16bd851641 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -19,8 +19,10 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.jupiter.api.Assertions; import org.mockito.Mockito; @@ -111,12 +113,21 @@ public class DeviceServiceTest extends AbstractServiceTest { private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; - private ListeningExecutorService executor; + private static ListeningExecutorService executor; + + @BeforeClass + public static void beforeClass() { + executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName("DeviceServiceTestScope"))); + } + + @AfterClass + public static void afterClass() { + executor.shutdownNow(); + } @Before public void before() { anotherTenantId = createTenant().getId(); - executor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10, ThingsBoardThreadFactory.forName(getClass().getSimpleName() + "-test-scope"))); } @After @@ -126,7 +137,6 @@ public class DeviceServiceTest extends AbstractServiceTest { tenantProfileService.deleteTenantProfiles(tenantId); tenantProfileService.deleteTenantProfiles(anotherTenantId); - executor.shutdownNow(); } @Test From 5b479e7298176181f012da9a427059364cabfffc Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Nov 2025 14:27:36 +0200 Subject: [PATCH 3/8] test improvements --- .../thingsboard/server/dao/service/AssetServiceTest.java | 6 +++++- .../thingsboard/server/dao/service/DeviceServiceTest.java | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java index 9ad8b42c21..b7f208b8c7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/AssetServiceTest.java @@ -70,6 +70,7 @@ import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -134,7 +135,7 @@ public class AssetServiceTest extends AbstractServiceTest { } @Test - public void testAssetLimitOnTenantProfileLevel() { + public void testAssetLimitOnTenantProfileLevel() throws InterruptedException { TenantProfile tenantProfile = new TenantProfile(); tenantProfile.setName("Test profile"); tenantProfile.setDescription("Test"); @@ -161,6 +162,9 @@ public class AssetServiceTest extends AbstractServiceTest { long countByTenantId = assetService.countByTenantId(anotherTenantId); return countByTenantId == 5; }); + + Thread.sleep(2000); + assertThat(assetService.countByTenantId(anotherTenantId)).isEqualTo(5); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java index 16bd851641..1ad1ec082a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DeviceServiceTest.java @@ -83,6 +83,7 @@ import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; @@ -156,7 +157,7 @@ public class DeviceServiceTest extends AbstractServiceTest { } @Test - public void testDeviceLimitOnTenantProfileLevel() { + public void testDeviceLimitOnTenantProfileLevel() throws InterruptedException { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(5l).build()); tenantProfileService.saveTenantProfile(tenantId, defaultTenantProfile); @@ -175,6 +176,9 @@ public class DeviceServiceTest extends AbstractServiceTest { long countByTenantId = deviceService.countByTenantId(tenantId); return countByTenantId == 5; }); + + Thread.sleep(2000); + assertThat(deviceService.countByTenantId(tenantId)).isEqualTo(5); } @Test From 2c3fc140d1cdd36ca473a3bac4d68b5dfb852203 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Nov 2025 15:34:46 +0200 Subject: [PATCH 4/8] fixes after merge --- .../org/thingsboard/server/dao/asset/BaseAssetService.java | 2 +- .../thingsboard/server/dao/customer/CustomerServiceImpl.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java index 8bbefc59f9..7d8442fd3a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/BaseAssetService.java @@ -151,7 +151,7 @@ public class BaseAssetService extends AbstractCachedEntityService saveAsset(asset, true, NameConflictStrategy.DEFAULT)); + return saveEntity(asset, () -> saveAsset(asset, true, nameConflictStrategy)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index f8d36fe02f..70d20852bd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -153,6 +153,9 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true, nameConflictStrategy)); } + private Customer saveCustomer(Customer customer, boolean doValidate) { + return saveCustomer(customer, doValidate, NameConflictStrategy.DEFAULT); + } private Customer saveCustomer(Customer customer, boolean doValidate, NameConflictStrategy nameConflictStrategy) { log.trace("Executing saveCustomer [{}]", customer); From 78c52e3141308787c1fe628854db1e358a803277 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Wed, 12 Nov 2025 15:42:44 +0200 Subject: [PATCH 5/8] fixes after merge --- .../thingsboard/server/dao/customer/CustomerServiceImpl.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 70d20852bd..3c3389baf2 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -143,8 +143,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true)); } @Override From 2a8f2a8e73e409fb4a831a111d551773193097aa Mon Sep 17 00:00:00 2001 From: ArtemDzhereleiko Date: Wed, 12 Nov 2025 16:05:26 +0200 Subject: [PATCH 6/8] UI: add translate for email description force 2fa --- .../pages/login/force-two-factor-auth-login.component.html | 2 +- ui-ngx/src/assets/locale/locale.constant-en_US.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html index 30f34b1d63..39236ef8e2 100644 --- a/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/force-two-factor-auth-login.component.html @@ -157,7 +157,7 @@
-
+

login.email-description

Date: Thu, 13 Nov 2025 12:10:04 +0200 Subject: [PATCH 7/8] refactoring --- .../thingsboard/server/dao/customer/CustomerServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java index 3c3389baf2..4187a96acc 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/customer/CustomerServiceImpl.java @@ -143,7 +143,7 @@ public class CustomerServiceImpl extends AbstractCachedEntityService saveCustomer(customer, true)); + return saveCustomer(customer, NameConflictStrategy.DEFAULT); } @Override From 61c695238daef10c1ffd164804c797a890c6d91e Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Thu, 13 Nov 2025 16:03:06 +0200 Subject: [PATCH 8/8] Fix alarm details in notification rules test --- .../server/service/notification/NotificationRuleApiTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java index c3a2119cbc..ff49a68d66 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java @@ -952,7 +952,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest { argument.setRefEntityKey(new ReferencedEntityKey("createAlarm", ArgumentType.ATTRIBUTE, AttributeScope.SERVER_SCOPE)); configuration.setArguments(Map.of("createAlarm", argument)); AlarmRule alarmRule = new AlarmRule(); - alarmRule.setAlarmDetails("attribute is ${bool}"); + alarmRule.setAlarmDetails("attribute is ${createAlarm}"); SimpleAlarmCondition condition = new SimpleAlarmCondition(); TbelAlarmConditionExpression expression = new TbelAlarmConditionExpression(); expression.setExpression("return createAlarm == true;");