From 7d1e12d4b18a46350585971d9006932514f5b1c7 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Thu, 9 Nov 2023 15:15:12 +0200 Subject: [PATCH 01/10] fixed save method to not put into cache --- .../dao/asset/AssetProfileServiceImpl.java | 9 +++-- .../dao/device/DeviceProfileServiceImpl.java | 8 ++-- .../server/dao/device/DeviceServiceImpl.java | 6 ++- .../dao/entityview/EntityViewServiceImpl.java | 4 +- .../server/dao/service/AssetServiceTest.java | 39 +++++++++++++++++++ .../server/dao/service/DeviceServiceTest.java | 29 ++++++++++++++ 6 files changed, 83 insertions(+), 12 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 9240bbc223..fd17c484d7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -126,8 +126,8 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService idComparator = new IdComparator<>(); @@ -75,6 +91,29 @@ public class AssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, savedAsset.getId()); } + @Test + public void testShouldNotPutInCacheRolledbackAssetProfile() { + AssetProfile assetProfile = new AssetProfile(); + assetProfile.setName(StringUtils.randomAlphabetic(10)); + assetProfile.setTenantId(tenantId); + + Asset asset = new Asset(); + asset.setName("My asset" + StringUtils.randomAlphabetic(15)); + asset.setType(assetProfile.getName()); + asset.setTenantId(tenantId); + + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + TransactionStatus status = platformTransactionManager.getTransaction(def); + try { + assetProfileService.saveAssetProfile(assetProfile); + assetService.saveAsset(asset); + } finally { + platformTransactionManager.rollback(status); + } + AssetProfile assetProfileByName = assetProfileService.findAssetProfileByName(tenantId, assetProfile.getName()); + Assert.assertNull(assetProfileByName); + } + @Test public void testSaveAssetWithEmptyName() { Asset asset = new Asset(); 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 1073f20be4..3e404f8d1d 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 @@ -22,6 +22,9 @@ 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.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -32,6 +35,8 @@ import org.thingsboard.server.common.data.OtaPackage; 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.AssetProfile; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.ota.ChecksumAlgorithm; @@ -72,6 +77,8 @@ public class DeviceServiceTest extends AbstractServiceTest { OtaPackageService otaPackageService; @Autowired TenantProfileService tenantProfileService; + @Autowired + private PlatformTransactionManager platformTransactionManager; private IdComparator idComparator = new IdComparator<>(); private TenantId anotherTenantId; @@ -305,6 +312,28 @@ public class DeviceServiceTest extends AbstractServiceTest { }); } + @Test + public void testShouldNotPutInCacheRolledbackDeviceProfile() { + DeviceProfile deviceProfile = createDeviceProfile(tenantId, "New device Profile" + StringUtils.randomAlphabetic(5)); + + + Device device = new Device(); + device.setType(deviceProfile.getName()); + device.setTenantId(tenantId); + device.setName("My device"+ StringUtils.randomAlphabetic(5)); + + DefaultTransactionDefinition def = new DefaultTransactionDefinition(); + TransactionStatus status = platformTransactionManager.getTransaction(def); + try { + deviceProfileService.saveDeviceProfile(deviceProfile); + deviceService.saveDevice(device); + } finally { + platformTransactionManager.rollback(status); + } + DeviceProfile deviceProfileByName = deviceProfileService.findDeviceProfileByName(tenantId, deviceProfile.getName()); + Assert.assertNull(deviceProfileByName); + } + @Test public void testAssignDeviceToNonExistentCustomer() { Device device = new Device(); From 6551658bfc5a07074badaa3dc91348918b315e7f Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 10 Nov 2023 13:17:23 +0200 Subject: [PATCH 02/10] updated TbTransactionalCache with method that retrieve but doesn`t put value in cache --- .../server/cache/TbTransactionalCache.java | 25 ++++++++++++++++++ .../server/dao/asset/AssetProfileService.java | 4 +++ .../dao/device/DeviceProfileService.java | 4 +++ .../dao/entityview/EntityViewService.java | 2 ++ .../dao/asset/AssetProfileServiceImpl.java | 23 +++++++++++----- .../dao/device/DeviceProfileServiceImpl.java | 26 +++++++++++++------ .../server/dao/device/DeviceServiceImpl.java | 6 ++--- .../dao/entityview/EntityViewServiceImpl.java | 13 +++++++--- 8 files changed, 80 insertions(+), 23 deletions(-) diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java index fc104ef7f1..219e89299f 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/TbTransactionalCache.java @@ -47,6 +47,18 @@ public interface TbTransactionalCache newTransactionForKeys(List keys); + default V getOrFetchFromDB(K key, Supplier dbCall, boolean cacheNullValue, boolean putToCache) { + if (putToCache) { + return getAndPutInTransaction(key, dbCall, cacheNullValue); + } else { + TbCacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + return cacheValueWrapper.get(); + } + return dbCall.get(); + } + } + default V getAndPutInTransaction(K key, Supplier dbCall, boolean cacheNullValue) { TbCacheValueWrapper cacheValueWrapper = get(key); if (cacheValueWrapper != null) { @@ -69,6 +81,19 @@ public interface TbTransactionalCache R getOrFetchFromDB(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue, boolean putToCache) { + if (putToCache) { + return getAndPutInTransaction(key, dbCall, cacheValueToResult, dbValueToCacheValue, cacheNullValue); + } else { + TbCacheValueWrapper cacheValueWrapper = get(key); + if (cacheValueWrapper != null) { + var cacheValue = cacheValueWrapper.get(); + return cacheValue == null ? null : cacheValueToResult.apply(cacheValue); + } + return dbCall.get(); + } + } + default R getAndPutInTransaction(K key, Supplier dbCall, Function cacheValueToResult, Function dbValueToCacheValue, boolean cacheNullValue) { TbCacheValueWrapper cacheValueWrapper = get(key); if (cacheValueWrapper != null) { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java index 1ec6fb3fb5..477c352b54 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java @@ -27,8 +27,12 @@ public interface AssetProfileService extends EntityDaoService { AssetProfile findAssetProfileById(TenantId tenantId, AssetProfileId assetProfileId); + AssetProfile findAssetProfileById(TenantId tenantId, AssetProfileId assetProfileId, boolean putInCache); + AssetProfile findAssetProfileByName(TenantId tenantId, String profileName); + AssetProfile findAssetProfileByName(TenantId tenantId, String profileName, boolean putInCache); + AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, AssetProfileId assetProfileId); AssetProfile saveAssetProfile(AssetProfile assetProfile, boolean doValidate); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java index f94b709c97..5d325aa1a6 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceProfileService.java @@ -27,8 +27,12 @@ public interface DeviceProfileService extends EntityDaoService { DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId); + DeviceProfile findDeviceProfileById(TenantId tenantId, DeviceProfileId deviceProfileId, boolean putInCache); + DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName); + DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName, boolean putInCache); + DeviceProfileInfo findDeviceProfileInfoById(TenantId tenantId, DeviceProfileId deviceProfileId); DeviceProfile saveDeviceProfile(DeviceProfile deviceProfile, boolean doValidate); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java index 09bad2b48d..7a020767f5 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entityview/EntityViewService.java @@ -50,6 +50,8 @@ public interface EntityViewService extends EntityDaoService { EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId); + EntityView findEntityViewById(TenantId tenantId, EntityViewId entityViewId, boolean putInCache); + EntityView findEntityViewByTenantIdAndName(TenantId tenantId, String name); PageData findEntityViewByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index fd17c484d7..075975cd96 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -90,14 +90,24 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService assetProfileDao.findById(tenantId, assetProfileId.getId()), true); + return cache.getOrFetchFromDB(AssetProfileCacheKey.fromId(assetProfileId), + () -> assetProfileDao.findById(tenantId, assetProfileId.getId()), true, putInCache); } @Override public AssetProfile findAssetProfileByName(TenantId tenantId, String profileName) { + return findAssetProfileByName(tenantId, profileName, true); + } + + @Override + public AssetProfile findAssetProfileByName(TenantId tenantId, String profileName, boolean putInCache) { log.trace("Executing findAssetProfileByName [{}][{}]", tenantId, profileName); Validator.validateString(profileName, INCORRECT_ASSET_PROFILE_NAME + profileName); return cache.getAndPutInTransaction(AssetProfileCacheKey.fromName(tenantId, profileName), @@ -126,8 +136,8 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true); + return cache.getOrFetchFromDB(DeviceProfileCacheKey.fromId(deviceProfileId), + () -> deviceProfileDao.findById(tenantId, deviceProfileId.getId()), true, putInCache); } @Override public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName) { + return findDeviceProfileByName(tenantId, profileName, true); + } + + @Override + public DeviceProfile findDeviceProfileByName(TenantId tenantId, String profileName, boolean putInCache) { log.trace("Executing findDeviceProfileByName [{}][{}]", tenantId, profileName); validateString(profileName, INCORRECT_DEVICE_PROFILE_NAME + profileName); - return cache.getAndPutInTransaction(DeviceProfileCacheKey.fromName(tenantId, profileName), - () -> deviceProfileDao.findByName(tenantId, profileName), true); + return cache.getOrFetchFromDB(DeviceProfileCacheKey.fromName(tenantId, profileName), + () -> deviceProfileDao.findByName(tenantId, profileName), true, putInCache); } @Override @@ -163,8 +173,8 @@ public class DeviceProfileServiceImpl extends AbstractCachedEntityService entityViewDao.findById(tenantId, entityViewId.getId()) - , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true); + , EntityViewCacheValue::getEntityView, v -> new EntityViewCacheValue(v, null), true, putInCache); } @Override From e6473c9c171255b17d9ef57cf6e85950e9892077 Mon Sep 17 00:00:00 2001 From: dashevchenko Date: Fri, 10 Nov 2023 15:40:56 +0200 Subject: [PATCH 03/10] fixed AssetProfileServiceImpl --- .../thingsboard/server/dao/asset/AssetProfileServiceImpl.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java index 075975cd96..7cc251f9ec 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/asset/AssetProfileServiceImpl.java @@ -110,8 +110,8 @@ public class AssetProfileServiceImpl extends AbstractCachedEntityService assetProfileDao.findByName(tenantId, profileName), false); + return cache.getOrFetchFromDB(AssetProfileCacheKey.fromName(tenantId, profileName), + () -> assetProfileDao.findByName(tenantId, profileName), false, putInCache); } @Override From 6d0d1c02111509ce0283bd2931b487f066e2ba3e Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Tue, 14 Nov 2023 19:34:54 +0200 Subject: [PATCH 04/10] validate node only after upgrade is called --- .../server/dao/rule/BaseRuleChainService.java | 3 ++- .../dao/service/validator/RuleChainDataValidator.java | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) 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 956e4b83f6..6257bb0420 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 @@ -155,7 +155,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC if (ruleChain == null) { return RuleChainUpdateResult.failed(); } - RuleChainDataValidator.validateMetaData(ruleChainMetaData); + RuleChainDataValidator.validateMetaDataFieldsAndConnections(ruleChainMetaData); List nodes = ruleChainMetaData.getNodes(); List toAddOrUpdate = new ArrayList<>(); @@ -194,6 +194,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC for (RuleNode node : toAddOrUpdate) { node.setRuleChainId(ruleChainId); node = ruleNodeUpdater.apply(node); + RuleChainDataValidator.validateRuleNode(node); RuleNode savedNode = ruleNodeDao.save(tenantId, node); relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(), EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN)); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java index 43878adc93..76dad19bcd 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/RuleChainDataValidator.java @@ -87,15 +87,18 @@ public class RuleChainDataValidator extends DataValidator { } public static List validateMetaData(RuleChainMetaData ruleChainMetaData) { - ConstraintValidator.validateFields(ruleChainMetaData); - List throwables = ruleChainMetaData.getNodes().stream() + validateMetaDataFieldsAndConnections(ruleChainMetaData); + return ruleChainMetaData.getNodes().stream() .map(RuleChainDataValidator::validateRuleNode) .filter(Objects::nonNull) .collect(Collectors.toList()); + } + + public static void validateMetaDataFieldsAndConnections(RuleChainMetaData ruleChainMetaData) { + ConstraintValidator.validateFields(ruleChainMetaData); if (CollectionUtils.isNotEmpty(ruleChainMetaData.getConnections())) { validateCircles(ruleChainMetaData.getConnections()); } - return throwables; } public static Throwable validateRuleNode(RuleNode ruleNode) { From c82e3d4791223901dbbf86182f7ea4bd0854e164 Mon Sep 17 00:00:00 2001 From: imbeacon Date: Mon, 11 Sep 2023 19:30:58 +0300 Subject: [PATCH 05/10] Added support for NON CoAP messages --- .../server/transport/coap/CoapTestClient.java | 32 +- .../client/CoapClientIntegrationTest.java | 314 ++++++++++++++++++ .../callback/AbstractSyncSessionCallback.java | 1 + .../coap/callback/CoapOkCallback.java | 4 +- 4 files changed, 346 insertions(+), 5 deletions(-) create mode 100644 application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java b/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java index 6c5f682a4f..3124571dc8 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/CoapTestClient.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.coap; +import lombok.Getter; import org.eclipse.californium.core.CoapClient; import org.eclipse.californium.core.CoapHandler; import org.eclipse.californium.core.CoapObserveRelation; @@ -34,7 +35,10 @@ public class CoapTestClient { private final CoapClient client; - public CoapTestClient(){ + @Getter + private CoAP.Type type = CoAP.Type.CON; + + public CoapTestClient() { this.client = createClient(); } @@ -80,9 +84,13 @@ public class CoapTestClient { return client.setTimeout(CLIENT_REQUEST_TIMEOUT).get(); } - public CoapObserveRelation getObserveRelation(CoapTestCallback callback){ + public CoapObserveRelation getObserveRelation(CoapTestCallback callback) { + return getObserveRelation(callback, true); + } + + public CoapObserveRelation getObserveRelation(CoapTestCallback callback, boolean confirmable) { Request request = Request.newGet().setObserve(); - request.setType(CoAP.Type.CON); + request.setType(confirmable ? CoAP.Type.CON : CoAP.Type.NON); return client.observe(request, callback); } @@ -94,12 +102,28 @@ public class CoapTestClient { } public void setURI(String accessToken, FeatureType featureType) { - if (featureType == null){ + if (featureType == null) { featureType = FeatureType.ATTRIBUTES; } setURI(getFeatureTokenUrl(accessToken, featureType)); } + public void useCONs() { + if (client == null) { + throw new RuntimeException("Failed to connect! CoapClient is not initialized!"); + } + type = CoAP.Type.CON; + client.useCONs(); + } + + public void useNONs() { + if (client == null) { + throw new RuntimeException("Failed to connect! CoapClient is not initialized!"); + } + type = CoAP.Type.NON; + client.useNONs(); + } + private CoapClient createClient() { return new CoapClient(); } diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java new file mode 100644 index 0000000000..3760935d41 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java @@ -0,0 +1,314 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.coap.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.californium.core.CoapHandler; +import org.eclipse.californium.core.CoapObserveRelation; +import org.eclipse.californium.core.CoapResponse; +import org.eclipse.californium.core.coap.CoAP; +import org.eclipse.californium.core.coap.MediaTypeRegistry; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.data.query.EntityKey; +import org.thingsboard.server.common.data.query.EntityKeyType; +import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.msg.session.FeatureType; +import org.thingsboard.server.dao.service.DaoSqlTest; +import org.thingsboard.server.transport.coap.AbstractCoapIntegrationTest; +import org.thingsboard.server.transport.coap.CoapTestCallback; +import org.thingsboard.server.transport.coap.CoapTestClient; +import org.thingsboard.server.transport.coap.CoapTestConfigProperties; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.query.EntityKeyType.CLIENT_ATTRIBUTE; +import static org.thingsboard.server.common.data.query.EntityKeyType.SHARED_ATTRIBUTE; + +@Slf4j +@DaoSqlTest +public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { + + private static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," + + " \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}"; + private static final List EXPECTED_KEYS = Arrays.asList("key1", "key2", "key3", "key4", "key5"); + private static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}"; + + + @Before + public void beforeTest() throws Exception { + CoapTestConfigProperties configProperties = CoapTestConfigProperties.builder() + .deviceName("Test Post Attributes device") + .build(); + processBeforeTest(configProperties); + } + + @After + public void afterTest() throws Exception { + processAfterTest(); + } + + @Test + public void testConfirmableRequests() throws Exception { + boolean confirmable = true; + processAttributesTest(confirmable); + processTwoWayRpcTest(confirmable); + processTestRequestAttributesValuesFromTheServer(confirmable); + } + + @Test + public void testNonConfirmableRequests() throws Exception { + boolean confirmable = false; + processAttributesTest(confirmable); + processTwoWayRpcTest(confirmable); + processTestRequestAttributesValuesFromTheServer(confirmable); + } + + protected void processAttributesTest(boolean confirmable) throws Exception { + client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); + CoapResponse coapResponse = client.postMethod(PAYLOAD_VALUES_STR.getBytes()); + assertEquals(CoAP.ResponseCode.CREATED, coapResponse.getCode()); + validateConfirmableFlag(client, coapResponse); + + DeviceId deviceId = savedDevice.getId(); + List actualKeys = getActualKeysList(deviceId); + assertNotNull(actualKeys); + + Set actualKeySet = new HashSet<>(actualKeys); + Set expectedKeySet = new HashSet<>(EXPECTED_KEYS); + assertEquals(expectedKeySet, actualKeySet); + + String attributesValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/attributes/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); + ; + List> values = doGetAsyncTyped(attributesValuesUrl, new TypeReference<>() { + }); + assertAttributesValues(values, actualKeySet); + String deleteAttributesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/CLIENT_SCOPE?keys=" + String.join(",", actualKeySet); + doDelete(deleteAttributesUrl); + } + + protected void processTwoWayRpcTest(boolean confirmable) throws Exception { + client = createClientForFeatureWithConfirmableParameter(FeatureType.RPC, confirmable); + CoapTestCallback callbackCoap = new TestCoapCallbackForRPC(client); + + CoapObserveRelation observeRelation = client.getObserveRelation(callbackCoap, confirmable); + String awaitAlias = "await Two Way Rpc (client.getObserveRelation)"; + await(awaitAlias) + .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .until(() -> CoAP.ResponseCode.VALID.equals(callbackCoap.getResponseCode()) && + callbackCoap.getObserve() != null && + 0 == callbackCoap.getObserve()); + validateCurrentStateNotification(callbackCoap); + + String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; + String deviceId = savedDevice.getId().getId().toString(); + int expectedObserveCountAfterGpioRequest1 = callbackCoap.getObserve() + 1; + String actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + awaitAlias = "await Two Way Rpc (setGpio(method, params, value) first"; + await(awaitAlias) + .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && + callbackCoap.getObserve() != null && + expectedObserveCountAfterGpioRequest1 == callbackCoap.getObserve()); + validateTwoWayStateChangedNotification(callbackCoap, actualResult); + + int expectedObserveCountAfterGpioRequest2 = callbackCoap.getObserve() + 1; + actualResult = doPostAsync("/api/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk()); + awaitAlias = "await Two Way Rpc (setGpio(method, params, value) second"; + await(awaitAlias) + .atMost(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS) + .until(() -> CoAP.ResponseCode.CONTENT.equals(callbackCoap.getResponseCode()) && + callbackCoap.getObserve() != null && + expectedObserveCountAfterGpioRequest2 == callbackCoap.getObserve()); + + validateTwoWayStateChangedNotification(callbackCoap, actualResult); + + observeRelation.proactiveCancel(); + assertTrue(observeRelation.isCanceled()); + } + + protected void processTestRequestAttributesValuesFromTheServer(boolean confirmable) throws Exception { + client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); + SingleEntityFilter dtf = new SingleEntityFilter(); + dtf.setSingleEntity(savedDevice.getId()); + List csKeys = getEntityKeys(CLIENT_ATTRIBUTE); + List shKeys = getEntityKeys(SHARED_ATTRIBUTE); + List keys = new ArrayList<>(); + keys.addAll(csKeys); + keys.addAll(shKeys); + getWsClient().subscribeLatestUpdate(keys, dtf); + getWsClient().registerWaitForUpdate(2); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", + PAYLOAD_VALUES_STR, String.class, status().isOk()); + + CoapResponse coapResponse = client.postMethod(PAYLOAD_VALUES_STR); + assertEquals(CoAP.ResponseCode.CREATED, coapResponse.getCode()); + + String update = getWsClient().waitForUpdate(); + assertThat(update).as("ws update received").isNotBlank(); + + String keysParam = String.join(",", EXPECTED_KEYS); + String featureTokenUrl = CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.ATTRIBUTES) + "?clientKeys=" + keysParam + "&sharedKeys=" + keysParam; + client.setURI(featureTokenUrl); + CoapResponse response = client.getMethod(); + validateConfirmableFlag(client, response); + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void assertAttributesValues(List> deviceValues, Set keySet) { + for (Map map : deviceValues) { + String key = (String) map.get("key"); + Object value = map.get("value"); + assertTrue(keySet.contains(key)); + switch (key) { + case "key1": + assertEquals("value1", value); + break; + case "key2": + assertEquals(true, value); + break; + case "key3": + assertEquals(3.0, value); + break; + case "key4": + assertEquals(4, value); + break; + case "key5": + assertNotNull(value); + assertEquals(3, ((LinkedHashMap) value).size()); + assertEquals(42, ((LinkedHashMap) value).get("someNumber")); + assertEquals(Arrays.asList(1, 2, 3), ((LinkedHashMap) value).get("someArray")); + LinkedHashMap someNestedObject = (LinkedHashMap) ((LinkedHashMap) value).get("someNestedObject"); + assertEquals("value", someNestedObject.get("key")); + break; + } + } + } + + private List getActualKeysList(DeviceId deviceId) throws Exception { + long start = System.currentTimeMillis(); + long end = System.currentTimeMillis() + 5000; + + List actualKeys = null; + while (start <= end) { + actualKeys = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", new TypeReference<>() { + }); + if (actualKeys.size() == EXPECTED_KEYS.size()) { + break; + } + Thread.sleep(100); + start += 100; + } + return actualKeys; + } + + private void validateCurrentStateNotification(CoapTestCallback callback) { + assertArrayEquals(EMPTY_PAYLOAD, callback.getPayloadBytes()); + } + + private void validateConfirmableFlag(CoapTestClient client, CoapResponse response) { + CoAP.Type responseType = response.advanced().getType(); + if (CoAP.Type.CON.equals(responseType) || CoAP.Type.NON.equals(responseType)) { + assertEquals(client.getType(), responseType); + } + } + + private void validateTwoWayStateChangedNotification(CoapTestCallback callback, String actualResult) { + assertEquals(DEVICE_RESPONSE, actualResult); + assertNotNull(callback.getPayloadBytes()); + } + + protected class TestCoapCallbackForRPC extends CoapTestCallback { + + private final CoapTestClient client; + + TestCoapCallbackForRPC(CoapTestClient client) { + this.client = client; + } + + @Override + public void onLoad(CoapResponse response) { + payloadBytes = response.getPayload(); + responseCode = response.getCode(); + observe = response.getOptions().getObserve(); + validateConfirmableFlag(client, response); + if (observe != null) { + if (observe > 0) { + processOnLoadResponse(response, client); + } + } + } + + @Override + public void onError() { + log.warn("Command Response Ack Error, No connect"); + } + } + + protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) { + validateConfirmableFlag(client, response); + JsonNode responseJson = JacksonUtil.fromBytes(response.getPayload()); + int requestId = responseJson.get("id").asInt(); + client.setURI(CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.RPC, requestId)); + client.postMethod(new CoapHandler() { + @Override + public void onLoad(CoapResponse response) { + validateConfirmableFlag(client, response); + log.warn("RPC {} command response ack: {}", requestId, response.getCode()); + } + + @Override + public void onError() { + log.warn("RPC {} command response ack error, no connect", requestId); + } + }, DEVICE_RESPONSE, MediaTypeRegistry.APPLICATION_JSON); + } + + private CoapTestClient createClientForFeatureWithConfirmableParameter(FeatureType featureType, boolean confirmable) { + CoapTestClient coapTestClient = new CoapTestClient(accessToken, featureType); + if (confirmable) { + coapTestClient.useCONs(); + } else { + coapTestClient.useNONs(); + } + return coapTestClient; + } + + private List getEntityKeys(EntityKeyType scope) { + return CoapClientIntegrationTest.EXPECTED_KEYS.stream().map(key -> new EntityKey(scope, key)).collect(Collectors.toList()); + } +} diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/AbstractSyncSessionCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/AbstractSyncSessionCallback.java index 6dc5cccf38..ca46d989e4 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/AbstractSyncSessionCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/AbstractSyncSessionCallback.java @@ -81,6 +81,7 @@ public abstract class AbstractSyncSessionCallback implements SessionMsgListener protected void respond(Response response) { response.getOptions().setContentFormat(TbCoapContentFormatUtil.getContentFormat(exchange.getRequestOptions().getContentFormat(), state.getContentFormat())); + response.setConfirmable(exchange.advanced().getRequest().isConfirmable()); exchange.respond(response); } diff --git a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java index db843a0ea3..04ce043331 100644 --- a/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java +++ b/common/transport/coap/src/main/java/org/thingsboard/server/transport/coap/callback/CoapOkCallback.java @@ -34,7 +34,9 @@ public class CoapOkCallback implements TransportServiceCallback { @Override public void onSuccess(Void msg) { - exchange.respond(new Response(onSuccessResponse)); + Response response = new Response(onSuccessResponse); + response.setConfirmable(isConRequest()); + exchange.respond(response); } @Override From c0cde53c52a3688eb7619670c13bfab9c179b05c Mon Sep 17 00:00:00 2001 From: imbeacon Date: Thu, 14 Sep 2023 09:40:58 +0300 Subject: [PATCH 06/10] Removed unneccesary check, refactoring and added assertion check for validation in callback --- .../client/CoapClientIntegrationTest.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java index 3760935d41..e50812403b 100644 --- a/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/coap/client/CoapClientIntegrationTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.transport.coap.client; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JsonNode; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.eclipse.californium.core.CoapHandler; import org.eclipse.californium.core.CoapObserveRelation; @@ -101,7 +102,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { client = createClientForFeatureWithConfirmableParameter(FeatureType.ATTRIBUTES, confirmable); CoapResponse coapResponse = client.postMethod(PAYLOAD_VALUES_STR.getBytes()); assertEquals(CoAP.ResponseCode.CREATED, coapResponse.getCode()); - validateConfirmableFlag(client, coapResponse); + assertEquals("CoAP response type is wrong!", client.getType(), coapResponse.advanced().getType()); DeviceId deviceId = savedDevice.getId(); List actualKeys = getActualKeysList(deviceId); @@ -185,7 +186,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { String featureTokenUrl = CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.ATTRIBUTES) + "?clientKeys=" + keysParam + "&sharedKeys=" + keysParam; client.setURI(featureTokenUrl); CoapResponse response = client.getMethod(); - validateConfirmableFlag(client, response); + assertEquals("CoAP response type is wrong!", client.getType(), response.advanced().getType()); } @SuppressWarnings({"unchecked", "rawtypes"}) @@ -240,13 +241,6 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { assertArrayEquals(EMPTY_PAYLOAD, callback.getPayloadBytes()); } - private void validateConfirmableFlag(CoapTestClient client, CoapResponse response) { - CoAP.Type responseType = response.advanced().getType(); - if (CoAP.Type.CON.equals(responseType) || CoAP.Type.NON.equals(responseType)) { - assertEquals(client.getType(), responseType); - } - } - private void validateTwoWayStateChangedNotification(CoapTestCallback callback, String actualResult) { assertEquals(DEVICE_RESPONSE, actualResult); assertNotNull(callback.getPayloadBytes()); @@ -256,6 +250,9 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { private final CoapTestClient client; + @Getter + private boolean wasSuccessful = false; + TestCoapCallbackForRPC(CoapTestClient client) { this.client = client; } @@ -265,7 +262,7 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { payloadBytes = response.getPayload(); responseCode = response.getCode(); observe = response.getOptions().getObserve(); - validateConfirmableFlag(client, response); + wasSuccessful = client.getType().equals(response.advanced().getType()); if (observe != null) { if (observe > 0) { processOnLoadResponse(response, client); @@ -280,14 +277,12 @@ public class CoapClientIntegrationTest extends AbstractCoapIntegrationTest { } protected void processOnLoadResponse(CoapResponse response, CoapTestClient client) { - validateConfirmableFlag(client, response); JsonNode responseJson = JacksonUtil.fromBytes(response.getPayload()); int requestId = responseJson.get("id").asInt(); client.setURI(CoapTestClient.getFeatureTokenUrl(accessToken, FeatureType.RPC, requestId)); client.postMethod(new CoapHandler() { @Override public void onLoad(CoapResponse response) { - validateConfirmableFlag(client, response); log.warn("RPC {} command response ack: {}", requestId, response.getCode()); } From 5f10163dce6405e4a32e50dae0d676e406bcded7 Mon Sep 17 00:00:00 2001 From: ShvaykaD Date: Tue, 14 Nov 2023 13:01:19 +0200 Subject: [PATCH 07/10] fixed copy string values from msg to md in TbCopyKeysNode --- .../rule/engine/transform/TbCopyKeysNode.java | 4 +++- .../rule/engine/transform/TbCopyKeysNodeTest.java | 13 ++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java index 8d9a7102d0..f413be43c1 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java @@ -89,7 +89,9 @@ public class TbCopyKeysNode implements TbNode { String keyData = entry.getKey(); if (checkKey(keyData)) { msgChanged = true; - metaData.putValue(keyData, JacksonUtil.toString(entry.getValue())); + String value = entry.getValue().isTextual() ? + entry.getValue().asText() : JacksonUtil.toString(entry.getValue()); + metaData.putValue(keyData, value); } } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java index e0ab34e35b..0fb9589e90 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java @@ -96,10 +96,12 @@ public class TbCopyKeysNodeTest { @Test void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception { config.setFromMetadata(false); + config.setKeys(Set.of(".*Key$")); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); - String data = "{\"DigitData\":22.5,\"TempDataValue\":10.5}"; + String data = "{\"nullKey\":null,\"stringKey\":\"value1\",\"booleanKey\":true,\"doubleKey\":42.0,\"longKey\":73," + + "\"jsonKey\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}"; node.onMsg(ctx, getTbMsg(deviceId, data)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); @@ -110,8 +112,13 @@ public class TbCopyKeysNodeTest { assertThat(newMsg).isNotNull(); Map metaDataMap = newMsg.getMetaData().getData(); - assertThat(metaDataMap.containsKey("DigitData")).isEqualTo(true); - assertThat(metaDataMap.containsKey("TempDataValue")).isEqualTo(true); + assertThat(metaDataMap.get("nullKey")).isEqualTo("null"); + assertThat(metaDataMap.get("stringKey")).isEqualTo("value1"); + assertThat(metaDataMap.get("booleanKey")).isEqualTo("true"); + assertThat(metaDataMap.get("doubleKey")).isEqualTo("42.0"); + assertThat(metaDataMap.get("longKey")).isEqualTo("73"); + assertThat(metaDataMap.get("jsonKey")) + .isEqualTo("{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}"); } @Test From edcc507f4b9ec40ee92c70f2c0d8f8490f7ccbe8 Mon Sep 17 00:00:00 2001 From: ViacheslavKlimov Date: Wed, 15 Nov 2023 15:14:41 +0200 Subject: [PATCH 08/10] Subscribe first in testSubscribingToUnreadNotificationsCount --- .../server/service/notification/NotificationApiTest.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java index 26be4e9bd3..ba0b4e4763 100644 --- a/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java +++ b/application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java @@ -113,15 +113,13 @@ public class NotificationApiTest extends AbstractNotificationApiTest { @Test public void testSubscribingToUnreadNotificationsCount() { + wsClient.subscribeForUnreadNotificationsCount().waitForReply(true); NotificationTarget notificationTarget = createNotificationTarget(customerUserId); String notificationText1 = "Notification 1"; submitNotificationRequest(notificationTarget.getId(), notificationText1); String notificationText2 = "Notification 2"; submitNotificationRequest(notificationTarget.getId(), notificationText2); - wsClient.subscribeForUnreadNotificationsCount(); - wsClient.waitForReply(true); - await().atMost(2, TimeUnit.SECONDS) .until(() -> wsClient.getLastCountUpdate().getTotalUnreadCount() == 2); } From a4bac1e7948e55b148a974e970305756fda0e613 Mon Sep 17 00:00:00 2001 From: Andrii Shvaika Date: Wed, 15 Nov 2023 15:57:58 +0200 Subject: [PATCH 09/10] Implement cache for the entity creation request --- .../transport/DefaultTransportApiService.java | 9 +++ .../src/main/resources/thingsboard.yml | 5 +- common/cluster-api/src/main/proto/queue.proto | 6 ++ .../server/common/data/DataConstants.java | 1 + .../AbstractGatewaySessionHandler.java | 22 ++++-- .../common/transport/TransportService.java | 2 +- .../limits/DefaultEntityLimitsCache.java | 74 +++++++++++++++++++ .../transport/limits/EntityLimitKey.java | 27 +++++++ .../transport/limits/EntityLimitsCache.java | 24 ++++++ .../service/DefaultTransportService.java | 48 ++++++++---- .../dao/exception/EntitiesLimitException.java | 35 +++++++++ .../server/dao/service/DataValidator.java | 3 +- .../src/main/resources/tb-coap-transport.yml | 4 + .../src/main/resources/tb-http-transport.yml | 4 + .../src/main/resources/tb-lwm2m-transport.yml | 4 + .../src/main/resources/tb-mqtt-transport.yml | 4 + .../src/main/resources/tb-snmp-transport.yml | 4 + 17 files changed, 251 insertions(+), 25 deletions(-) create mode 100644 common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultEntityLimitsCache.java create mode 100644 common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitKey.java create mode 100644 common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitsCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java index 3505052fca..631e64a285 100644 --- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java +++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java @@ -78,6 +78,7 @@ import org.thingsboard.server.dao.device.provision.ProvisionFailedException; import org.thingsboard.server.dao.device.provision.ProvisionRequest; import org.thingsboard.server.dao.device.provision.ProvisionResponse; import org.thingsboard.server.dao.device.provision.ProvisionResponseStatus; +import org.thingsboard.server.dao.exception.EntitiesLimitException; import org.thingsboard.server.dao.ota.OtaPackageService; import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; @@ -164,6 +165,7 @@ public class DefaultTransportApiService implements TransportApiService { public void init() { handlerExecutor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(maxCoreHandlerThreads, "transport-api-service-core-handler")); } + @PreDestroy public void destroy() { if (handlerExecutor != null) { @@ -400,6 +402,13 @@ public class DefaultTransportApiService implements TransportApiService { } catch (JsonProcessingException e) { log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e); throw new RuntimeException(e); + } catch (EntitiesLimitException e) { + log.warn("[{}][{}] API limit exception: [{}]", e.getTenantId(), gatewayId, e.getMessage()); + return TransportApiResponseMsg.newBuilder() + .setGetOrCreateDeviceResponseMsg( + GetOrCreateDeviceFromGatewayResponseMsg.newBuilder() + .setError(TransportProtos.TransportApiRequestErrorCode.ENTITY_LIMIT)) + .build(); } finally { deviceCreationLock.unlock(); } diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index bdf0ad8252..ecc89c84c5 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -583,6 +583,9 @@ cache: rateLimits: timeToLiveInMinutes: "${CACHE_SPECS_RATE_LIMITS_TTL:120}" # Rate limits cache TTL maxSize: "${CACHE_SPECS_RATE_LIMITS_MAX_SIZE:200000}" # 0 means the cache is disabled + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Spring data parameters spring.data.redis.repositories.enabled: false # Disable this because it is not required. @@ -875,7 +878,7 @@ transport: ip_limits_enabled: "${TB_TRANSPORT_IP_RATE_LIMITS_ENABLED:false}" # Maximum number of connect attempts with invalid credentials max_wrong_credentials_per_ip: "${TB_TRANSPORT_MAX_WRONG_CREDENTIALS_PER_IP:10}" - # Timeout to expire block IP addresses + # Timeout (in milliseconds) to expire block IP addresses ip_block_timeout: "${TB_TRANSPORT_IP_BLOCK_TIMEOUT:60000}" # Local HTTP transport parameters http: diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index 016a40f5dc..0cb91faee0 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -255,6 +255,12 @@ message GetOrCreateDeviceFromGatewayRequestMsg { message GetOrCreateDeviceFromGatewayResponseMsg { DeviceInfoProto deviceInfo = 1; bytes profileBody = 2; + TransportApiRequestErrorCode error = 3; +} + +enum TransportApiRequestErrorCode { + UNKNOWN_TRANSPORT_API_ERROR = 0; + ENTITY_LIMIT = 1; } message GetEntityProfileRequestMsg { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 544f5805a6..4a84743283 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -49,6 +49,7 @@ public class DataConstants { public static final String MQTT_TRANSPORT_NAME = "MQTT"; public static final String HTTP_TRANSPORT_NAME = "HTTP"; public static final String SNMP_TRANSPORT_NAME = "SNMP"; + public static final String MAXIMUM_NUMBER_OF_DEVICES_REACHED = "Maximum number of devices reached!"; public static final String[] allScopes() { diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java index fd0297203d..8f964f8369 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -35,6 +35,7 @@ import io.netty.handler.codec.mqtt.MqttPublishMessage; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.TransportService; @@ -215,8 +216,7 @@ public abstract class AbstractGatewaySessionHandler callback); - void process(GetOrCreateDeviceFromGatewayRequestMsg msg, + void process(TenantId tenantId, GetOrCreateDeviceFromGatewayRequestMsg msg, TransportServiceCallback callback); void process(ProvisionDeviceRequestMsg msg, diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultEntityLimitsCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultEntityLimitsCache.java new file mode 100644 index 0000000000..3bfab1e3f0 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/DefaultEntityLimitsCache.java @@ -0,0 +1,74 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Expiry; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.thingsboard.server.queue.util.TbTransportComponent; + +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +@Service +@TbTransportComponent +@Slf4j +public class DefaultEntityLimitsCache implements EntityLimitsCache { + + private static final int DEVIATION = 10; + private final Cache cache; + + public DefaultEntityLimitsCache(@Value("${cache.entityLimits.timeToLiveInMinutes:5}") int ttl, + @Value("${cache.entityLimits.maxSize:100000}") int maxSize) { + // We use the 'random' expiration time to avoid peak loads. + long mainPart = (TimeUnit.MINUTES.toNanos(ttl) / 100) * (100 - DEVIATION); + long randomPart = (TimeUnit.MINUTES.toNanos(ttl) / 100) * DEVIATION; + cache = Caffeine.newBuilder() + .expireAfter(new Expiry() { + @Override + public long expireAfterCreate(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime) { + return mainPart + (long) (randomPart * ThreadLocalRandom.current().nextDouble()); + } + + @Override + public long expireAfterUpdate(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime, long currentDuration) { + return currentDuration; + } + + @Override + public long expireAfterRead(@NotNull EntityLimitKey key, @NotNull Boolean value, long currentTime, long currentDuration) { + return currentDuration; + } + }) + .maximumSize(maxSize) + .build(); + } + + @Override + public boolean get(EntityLimitKey key) { + var result = cache.getIfPresent(key); + return result != null ? result : false; + } + + @Override + public void put(EntityLimitKey key, boolean value) { + cache.put(key, value); + } +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitKey.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitKey.java new file mode 100644 index 0000000000..f8957f780d --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitKey.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +import lombok.Data; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class EntityLimitKey { + + private final TenantId tenantId; + private final String deviceName; + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitsCache.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitsCache.java new file mode 100644 index 0000000000..b350a2eb27 --- /dev/null +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/limits/EntityLimitsCache.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.transport.limits; + +public interface EntityLimitsCache { + + boolean get(EntityLimitKey key); + + void put(EntityLimitKey key, boolean value); + +} diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index cc4a1bc3df..c4840b2402 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -76,6 +76,8 @@ import org.thingsboard.server.common.transport.TransportTenantProfileCache; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.common.transport.auth.ValidateDeviceCredentialsResponse; +import org.thingsboard.server.common.transport.limits.EntityLimitKey; +import org.thingsboard.server.common.transport.limits.EntityLimitsCache; import org.thingsboard.server.common.transport.limits.TransportRateLimitService; import org.thingsboard.server.common.transport.util.JsonUtils; import org.thingsboard.server.gen.transport.TransportProtos; @@ -161,6 +163,7 @@ public class DefaultTransportService implements TransportService { @Value("${transport.stats.enabled:false}") private boolean statsEnabled; + @Autowired @Lazy private TbApiUsageReportClient apiUsageClient; @@ -184,6 +187,8 @@ public class DefaultTransportService implements TransportService { private final TransportResourceCache transportResourceCache; private final NotificationRuleProcessor notificationRuleProcessor; + private final EntityLimitsCache entityLimitsCache; + protected TbQueueRequestTemplate, TbProtoQueueMsg> transportApiRequestTemplate; protected TbQueueProducer> ruleEngineMsgProducer; protected TbQueueProducer> tbCoreMsgProducer; @@ -212,7 +217,8 @@ public class DefaultTransportService implements TransportService { TransportTenantProfileCache tenantProfileCache, TransportRateLimitService rateLimitService, DataDecodingEncodingService dataDecodingEncodingService, SchedulerComponent scheduler, TransportResourceCache transportResourceCache, - ApplicationEventPublisher eventPublisher, NotificationRuleProcessor notificationRuleProcessor) { + ApplicationEventPublisher eventPublisher, NotificationRuleProcessor notificationRuleProcessor, + EntityLimitsCache entityLimitsCache) { this.partitionService = partitionService; this.serviceInfoProvider = serviceInfoProvider; this.queueProvider = queueProvider; @@ -227,6 +233,7 @@ public class DefaultTransportService implements TransportService { this.transportResourceCache = transportResourceCache; this.eventPublisher = eventPublisher; this.notificationRuleProcessor = notificationRuleProcessor; + this.entityLimitsCache = entityLimitsCache; } @PostConstruct @@ -249,7 +256,7 @@ public class DefaultTransportService implements TransportService { } @AfterStartUp(order = AfterStartUp.TRANSPORT_SERVICE) - private void start() { + public void start() { mainConsumerExecutor.execute(() -> { while (!stopped) { try { @@ -473,24 +480,33 @@ public class DefaultTransportService implements TransportService { AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); } + @Override - public void process(TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg requestMsg, TransportServiceCallback callback) { + public void process(TenantId tenantId, TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg requestMsg, TransportServiceCallback callback) { TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetOrCreateDeviceRequestMsg(requestMsg).build()); log.trace("Processing msg: {}", requestMsg); - ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { - TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg msg = tmp.getValue().getGetOrCreateDeviceResponseMsg(); - GetOrCreateDeviceFromGatewayResponse.GetOrCreateDeviceFromGatewayResponseBuilder result = GetOrCreateDeviceFromGatewayResponse.builder(); - if (msg.hasDeviceInfo()) { - TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); - result.deviceInfo(tdi); - ByteString profileBody = msg.getProfileBody(); - if (profileBody != null && !profileBody.isEmpty()) { - result.deviceProfile(deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody)); + var key = new EntityLimitKey(tenantId, StringUtils.truncate(requestMsg.getDeviceName(), 256)); + if (entityLimitsCache.get(key)) { + transportCallbackExecutor.submit(() -> callback.onError(new RuntimeException(DataConstants.MAXIMUM_NUMBER_OF_DEVICES_REACHED))); + } else { + ListenableFuture response = Futures.transform(transportApiRequestTemplate.send(protoMsg), tmp -> { + TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg msg = tmp.getValue().getGetOrCreateDeviceResponseMsg(); + GetOrCreateDeviceFromGatewayResponse.GetOrCreateDeviceFromGatewayResponseBuilder result = GetOrCreateDeviceFromGatewayResponse.builder(); + if (msg.hasDeviceInfo()) { + TransportDeviceInfo tdi = getTransportDeviceInfo(msg.getDeviceInfo()); + result.deviceInfo(tdi); + ByteString profileBody = msg.getProfileBody(); + if (!profileBody.isEmpty()) { + result.deviceProfile(deviceProfileCache.getOrCreate(tdi.getDeviceProfileId(), profileBody)); + } + } else if (TransportProtos.TransportApiRequestErrorCode.ENTITY_LIMIT.equals(msg.getError())) { + entityLimitsCache.put(key, true); + throw new RuntimeException(DataConstants.MAXIMUM_NUMBER_OF_DEVICES_REACHED); } - } - return result.build(); - }, MoreExecutors.directExecutor()); - AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + return result.build(); + }, MoreExecutors.directExecutor()); + AsyncCallbackTemplate.withCallback(response, callback::onSuccess, callback::onError, transportCallbackExecutor); + } } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java new file mode 100644 index 0000000000..2f618e94d5 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/exception/EntitiesLimitException.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.exception; + +import lombok.Getter; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.id.TenantId; + +public class EntitiesLimitException extends DataValidationException { + private static final long serialVersionUID = -9211462514373279196L; + + @Getter + private final TenantId tenantId; + @Getter + private final EntityType entityType; + + public EntitiesLimitException(TenantId tenantId, EntityType entityType) { + super(entityType.getNormalName() + "s limit reached"); + this.tenantId = tenantId; + this.entityType = entityType; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java index d81c05044f..2c011b810e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/DataValidator.java @@ -26,6 +26,7 @@ import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.dao.TenantEntityWithDataDao; import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.exception.EntitiesLimitException; import org.thingsboard.server.dao.usagerecord.ApiLimitService; import java.util.HashSet; @@ -117,7 +118,7 @@ public abstract class DataValidator> { protected void validateNumberOfEntitiesPerTenant(TenantId tenantId, EntityType entityType) { if (!apiLimitService.checkEntitiesLimit(tenantId, entityType)) { - throw new DataValidationException(entityType.getNormalName() + "s limit reached"); + throw new EntitiesLimitException(tenantId, entityType); } } diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index 437f551690..959b3285de 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -49,6 +49,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 8d4d6fadf0..7edaa640a1 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -79,6 +79,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 8fd1c11e6e..cebc0425d5 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -49,6 +49,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 6c51d18ab7..330473a624 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -49,6 +49,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index eeb6807714..ca13212293 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -49,6 +49,10 @@ zk: cache: # caffeine or redis type: "${CACHE_TYPE:redis}" + # Deliberately placed outside the 'specs' group above + entityLimits: + timeToLiveInMinutes: "${CACHE_SPECS_ENTITY_LIMITS_TTL:5}" # Entity limits cache TTL + maxSize: "${CACHE_SPECS_ENTITY_LIMITS_MAX_SIZE:100000}" # 0 means the cache is disabled # Redis configuration parameters redis: From 1bb01ef161dddb46e450c6af308d21f4ed9ab1b0 Mon Sep 17 00:00:00 2001 From: Artem Dzhereleiko Date: Wed, 15 Nov 2023 16:41:06 +0200 Subject: [PATCH 10/10] UI: Fixed uncontroled focus on field --- .../shared/components/entity/entity-subtype-list.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts index 6628d14439..89900fb065 100644 --- a/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts +++ b/ui-ngx/src/app/shared/components/entity/entity-subtype-list.component.ts @@ -265,8 +265,8 @@ export class EntitySubTypeListComponent implements ControlValueAccessor, OnInit, const value = (event.value || '').trim(); if (value) { this.add(value); + this.clear(''); } - this.clear(''); } remove(entitySubtype: string) { const index = this.entitySubtypeList.indexOf(entitySubtype);