Browse Source

Merge pull request #13428 from ShvaykaD/feature/separate-cassandra-rate-limits

Separate rate limits (READ and WRITE) for Cassandra
pull/13501/head
Viacheslav Klimov 1 year ago
committed by GitHub
parent
commit
0047168bfc
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      application/pom.xml
  2. 28
      application/src/main/data/upgrade/basic/schema_update.sql
  3. 46
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  4. 38
      application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java
  5. 19
      application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java
  6. 4
      common/coap-server/pom.xml
  7. 36
      common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java
  8. 30
      common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitEntry.java
  9. 92
      common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java
  10. 74
      common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java
  11. 39
      common/data/src/main/java/org/thingsboard/server/common/data/validation/RateLimit.java
  12. 101
      common/data/src/test/java/org/thingsboard/server/common/data/limit/LimitedApiTest.java
  13. 99
      common/data/src/test/java/org/thingsboard/server/common/data/limit/RateLimitUtilTest.java
  14. 73
      common/discovery-api/pom.xml
  15. 2
      common/discovery-api/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java
  16. 4
      common/edqs/pom.xml
  17. 28
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java
  18. 41
      common/message/src/test/java/org/thingsboard/server/common/msg/tools/TbRateLimitsTest.java
  19. 1
      common/pom.xml
  20. 4
      common/queue/pom.xml
  21. 13
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java
  22. 4
      common/transport/transport-api/pom.xml
  23. 4
      dao/pom.xml
  24. 16
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java
  25. 16
      dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java
  26. 2
      dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java
  27. 32
      dao/src/main/java/org/thingsboard/server/dao/service/RateLimitValidator.java
  28. 6
      dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java
  29. 49
      dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java
  30. 40
      dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateExecutorType.java
  31. 5
      pom.xml
  32. 20
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html
  33. 5
      ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts
  34. 15
      ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts
  35. 10
      ui-ngx/src/app/shared/models/tenant.model.ts
  36. 10
      ui-ngx/src/assets/locale/locale.constant-en_US.json

4
application/pom.xml

@ -128,6 +128,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>edqs</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard</groupId>
<artifactId>dao</artifactId>

28
application/src/main/data/upgrade/basic/schema_update.sql

@ -14,3 +14,31 @@
-- limitations under the License.
--
UPDATE tenant_profile
SET profile_data = jsonb_set(
profile_data,
'{configuration}',
(
(profile_data -> 'configuration') - 'cassandraQueryTenantRateLimitsConfiguration'
||
COALESCE(
CASE
WHEN profile_data -> 'configuration' ->
'cassandraQueryTenantRateLimitsConfiguration' IS NOT NULL THEN
jsonb_build_object(
'cassandraReadQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantCoreRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraReadQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration',
'cassandraWriteQueryTenantRuleEngineRateLimits',
profile_data -> 'configuration' -> 'cassandraQueryTenantRateLimitsConfiguration'
)
END,
'{}'::jsonb
)
)
)
WHERE profile_data -> 'configuration' ? 'cassandraQueryTenantRateLimitsConfiguration';

46
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
@ -34,8 +35,10 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.component.RuleNodeClassInfo;
import org.thingsboard.server.service.install.DbUpgradeExecutorService;
@ -69,14 +72,57 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private DbUpgradeExecutorService executorService;
@Autowired
private TenantProfileService tenantProfileService;
@Override
public void updateData() throws Exception {
log.info("Updating data ...");
//TODO: should be cleaned after each release
updateInputNodes();
deduplicateRateLimitsPerSecondsConfigurations();
log.info("Data updated.");
}
private void deduplicateRateLimitsPerSecondsConfigurations() {
log.info("Starting update of tenant profiles...");
int totalProfiles = 0;
int updatedTenantProfiles = 0;
int skippedProfiles = 0;
int failedProfiles = 0;
var tenantProfiles = new PageDataIterable<>(
pageLink -> tenantProfileService.findTenantProfiles(TenantId.SYS_TENANT_ID, pageLink), 1024);
for (TenantProfile tenantProfile : tenantProfiles) {
totalProfiles++;
String profileName = tenantProfile.getName();
UUID profileId = tenantProfile.getId().getId();
try {
Optional<DefaultTenantProfileConfiguration> profileConfiguration = tenantProfile.getProfileConfiguration();
if (profileConfiguration.isEmpty()) {
log.debug("[{}][{}] Skipping tenant profile with non-default configuration.", profileId, profileName);
skippedProfiles++;
continue;
}
DefaultTenantProfileConfiguration defaultTenantProfileConfiguration = profileConfiguration.get();
defaultTenantProfileConfiguration.deduplicateRateLimitsConfigs();
tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile);
updatedTenantProfiles++;
log.debug("[{}][{}] Successfully updated tenant profile.", profileId, profileName);
} catch (Exception e) {
log.error("[{}][{}] Failed to updated tenant profile: ", profileId, profileName, e);
failedProfiles++;
}
}
log.info("Tenant profiles update completed. Total: {}, Updated: {}, Skipped: {}, Failed: {}",
totalProfiles, updatedTenantProfiles, skippedProfiles, failedProfiles);
}
private void updateInputNodes() {
log.info("Creating relations for input nodes...");
int n = 0;

38
application/src/test/java/org/thingsboard/server/controller/TenantProfileControllerTest.java

@ -36,9 +36,11 @@ import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.validation.RateLimit;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.queue.TbQueueCallback;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@ -314,6 +316,42 @@ public class TenantProfileControllerTest extends AbstractControllerTest {
Assert.assertEquals(1, pageData.getTotalElements());
}
@Test
public void testRateLimitValidationAllFields() throws Exception {
loginSysAdmin();
Mockito.reset(tbClusterService);
List<String> failedFields = new ArrayList<>();
for (Field field : DefaultTenantProfileConfiguration.class.getDeclaredFields()) {
RateLimit rateLimit = field.getAnnotation(RateLimit.class);
if (rateLimit == null) continue;
String fieldName = field.getName();
String expectedLabel = rateLimit.fieldName();
TenantProfile tenantProfile = createTenantProfile("Invalid RateLimit - " + fieldName);
DefaultTenantProfileConfiguration config = (DefaultTenantProfileConfiguration) tenantProfile.getProfileData().getConfiguration();
field.setAccessible(true);
field.set(config, "10:1,10:1"); // Set invalid duplicate value
try {
doPost("/api/tenantProfile", tenantProfile)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(expectedLabel + " rate limit has duplicate 'Per seconds' configuration.")));
} catch (AssertionError e) {
failedFields.add(fieldName + " (label: " + expectedLabel + ")");
}
}
if (!failedFields.isEmpty()) {
throw new AssertionError("RateLimit validation failed for fields: " + String.join(", ", failedFields));
}
testBroadcastEntityStateChangeEventNeverTenantProfile();
}
private TenantProfile createTenantProfile(String name) {
TenantProfile tenantProfile = new TenantProfile();
tenantProfile.setName(name);

19
application/src/test/java/org/thingsboard/server/service/limits/RateLimitServiceTest.java

@ -52,7 +52,7 @@ public class RateLimitServiceTest {
public void beforeEach() {
tenantProfileCache = Mockito.mock(DefaultTbTenantProfileCache.class);
rateLimitService = new DefaultRateLimitService(tenantProfileCache, mock(NotificationRuleProcessor.class), 60, 100);
tenantId = new TenantId(UUID.randomUUID());
tenantId = TenantId.fromUUID(UUID.randomUUID());
}
@Test
@ -67,7 +67,10 @@ public class RateLimitServiceTest {
profileConfiguration.setTenantServerRestLimitsConfiguration(rateLimit);
profileConfiguration.setCustomerServerRestLimitsConfiguration(rateLimit);
profileConfiguration.setWsUpdatesPerSessionRateLimit(rateLimit);
profileConfiguration.setCassandraQueryTenantRateLimitsConfiguration(rateLimit);
profileConfiguration.setCassandraReadQueryTenantCoreRateLimits(rateLimit);
profileConfiguration.setCassandraWriteQueryTenantCoreRateLimits(rateLimit);
profileConfiguration.setCassandraReadQueryTenantRuleEngineRateLimits(rateLimit);
profileConfiguration.setCassandraWriteQueryTenantRuleEngineRateLimits(rateLimit);
profileConfiguration.setEdgeEventRateLimits(rateLimit);
profileConfiguration.setEdgeEventRateLimitsPerEdge(rateLimit);
profileConfiguration.setEdgeUplinkMessagesRateLimits(rateLimit);
@ -79,7 +82,10 @@ public class RateLimitServiceTest {
LimitedApi.ENTITY_IMPORT,
LimitedApi.NOTIFICATION_REQUESTS,
LimitedApi.REST_REQUESTS_PER_CUSTOMER,
LimitedApi.CASSANDRA_QUERIES,
LimitedApi.CASSANDRA_READ_QUERIES_CORE,
LimitedApi.CASSANDRA_WRITE_QUERIES_CORE,
LimitedApi.CASSANDRA_READ_QUERIES_RULE_ENGINE,
LimitedApi.CASSANDRA_WRITE_QUERIES_RULE_ENGINE,
LimitedApi.EDGE_EVENTS,
LimitedApi.EDGE_EVENTS_PER_EDGE,
LimitedApi.EDGE_UPLINK_MESSAGES,
@ -88,6 +94,13 @@ public class RateLimitServiceTest {
testRateLimits(limitedApi, max, tenantId);
}
for (LimitedApi limitedApi : List.of(
LimitedApi.CASSANDRA_READ_QUERIES_MONOLITH,
LimitedApi.CASSANDRA_WRITE_QUERIES_MONOLITH
)) {
testRateLimits(limitedApi, max * 2, tenantId);
}
CustomerId customerId = new CustomerId(UUID.randomUUID());
testRateLimits(LimitedApi.REST_REQUESTS_PER_CUSTOMER, max, customerId);

4
common/coap-server/pom.xml

@ -46,6 +46,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>data</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common.transport</groupId>
<artifactId>transport-api</artifactId>

36
common/data/src/main/java/org/thingsboard/server/common/data/limit/LimitedApi.java

@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileCon
import java.util.Optional;
import java.util.function.Function;
@Getter
public enum LimitedApi {
ENTITY_EXPORT(DefaultTenantProfileConfiguration::getTenantEntityExportRateLimit, "entity version creation", true),
@ -30,7 +31,18 @@ public enum LimitedApi {
REST_REQUESTS_PER_TENANT(DefaultTenantProfileConfiguration::getTenantServerRestLimitsConfiguration, "REST API requests", true),
REST_REQUESTS_PER_CUSTOMER(DefaultTenantProfileConfiguration::getCustomerServerRestLimitsConfiguration, "REST API requests per customer", false),
WS_UPDATES_PER_SESSION(DefaultTenantProfileConfiguration::getWsUpdatesPerSessionRateLimit, "WS updates per session", true),
CASSANDRA_QUERIES(DefaultTenantProfileConfiguration::getCassandraQueryTenantRateLimitsConfiguration, "Cassandra queries", true),
CASSANDRA_WRITE_QUERIES_CORE(DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantCoreRateLimits, "Rest API Cassandra write queries", true),
CASSANDRA_READ_QUERIES_CORE(DefaultTenantProfileConfiguration::getCassandraReadQueryTenantCoreRateLimits, "Rest API and WS telemetry Cassandra read queries", true),
CASSANDRA_WRITE_QUERIES_RULE_ENGINE(DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantRuleEngineRateLimits, "Rule Engine telemetry Cassandra write queries", true),
CASSANDRA_READ_QUERIES_RULE_ENGINE(DefaultTenantProfileConfiguration::getCassandraReadQueryTenantRuleEngineRateLimits, "Rule Engine telemetry Cassandra read queries", true),
CASSANDRA_READ_QUERIES_MONOLITH(
RateLimitUtil.merge(
DefaultTenantProfileConfiguration::getCassandraReadQueryTenantCoreRateLimits,
DefaultTenantProfileConfiguration::getCassandraReadQueryTenantRuleEngineRateLimits), "Telemetry read queries", true),
CASSANDRA_WRITE_QUERIES_MONOLITH(
RateLimitUtil.merge(
DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantCoreRateLimits,
DefaultTenantProfileConfiguration::getCassandraWriteQueryTenantRuleEngineRateLimits), "Telemetry write queries", true),
EDGE_EVENTS(DefaultTenantProfileConfiguration::getEdgeEventRateLimits, "Edge events", true),
EDGE_EVENTS_PER_EDGE(DefaultTenantProfileConfiguration::getEdgeEventRateLimitsPerEdge, "Edge events per edge", false),
EDGE_UPLINK_MESSAGES(DefaultTenantProfileConfiguration::getEdgeUplinkMessagesRateLimits, "Edge uplink messages", true),
@ -46,28 +58,28 @@ public enum LimitedApi {
WS_SUBSCRIPTIONS("WS subscriptions", false),
CALCULATED_FIELD_DEBUG_EVENTS("calculated field debug events", true);
private Function<DefaultTenantProfileConfiguration, String> configExtractor;
@Getter
private final Function<DefaultTenantProfileConfiguration, String> configExtractor;
private final boolean perTenant;
@Getter
private boolean refillRateLimitIntervally;
@Getter
private String label;
private final boolean refillRateLimitIntervally;
private final String label;
LimitedApi(Function<DefaultTenantProfileConfiguration, String> configExtractor, String label, boolean perTenant) {
this.configExtractor = configExtractor;
this.label = label;
this.perTenant = perTenant;
this(configExtractor, label, perTenant, false);
}
LimitedApi(boolean perTenant, boolean refillRateLimitIntervally) {
this.perTenant = perTenant;
this.refillRateLimitIntervally = refillRateLimitIntervally;
this(null, null, perTenant, refillRateLimitIntervally);
}
LimitedApi(String label, boolean perTenant) {
this(null, label, perTenant, false);
}
LimitedApi(Function<DefaultTenantProfileConfiguration, String> configExtractor, String label, boolean perTenant, boolean refillRateLimitIntervally) {
this.configExtractor = configExtractor;
this.label = label;
this.perTenant = perTenant;
this.refillRateLimitIntervally = refillRateLimitIntervally;
}
public String getLimitConfig(DefaultTenantProfileConfiguration profileConfiguration) {

30
common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitEntry.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2025 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.data.limit;
public record RateLimitEntry(long capacity, long durationSeconds) {
public static RateLimitEntry parse(String s) {
String[] parts = s.split(":");
return new RateLimitEntry(Long.parseLong(parts[0]), Long.parseLong(parts[1]));
}
@Override
public String toString() {
return capacity + ":" + durationSeconds;
}
}

92
common/data/src/main/java/org/thingsboard/server/common/data/limit/RateLimitUtil.java

@ -0,0 +1,92 @@
/**
* Copyright © 2016-2025 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.data.limit;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
public class RateLimitUtil {
public static List<RateLimitEntry> parseConfig(String config) {
if (config == null || config.isEmpty()) {
return Collections.emptyList();
}
return Arrays.stream(config.split(","))
.map(RateLimitEntry::parse)
.toList();
}
public static Function<DefaultTenantProfileConfiguration, String> merge(
Function<DefaultTenantProfileConfiguration, String> configExtractor1,
Function<DefaultTenantProfileConfiguration, String> configExtractor2) {
return config -> {
String config1 = configExtractor1.apply(config);
String config2 = configExtractor2.apply(config);
return RateLimitUtil.mergeStrConfigs(config1, config2); // merges the configs
};
}
private static String mergeStrConfigs(String firstConfig, String secondConfig) {
List<RateLimitEntry> all = new ArrayList<>();
all.addAll(parseConfig(firstConfig));
all.addAll(parseConfig(secondConfig));
Map<Long, Long> merged = new HashMap<>();
for (RateLimitEntry entry : all) {
merged.merge(entry.durationSeconds(), entry.capacity(), Long::sum);
}
return merged.entrySet().stream()
.sorted(Map.Entry.comparingByKey()) // optional: sort by duration
.map(e -> e.getValue() + ":" + e.getKey())
.collect(Collectors.joining(","));
}
public static boolean isValid(String configStr) {
List<RateLimitEntry> limitedApiEntries = parseConfig(configStr);
Set<Long> distinctDurations = new HashSet<>();
for (RateLimitEntry entry : limitedApiEntries) {
if (!distinctDurations.add(entry.durationSeconds())) {
return false;
}
}
return true;
}
@Deprecated(forRemoval = true, since = "4.1")
public static String deduplicateByDuration(String configStr) {
if (configStr == null) {
return null;
}
Set<Long> distinctDurations = new HashSet<>();
return parseConfig(configStr).stream()
.filter(entry -> distinctDurations.add(entry.durationSeconds()))
.map(RateLimitEntry::toString)
.collect(Collectors.joining(","));
}
}

74
common/data/src/main/java/org/thingsboard/server/common/data/tenant/profile/DefaultTenantProfileConfiguration.java

@ -24,6 +24,8 @@ import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.TenantProfileType;
import org.thingsboard.server.common.data.limit.RateLimitUtil;
import org.thingsboard.server.common.data.validation.RateLimit;
import java.io.Serial;
@ -49,37 +51,53 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxResourceSize;
@Schema(example = "1000:1,20000:60")
@RateLimit(fieldName = "Transport tenant messages")
private String transportTenantMsgRateLimit;
@Schema(example = "1000:1,20000:60")
@RateLimit(fieldName = "Transport tenant telemetry messages")
private String transportTenantTelemetryMsgRateLimit;
@Schema(example = "1000:1,20000:60")
@RateLimit(fieldName = "Transport tenant telemetry data points")
private String transportTenantTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport device messages")
private String transportDeviceMsgRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport device telemetry messages")
private String transportDeviceTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport device telemetry data points")
private String transportDeviceTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport gateway messages")
private String transportGatewayMsgRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport gateway telemetry messages")
private String transportGatewayTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport gateway telemetry data points")
private String transportGatewayTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport gateway device messages")
private String transportGatewayDeviceMsgRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport gateway device telemetry messages")
private String transportGatewayDeviceTelemetryMsgRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Transport gateway device telemetry data points")
private String transportGatewayDeviceTelemetryDataPointsRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Entity version creation")
private String tenantEntityExportRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Entity version load")
private String tenantEntityImportRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Notification requests")
private String tenantNotificationRequestsRateLimit;
@Schema(example = "20:1,600:60")
@RateLimit(fieldName = "Notification requests per notification rule")
private String tenantNotificationRequestsPerRuleRateLimit;
@Schema(example = "10000000")
@ -107,7 +125,9 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
@Schema(example = "1000")
private long maxCreatedAlarms;
@RateLimit(fieldName = "REST requests for tenant")
private String tenantServerRestLimitsConfiguration;
@RateLimit(fieldName = "REST requests for customer")
private String customerServerRestLimitsConfiguration;
private int maxWsSessionsPerTenant;
@ -119,13 +139,26 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
private long maxWsSubscriptionsPerCustomer;
private long maxWsSubscriptionsPerRegularUser;
private long maxWsSubscriptionsPerPublicUser;
@RateLimit(fieldName = "WS updates per session")
private String wsUpdatesPerSessionRateLimit;
private String cassandraQueryTenantRateLimitsConfiguration;
@RateLimit(fieldName = "Rest API and WS telemetry Cassandra read queries")
private String cassandraReadQueryTenantCoreRateLimits;
@RateLimit(fieldName = "Rest API Cassandra write queries")
private String cassandraWriteQueryTenantCoreRateLimits;
@RateLimit(fieldName = "Rule Engine telemetry Cassandra read queries")
private String cassandraReadQueryTenantRuleEngineRateLimits;
@RateLimit(fieldName = "Rule Engine telemetry Cassandra write queries")
private String cassandraWriteQueryTenantRuleEngineRateLimits;
@RateLimit(fieldName = "Edge events")
private String edgeEventRateLimits;
@RateLimit(fieldName = "Edge events per edge")
private String edgeEventRateLimitsPerEdge;
@RateLimit(fieldName = "Edge uplink messages")
private String edgeUplinkMessagesRateLimits;
@RateLimit(fieldName = "Edge uplink messages per edge")
private String edgeUplinkMessagesRateLimitsPerEdge;
private int defaultStorageTtlDays;
@ -203,4 +236,43 @@ public class DefaultTenantProfileConfiguration implements TenantProfileConfigura
return maxRuleNodeExecutionsPerMessage;
}
@Deprecated(forRemoval = true, since = "4.1")
public void deduplicateRateLimitsConfigs() {
this.transportTenantMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantMsgRateLimit);
this.transportTenantTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryMsgRateLimit);
this.transportTenantTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportTenantTelemetryDataPointsRateLimit);
this.transportDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceMsgRateLimit);
this.transportDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryMsgRateLimit);
this.transportDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportDeviceTelemetryDataPointsRateLimit);
this.transportGatewayMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayMsgRateLimit);
this.transportGatewayTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryMsgRateLimit);
this.transportGatewayTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayTelemetryDataPointsRateLimit);
this.transportGatewayDeviceMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceMsgRateLimit);
this.transportGatewayDeviceTelemetryMsgRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryMsgRateLimit);
this.transportGatewayDeviceTelemetryDataPointsRateLimit = RateLimitUtil.deduplicateByDuration(transportGatewayDeviceTelemetryDataPointsRateLimit);
this.tenantEntityExportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityExportRateLimit);
this.tenantEntityImportRateLimit = RateLimitUtil.deduplicateByDuration(tenantEntityImportRateLimit);
this.tenantNotificationRequestsRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsRateLimit);
this.tenantNotificationRequestsPerRuleRateLimit = RateLimitUtil.deduplicateByDuration(tenantNotificationRequestsPerRuleRateLimit);
this.cassandraReadQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantCoreRateLimits);
this.cassandraWriteQueryTenantCoreRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantCoreRateLimits);
this.cassandraReadQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraReadQueryTenantRuleEngineRateLimits);
this.cassandraWriteQueryTenantRuleEngineRateLimits = RateLimitUtil.deduplicateByDuration(cassandraWriteQueryTenantRuleEngineRateLimits);
this.edgeEventRateLimits = RateLimitUtil.deduplicateByDuration(edgeEventRateLimits);
this.edgeEventRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeEventRateLimitsPerEdge);
this.edgeUplinkMessagesRateLimits = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimits);
this.edgeUplinkMessagesRateLimitsPerEdge = RateLimitUtil.deduplicateByDuration(edgeUplinkMessagesRateLimitsPerEdge);
this.wsUpdatesPerSessionRateLimit = RateLimitUtil.deduplicateByDuration(wsUpdatesPerSessionRateLimit);
this.tenantServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(tenantServerRestLimitsConfiguration);
this.customerServerRestLimitsConfiguration = RateLimitUtil.deduplicateByDuration(customerServerRestLimitsConfiguration);
}
}

39
common/data/src/main/java/org/thingsboard/server/common/data/validation/RateLimit.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2025 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.data.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
@Constraint(validatedBy = {})
public @interface RateLimit {
String message() default "rate limit has duplicate 'Per seconds' configuration.";
String fieldName() default "";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

101
common/data/src/test/java/org/thingsboard/server/common/data/limit/LimitedApiTest.java

@ -0,0 +1,101 @@
/**
* Copyright © 2016-2025 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.data.limit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
class LimitedApiTest {
private DefaultTenantProfileConfiguration config;
@BeforeEach
void setUp() {
config = mock(DefaultTenantProfileConfiguration.class);
}
@Test
void testCorrectConfigExtractorsUsed() {
Map<LimitedApi, Runnable> verifierMap = Map.ofEntries(
Map.entry(LimitedApi.ENTITY_EXPORT, () ->
verify(config).getTenantEntityExportRateLimit()),
Map.entry(LimitedApi.ENTITY_IMPORT, () ->
verify(config).getTenantEntityImportRateLimit()),
Map.entry(LimitedApi.NOTIFICATION_REQUESTS, () ->
verify(config).getTenantNotificationRequestsRateLimit()),
Map.entry(LimitedApi.NOTIFICATION_REQUESTS_PER_RULE, () ->
verify(config).getTenantNotificationRequestsPerRuleRateLimit()),
Map.entry(LimitedApi.REST_REQUESTS_PER_TENANT, () ->
verify(config).getTenantServerRestLimitsConfiguration()),
Map.entry(LimitedApi.REST_REQUESTS_PER_CUSTOMER, () ->
verify(config).getCustomerServerRestLimitsConfiguration()),
Map.entry(LimitedApi.WS_UPDATES_PER_SESSION, () ->
verify(config).getWsUpdatesPerSessionRateLimit()),
Map.entry(LimitedApi.CASSANDRA_WRITE_QUERIES_CORE, () ->
verify(config).getCassandraWriteQueryTenantCoreRateLimits()),
Map.entry(LimitedApi.CASSANDRA_READ_QUERIES_CORE, () ->
verify(config).getCassandraReadQueryTenantCoreRateLimits()),
Map.entry(LimitedApi.CASSANDRA_WRITE_QUERIES_RULE_ENGINE, () ->
verify(config).getCassandraWriteQueryTenantRuleEngineRateLimits()),
Map.entry(LimitedApi.CASSANDRA_READ_QUERIES_RULE_ENGINE, () ->
verify(config).getCassandraReadQueryTenantRuleEngineRateLimits()),
Map.entry(LimitedApi.CASSANDRA_READ_QUERIES_MONOLITH, () -> {
verify(config).getCassandraReadQueryTenantCoreRateLimits();
verify(config).getCassandraReadQueryTenantRuleEngineRateLimits();
}),
Map.entry(LimitedApi.CASSANDRA_WRITE_QUERIES_MONOLITH, () -> {
verify(config).getCassandraWriteQueryTenantCoreRateLimits();
verify(config).getCassandraWriteQueryTenantRuleEngineRateLimits();
}),
Map.entry(LimitedApi.EDGE_EVENTS, () ->
verify(config).getEdgeEventRateLimits()),
Map.entry(LimitedApi.EDGE_EVENTS_PER_EDGE, () ->
verify(config).getEdgeEventRateLimitsPerEdge()),
Map.entry(LimitedApi.EDGE_UPLINK_MESSAGES, () ->
verify(config).getEdgeUplinkMessagesRateLimits()),
Map.entry(LimitedApi.EDGE_UPLINK_MESSAGES_PER_EDGE, () ->
verify(config).getEdgeUplinkMessagesRateLimitsPerEdge())
);
Set<LimitedApi> expected = verifierMap.keySet();
Set<LimitedApi> actual = Arrays.stream(LimitedApi.values())
.filter(api -> api.getConfigExtractor() != null)
.collect(Collectors.toSet());
assertThat(expected)
.as("Verifier map should cover all LimitedApis with extractors")
.containsExactlyInAnyOrderElementsOf(actual);
for (Map.Entry<LimitedApi, Runnable> entry : verifierMap.entrySet()) {
LimitedApi api = entry.getKey();
api.getLimitConfig(config);
entry.getValue().run();
clearInvocations(config);
}
}
}

99
common/data/src/test/java/org/thingsboard/server/common/data/limit/RateLimitUtilTest.java

@ -0,0 +1,99 @@
/**
* Copyright © 2016-2025 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.data.limit;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import java.util.List;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
class RateLimitUtilTest {
@Test
@DisplayName("LimitedApiUtil should parse single entry correctly")
void testParseSingleEntry() {
List<RateLimitEntry> entries = RateLimitUtil.parseConfig("100:60");
assertThat(entries).hasSize(1);
assertThat(entries.get(0).capacity()).isEqualTo(100);
assertThat(entries.get(0).durationSeconds()).isEqualTo(60);
}
@Test
@DisplayName("LimitedApiUtil should parse multiple entries correctly")
void testParseMultipleEntries() {
List<RateLimitEntry> entries = RateLimitUtil.parseConfig("100:60,200:30");
assertThat(entries).hasSize(2);
assertThat(entries.get(0).capacity()).isEqualTo(100);
assertThat(entries.get(0).durationSeconds()).isEqualTo(60);
assertThat(entries.get(1).capacity()).isEqualTo(200);
assertThat(entries.get(1).durationSeconds()).isEqualTo(30);
}
@Test
@DisplayName("LimitedApiUtil should return empty list for null or empty config")
void testParseEmptyConfig() {
assertThat(RateLimitUtil.parseConfig(null)).isEmpty();
assertThat(RateLimitUtil.parseConfig("")).isEmpty();
}
@Test
@DisplayName("LimitedApiUtil should merge two configs by summing capacities with same durations")
void testMergeStrConfigs() {
Function<DefaultTenantProfileConfiguration, String> extractor1 = cfg -> "100:60,50:30";
Function<DefaultTenantProfileConfiguration, String> extractor2 = cfg -> "200:60,25:10";
// Fake config instance (not used directly in lambda logic)
DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration();
String result = RateLimitUtil.merge(extractor1, extractor2).apply(config);
// Should be: 300:60 (100+200), 50:30, 25:10
assertThat(result).isEqualTo("25:10,50:30,300:60");
}
@Test
@DisplayName("LimitedApiUtil should merge configs when one is empty")
void testMergeWithEmptyOne() {
Function<DefaultTenantProfileConfiguration, String> extractor1 = cfg -> "100:60";
Function<DefaultTenantProfileConfiguration, String> extractor2 = cfg -> "";
// Fake config instance (not used directly in lambda logic)
DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration();
String result = RateLimitUtil.merge(extractor1, extractor2).apply(config);
assertThat(result).isEqualTo("100:60");
}
@Test
@DisplayName("LimitedApiUtil should merge configs when both have distinct durations")
void testMergeWithDistinctDurations() {
Function<DefaultTenantProfileConfiguration, String> extractor1 = cfg -> "100:60";
Function<DefaultTenantProfileConfiguration, String> extractor2 = cfg -> "200:10";
// Fake config instance (not used directly in lambda logic)
DefaultTenantProfileConfiguration config = new DefaultTenantProfileConfiguration();
String result = RateLimitUtil.merge(extractor1, extractor2).apply(config);
assertThat(result).isEqualTo("200:10,100:60");
}
}

73
common/discovery-api/pom.xml

@ -0,0 +1,73 @@
<!--
Copyright © 2016-2025 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.thingsboard</groupId>
<version>4.1.0-SNAPSHOT</version>
<artifactId>common</artifactId>
</parent>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
<packaging>jar</packaging>
<name>Thingsboard Server Service Info Provider API</name>
<url>https://thingsboard.io</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<main.dir>${basedir}/../..</main.dir>
</properties>
<dependencies>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>proto</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>message</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

2
common/queue/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java → common/discovery-api/src/main/java/org/thingsboard/server/queue/discovery/TbServiceInfoProvider.java

@ -29,6 +29,8 @@ public interface TbServiceInfoProvider {
ServiceInfo getServiceInfo();
boolean isMonolith();
boolean isService(ServiceType serviceType);
ServiceInfo generateNewServiceInfoWithCurrentSystemInfo();

4
common/edqs/pom.xml

@ -68,6 +68,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>queue</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

28
common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java

@ -16,13 +16,16 @@
package org.thingsboard.server.common.msg.tools;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.BandwidthBuilder;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Refill;
import io.github.bucket4j.local.LocalBucket;
import io.github.bucket4j.local.LocalBucketBuilder;
import lombok.Getter;
import org.thingsboard.server.common.data.limit.RateLimitEntry;
import org.thingsboard.server.common.data.limit.RateLimitUtil;
import java.time.Duration;
import java.util.List;
/**
* Created by ashvayka on 22.10.18.
@ -38,20 +41,19 @@ public class TbRateLimits {
}
public TbRateLimits(String limitsConfiguration, boolean refillIntervally) {
LocalBucketBuilder builder = Bucket.builder();
boolean initialized = false;
for (String limitSrc : limitsConfiguration.split(",")) {
long capacity = Long.parseLong(limitSrc.split(":")[0]);
long duration = Long.parseLong(limitSrc.split(":")[1]);
Refill refill = refillIntervally ? Refill.intervally(capacity, Duration.ofSeconds(duration)) : Refill.greedy(capacity, Duration.ofSeconds(duration));
builder.addLimit(Bandwidth.classic(capacity, refill));
initialized = true;
}
if (initialized) {
bucket = builder.build();
} else {
List<RateLimitEntry> limitedApiEntries = RateLimitUtil.parseConfig(limitsConfiguration);
if (limitedApiEntries.isEmpty()) {
throw new IllegalArgumentException("Failed to parse rate limits configuration: " + limitsConfiguration);
}
LocalBucketBuilder localBucket = Bucket.builder();
for (RateLimitEntry entry : limitedApiEntries) {
BandwidthBuilder.BandwidthBuilderRefillStage bandwidthBuilder = Bandwidth.builder().capacity(entry.capacity());
Bandwidth bandwidth = refillIntervally ?
bandwidthBuilder.refillIntervally(entry.capacity(), Duration.ofSeconds(entry.durationSeconds())).build() :
bandwidthBuilder.refillGreedy(entry.capacity(), Duration.ofSeconds(entry.durationSeconds())).build();
localBucket.addLimit(bandwidth);
}
this.bucket = localBucket.build();
this.configuration = limitsConfiguration;
}

41
common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java → common/message/src/test/java/org/thingsboard/server/common/msg/tools/TbRateLimitsTest.java

@ -16,14 +16,16 @@
package org.thingsboard.server.common.msg.tools;
import org.awaitility.pollinterval.FixedPollInterval;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.awaitility.Awaitility.await;
public class RateLimitsTest {
public class TbRateLimitsTest {
@Test
public void testRateLimits_greedyRefill() {
@ -83,4 +85,41 @@ public class RateLimitsTest {
});
}
@Test
@DisplayName("TbRateLimits should construct with single rate limit")
void testSingleLimitConstructor() {
TbRateLimits limits = new TbRateLimits("10:1", false);
assertThat(limits.getConfiguration()).isEqualTo("10:1");
}
@Test
@DisplayName("TbRateLimits should construct with multiple rate limits")
void testMultipleLimitConstructor() {
String config = "10:1,100:10";
TbRateLimits limits = new TbRateLimits(config, false);
assertThat(limits.getConfiguration()).isEqualTo(config);
}
@Test
@DisplayName("TbRateLimits should throw IllegalArgumentException on empty string")
void testEmptyConfigThrows() {
assertThatThrownBy(() -> new TbRateLimits("", false))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Failed to parse rate limits configuration: ");
}
@Test
@DisplayName("TbRateLimits should throw NumberFormatException on malformed value")
void testMalformedConfigThrows() {
assertThatThrownBy(() -> new TbRateLimits("not_a_number:second", false))
.isInstanceOf(NumberFormatException.class);
}
@Test
@DisplayName("TbRateLimits should throw ArrayIndexOutOfBoundsException on missing colon")
void testColonMissingThrows() {
assertThatThrownBy(() -> new TbRateLimits("100", false))
.isInstanceOf(ArrayIndexOutOfBoundsException.class);
}
}

1
common/pom.xml

@ -50,6 +50,7 @@
<module>version-control</module>
<module>script</module>
<module>edqs</module>
<module>discovery-api</module>
</modules>
</project>

4
common/queue/pom.xml

@ -60,6 +60,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>cluster-api</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
</dependency>
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>

13
common/queue/src/main/java/org/thingsboard/server/queue/discovery/DefaultTbServiceInfoProvider.java

@ -91,11 +91,9 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider {
}
}
log.info("Current Service ID: {}", serviceId);
if (serviceType.equalsIgnoreCase("monolith")) {
serviceTypes = List.of(ServiceType.values());
} else {
serviceTypes = Collections.singletonList(ServiceType.of(serviceType));
}
serviceTypes = isMonolith() ?
List.of(ServiceType.values()) :
Collections.singletonList(ServiceType.of(serviceType));
if (!serviceTypes.contains(ServiceType.TB_RULE_ENGINE) || assignedTenantProfiles == null) {
assignedTenantProfiles = Collections.emptySet();
}
@ -134,6 +132,11 @@ public class DefaultTbServiceInfoProvider implements TbServiceInfoProvider {
return serviceInfo;
}
@Override
public boolean isMonolith() {
return serviceType.equalsIgnoreCase("monolith");
}
@Override
public boolean isService(ServiceType serviceType) {
return serviceTypes.contains(serviceType);

4
common/transport/transport-api/pom.xml

@ -60,6 +60,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>util</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>

4
dao/pom.xml

@ -59,6 +59,10 @@
<groupId>org.thingsboard.common</groupId>
<artifactId>util</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>

16
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateReadExecutor.java

@ -28,7 +28,9 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor;
import org.thingsboard.server.dao.util.AsyncTaskContext;
import org.thingsboard.server.dao.util.BufferedRateExecutorType;
import org.thingsboard.server.dao.util.NoSqlAnyDao;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
/**
* Created by ashvayka on 24.10.18.
@ -38,8 +40,6 @@ import org.thingsboard.server.dao.util.NoSqlAnyDao;
@NoSqlAnyDao
public class CassandraBufferedRateReadExecutor extends AbstractBufferedRateExecutor<CassandraStatementTask, TbResultSetFuture, TbResultSet> {
static final String BUFFER_NAME = "Read";
public CassandraBufferedRateReadExecutor(
@Value("${cassandra.query.buffer_size}") int queueLimit,
@Value("${cassandra.query.concurrent_limit}") int concurrencyLimit,
@ -51,9 +51,10 @@ public class CassandraBufferedRateReadExecutor extends AbstractBufferedRateExecu
@Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq,
@Autowired StatsFactory statsFactory,
@Autowired EntityService entityService,
@Autowired RateLimitService rateLimitService) {
super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, printQueriesFreq, statsFactory,
entityService, rateLimitService, printTenantNames);
@Autowired RateLimitService rateLimitService,
@Autowired(required = false) TbServiceInfoProvider serviceInfoProvider) {
super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, printQueriesFreq,
BufferedRateExecutorType.READ, serviceInfoProvider, rateLimitService, statsFactory, entityService, printTenantNames);
}
@Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
@ -67,11 +68,6 @@ public class CassandraBufferedRateReadExecutor extends AbstractBufferedRateExecu
super.stop();
}
@Override
public String getBufferName() {
return BUFFER_NAME;
}
@Override
protected SettableFuture<TbResultSet> create() {
return SettableFuture.create();

16
dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraBufferedRateWriteExecutor.java

@ -28,7 +28,9 @@ import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.util.AbstractBufferedRateExecutor;
import org.thingsboard.server.dao.util.AsyncTaskContext;
import org.thingsboard.server.dao.util.BufferedRateExecutorType;
import org.thingsboard.server.dao.util.NoSqlAnyDao;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
/**
* Created by ashvayka on 24.10.18.
@ -38,8 +40,6 @@ import org.thingsboard.server.dao.util.NoSqlAnyDao;
@NoSqlAnyDao
public class CassandraBufferedRateWriteExecutor extends AbstractBufferedRateExecutor<CassandraStatementTask, TbResultSetFuture, TbResultSet> {
static final String BUFFER_NAME = "Write";
public CassandraBufferedRateWriteExecutor(
@Value("${cassandra.query.buffer_size}") int queueLimit,
@Value("${cassandra.query.concurrent_limit}") int concurrencyLimit,
@ -51,9 +51,10 @@ public class CassandraBufferedRateWriteExecutor extends AbstractBufferedRateExec
@Value("${cassandra.query.print_queries_freq:0}") int printQueriesFreq,
@Autowired StatsFactory statsFactory,
@Autowired EntityService entityService,
@Autowired RateLimitService rateLimitService) {
super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, printQueriesFreq, statsFactory,
entityService, rateLimitService, printTenantNames);
@Autowired RateLimitService rateLimitService,
@Autowired(required = false) TbServiceInfoProvider serviceInfoProvider) {
super(queueLimit, concurrencyLimit, maxWaitTime, dispatcherThreads, callbackThreads, pollMs, printQueriesFreq,
BufferedRateExecutorType.WRITE, serviceInfoProvider, rateLimitService, statsFactory, entityService, printTenantNames);
}
@Scheduled(fixedDelayString = "${cassandra.query.rate_limit_print_interval_ms}")
@ -67,11 +68,6 @@ public class CassandraBufferedRateWriteExecutor extends AbstractBufferedRateExec
super.stop();
}
@Override
public String getBufferName() {
return BUFFER_NAME;
}
@Override
protected SettableFuture<TbResultSet> create() {
return SettableFuture.create();

2
dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java

@ -33,6 +33,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import org.thingsboard.server.common.data.validation.RateLimit;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.Collection;
@ -103,6 +104,7 @@ public class ConstraintValidator {
ConstraintMapping constraintMapping = new DefaultConstraintMapping(null);
constraintMapping.constraintDefinition(NoXss.class).validatedBy(NoXssValidator.class);
constraintMapping.constraintDefinition(Length.class).validatedBy(StringLengthValidator.class);
constraintMapping.constraintDefinition(RateLimit.class).validatedBy(RateLimitValidator.class);
return constraintMapping;
}

32
dao/src/main/java/org/thingsboard/server/dao/service/RateLimitValidator.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2025 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.service;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.limit.RateLimitUtil;
import org.thingsboard.server.common.data.validation.RateLimit;
@Slf4j
public class RateLimitValidator implements ConstraintValidator<RateLimit, String> {
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
return value == null || RateLimitUtil.isValid(value);
}
}

6
dao/src/main/java/org/thingsboard/server/dao/service/validator/TenantProfileDataValidator.java

@ -24,6 +24,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.queue.ProcessingStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.service.DataValidator;
@ -51,9 +52,12 @@ public class TenantProfileDataValidator extends DataValidator<TenantProfile> {
if (tenantProfile.getProfileData() == null) {
throw new DataValidationException("Tenant profile data should be specified!");
}
if (tenantProfile.getProfileData().getConfiguration() == null) {
Optional<DefaultTenantProfileConfiguration> profileConfiguration = tenantProfile.getProfileConfiguration();
if (profileConfiguration.isEmpty()) {
throw new DataValidationException("Tenant profile data configuration should be specified!");
}
if (tenantProfile.isDefault()) {
TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId);
if (defaultTenantProfile != null && !defaultTenantProfile.getId().equals(tenantProfile.getId())) {

49
dao/src/main/java/org/thingsboard/server/dao/util/AbstractBufferedRateExecutor.java

@ -34,12 +34,14 @@ import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cache.limits.RateLimitService;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.stats.DefaultCounter;
import org.thingsboard.server.common.stats.StatsCounter;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.common.stats.StatsType;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.nosql.CassandraStatementTask;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import java.util.HashMap;
import java.util.Map;
@ -64,6 +66,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
private final long maxWaitTime;
private final long pollMs;
private final String bufferName;
private final BlockingQueue<AsyncTaskContext<T, V>> queue;
private final ExecutorService dispatcherExecutor;
private final ExecutorService callbackExecutor;
@ -82,19 +85,23 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
private final boolean printTenantNames;
private final Map<TenantId, String> tenantNamesCache = new HashMap<>();
private final LimitedApi myLimitedApi;
public AbstractBufferedRateExecutor(int queueLimit, int concurrencyLimit, long maxWaitTime, int dispatcherThreads,
int callbackThreads, long pollMs, int printQueriesFreq, StatsFactory statsFactory,
EntityService entityService, RateLimitService rateLimitService, boolean printTenantNames) {
int callbackThreads, long pollMs, int printQueriesFreq, BufferedRateExecutorType executorType, TbServiceInfoProvider serviceInfoProvider,
RateLimitService rateLimitService, StatsFactory statsFactory, EntityService entityService, boolean printTenantNames) {
this.maxWaitTime = maxWaitTime;
this.pollMs = pollMs;
this.bufferName = executorType.getDisplayName();
this.myLimitedApi = resolveLimitedApi(serviceInfoProvider, executorType);
this.concurrencyLimit = concurrencyLimit;
this.printQueriesFreq = printQueriesFreq;
this.queue = new LinkedBlockingDeque<>(queueLimit);
this.dispatcherExecutor = Executors.newFixedThreadPool(dispatcherThreads, ThingsBoardThreadFactory.forName("nosql-" + getBufferName() + "-dispatcher"));
this.callbackExecutor = ThingsBoardExecutors.newWorkStealingPool(callbackThreads, "nosql-" + getBufferName() + "-callback");
this.timeoutExecutor = ThingsBoardExecutors.newSingleThreadScheduledExecutor("nosql-" + getBufferName() + "-timeout");
this.dispatcherExecutor = Executors.newFixedThreadPool(dispatcherThreads, ThingsBoardThreadFactory.forName("nosql-" + bufferName + "-dispatcher"));
this.callbackExecutor = ThingsBoardExecutors.newWorkStealingPool(callbackThreads, "nosql-" + bufferName + "-callback");
this.timeoutExecutor = ThingsBoardExecutors.newSingleThreadScheduledExecutor("nosql-" + bufferName + "-timeout");
this.stats = new BufferedRateExecutorStats(statsFactory);
String concurrencyLevelKey = StatsType.RATE_EXECUTOR.getName() + "." + CONCURRENCY_LEVEL + getBufferName(); //metric name may change with buffer name suffix
String concurrencyLevelKey = StatsType.RATE_EXECUTOR.getName() + "." + CONCURRENCY_LEVEL + bufferName; //metric name may change with buffer name suffix
this.concurrencyLevel = statsFactory.createGauge(concurrencyLevelKey, new AtomicInteger(0));
this.entityService = entityService;
@ -114,14 +121,14 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
boolean perTenantLimitReached = false;
TenantId tenantId = task.getTenantId();
if (tenantId != null && !tenantId.isSysTenantId()) {
if (!rateLimitService.checkRateLimit(LimitedApi.CASSANDRA_QUERIES, tenantId, tenantId, true)) {
if (!rateLimitService.checkRateLimit(myLimitedApi, tenantId, tenantId, true)) {
stats.incrementRateLimitedTenant(tenantId);
stats.getTotalRateLimited().increment();
settableFuture.setException(new TenantRateLimitException());
perTenantLimitReached = true;
}
} else if (tenantId == null) {
log.info("[{}] Invalid task received: {}", getBufferName(), task);
log.info("[{}] Invalid task received: {}", bufferName, task);
}
if (!perTenantLimitReached) {
@ -136,6 +143,16 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
return result;
}
private LimitedApi resolveLimitedApi(TbServiceInfoProvider serviceInfoProvider, BufferedRateExecutorType executorType) {
if (serviceInfoProvider == null || serviceInfoProvider.isMonolith()) {
return executorType.getMonolithLimitedApi();
}
if (serviceInfoProvider.isService(ServiceType.TB_RULE_ENGINE)) {
return executorType.getRuleEngineLimitedApi();
}
return executorType.getCoreLimitedApi();
}
public void stop() {
if (dispatcherExecutor != null) {
dispatcherExecutor.shutdownNow();
@ -154,10 +171,8 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
protected abstract ListenableFuture<V> execute(AsyncTaskContext<T, V> taskCtx);
public abstract String getBufferName();
private void dispatch() {
log.info("[{}] Buffered rate executor thread started", getBufferName());
log.info("[{}] Buffered rate executor thread started", bufferName);
while (!Thread.interrupted()) {
int curLvl = concurrencyLevel.get();
AsyncTaskContext<T, V> taskCtx = null;
@ -169,7 +184,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
if (printQueriesIdx.incrementAndGet() >= printQueriesFreq) {
printQueriesIdx.set(0);
String query = queryToString(finalTaskCtx);
log.info("[{}][{}] Cassandra query: {}", getBufferName(), taskCtx.getId(), query);
log.info("[{}][{}] Cassandra query: {}", bufferName, taskCtx.getId(), query);
}
}
logTask("Processing", finalTaskCtx);
@ -222,7 +237,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
}
}
}
log.info("[{}] Buffered rate executor thread stopped", getBufferName());
log.info("[{}] Buffered rate executor thread stopped", bufferName);
}
private void logTask(String action, AsyncTaskContext<T, V> taskCtx) {
@ -298,7 +313,7 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
statsBuilder.append(CONCURRENCY_LEVEL).append(" = [").append(concurrencyLevel.get()).append("] ");
stats.getStatsCounters().forEach(StatsCounter::clear);
log.info("[{}] Permits {}", getBufferName(), statsBuilder);
log.info("[{}] Permits {}", bufferName, statsBuilder);
}
stats.getRateLimitedTenants().entrySet().stream()
@ -314,13 +329,13 @@ public abstract class AbstractBufferedRateExecutor<T extends AsyncTask, F extend
try {
return entityService.fetchEntityName(TenantId.SYS_TENANT_ID, tenantId).orElse(defaultName);
} catch (Exception e) {
log.error("[{}][{}] Failed to get tenant name", getBufferName(), tenantId, e);
log.error("[{}][{}] Failed to get tenant name", bufferName, tenantId, e);
return defaultName;
}
});
log.info("[{}][{}][{}] Rate limited requests: {}", getBufferName(), tenantId, name, rateLimitedRequests);
log.info("[{}][{}][{}] Rate limited requests: {}", bufferName, tenantId, name, rateLimitedRequests);
} else {
log.info("[{}][{}] Rate limited requests: {}", getBufferName(), tenantId, rateLimitedRequests);
log.info("[{}][{}] Rate limited requests: {}", bufferName, tenantId, rateLimitedRequests);
}
});
}

40
dao/src/main/java/org/thingsboard/server/dao/util/BufferedRateExecutorType.java

@ -0,0 +1,40 @@
/**
* Copyright © 2016-2025 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.util;
import lombok.Getter;
import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.limit.LimitedApi;
@Getter
public enum BufferedRateExecutorType {
READ(LimitedApi.CASSANDRA_READ_QUERIES_CORE, LimitedApi.CASSANDRA_READ_QUERIES_RULE_ENGINE, LimitedApi.CASSANDRA_READ_QUERIES_MONOLITH),
WRITE(LimitedApi.CASSANDRA_WRITE_QUERIES_CORE, LimitedApi.CASSANDRA_WRITE_QUERIES_RULE_ENGINE, LimitedApi.CASSANDRA_WRITE_QUERIES_MONOLITH);
private final LimitedApi coreLimitedApi;
private final LimitedApi ruleEngineLimitedApi;
private final LimitedApi monolithLimitedApi;
private final String displayName = StringUtils.capitalize(name().toLowerCase());
BufferedRateExecutorType(LimitedApi coreLimitedApi, LimitedApi ruleEngineLimitedApi, LimitedApi monolithLimitedApi) {
this.coreLimitedApi = coreLimitedApi;
this.ruleEngineLimitedApi = ruleEngineLimitedApi;
this.monolithLimitedApi = monolithLimitedApi;
}
}

5
pom.xml

@ -964,6 +964,11 @@
<artifactId>cluster-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.thingsboard.common</groupId>
<artifactId>discovery-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.thingsboard.rule-engine</groupId>
<artifactId>rule-engine-api</artifactId>

20
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.html

@ -693,11 +693,19 @@
</tb-rate-limits>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<tb-rate-limits class="flex-1" formControlName="wsUpdatesPerSessionRateLimit"
[type]="rateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT">
<tb-rate-limits class="flex-1" formControlName="cassandraWriteQueryTenantCoreRateLimits"
[type]="rateLimitsType.CASSANDRA_WRITE_QUERY_TENANT_CORE_RATE_LIMITS">
</tb-rate-limits>
<tb-rate-limits class="flex-1" formControlName="cassandraReadQueryTenantCoreRateLimits"
[type]="rateLimitsType.CASSANDRA_READ_QUERY_TENANT_CORE_RATE_LIMITS">
</tb-rate-limits>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<tb-rate-limits class="flex-1" formControlName="cassandraWriteQueryTenantRuleEngineRateLimits"
[type]="rateLimitsType.CASSANDRA_WRITE_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS">
</tb-rate-limits>
<tb-rate-limits class="flex-1" formControlName="cassandraQueryTenantRateLimitsConfiguration"
[type]="rateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION">
<tb-rate-limits class="flex-1" formControlName="cassandraReadQueryTenantRuleEngineRateLimits"
[type]="rateLimitsType.CASSANDRA_READ_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS">
</tb-rate-limits>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
@ -725,8 +733,8 @@
</tb-rate-limits>
</div>
<div class="flex flex-1 flex-row xs:flex-col gt-xs:gap-4">
<tb-rate-limits class="flex-1" formControlName="calculatedFieldDebugEventsRateLimit"
[type]="rateLimitsType.CALCULATED_FIELD_DEBUG_EVENT_RATE_LIMIT">
<tb-rate-limits class="flex-1" formControlName="wsUpdatesPerSessionRateLimit"
[type]="rateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT">
</tb-rate-limits>
<div class="flex-1"></div>
</div>

5
ui-ngx/src/app/modules/home/components/profile/tenant/default-tenant-profile-configuration.component.ts

@ -114,7 +114,10 @@ export class DefaultTenantProfileConfigurationComponent implements ControlValueA
maxWsSubscriptionsPerRegularUser: [null, [Validators.min(0)]],
maxWsSubscriptionsPerPublicUser: [null, [Validators.min(0)]],
wsUpdatesPerSessionRateLimit: [null, []],
cassandraQueryTenantRateLimitsConfiguration: [null, []],
cassandraWriteQueryTenantCoreRateLimits: [null, []],
cassandraReadQueryTenantCoreRateLimits: [null, []],
cassandraWriteQueryTenantRuleEngineRateLimits: [null, []],
cassandraReadQueryTenantRuleEngineRateLimits: [null, []],
edgeEventRateLimits: [null, []],
edgeEventRateLimitsPerEdge: [null, []],
edgeUplinkMessagesRateLimits: [null, []],

15
ui-ngx/src/app/modules/home/components/profile/tenant/rate-limits/rate-limits.models.ts

@ -37,7 +37,10 @@ export enum RateLimitsType {
TENANT_SERVER_REST_LIMITS_CONFIGURATION = 'TENANT_SERVER_REST_LIMITS_CONFIGURATION',
CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION = 'CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION',
WS_UPDATE_PER_SESSION_RATE_LIMIT = 'WS_UPDATE_PER_SESSION_RATE_LIMIT',
CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION = 'CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION',
CASSANDRA_WRITE_QUERY_TENANT_CORE_RATE_LIMITS = 'CASSANDRA_WRITE_QUERY_TENANT_CORE_RATE_LIMITS',
CASSANDRA_READ_QUERY_TENANT_CORE_RATE_LIMITS = 'CASSANDRA_READ_QUERY_TENANT_CORE_RATE_LIMITS',
CASSANDRA_WRITE_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS = 'CASSANDRA_WRITE_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS',
CASSANDRA_READ_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS = 'CASSANDRA_READ_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS',
TENANT_ENTITY_EXPORT_RATE_LIMIT = 'TENANT_ENTITY_EXPORT_RATE_LIMIT',
TENANT_ENTITY_IMPORT_RATE_LIMIT = 'TENANT_ENTITY_IMPORT_RATE_LIMIT',
TENANT_NOTIFICATION_REQUEST_RATE_LIMIT = 'TENANT_NOTIFICATION_REQUEST_RATE_LIMIT',
@ -66,7 +69,10 @@ export const rateLimitsLabelTranslationMap = new Map<RateLimitsType, string>(
[RateLimitsType.TENANT_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rest-requests-for-tenant'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.customer-rest-limits'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.ws-limit-updates-per-session'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.cassandra-tenant-limits-configuration'],
[RateLimitsType.CASSANDRA_WRITE_QUERY_TENANT_CORE_RATE_LIMITS, 'tenant-profile.cassandra-write-tenant-core-limits-configuration'],
[RateLimitsType.CASSANDRA_READ_QUERY_TENANT_CORE_RATE_LIMITS, 'tenant-profile.cassandra-read-tenant-core-limits-configuration'],
[RateLimitsType.CASSANDRA_WRITE_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS, 'tenant-profile.cassandra-write-tenant-rule-engine-limits-configuration'],
[RateLimitsType.CASSANDRA_READ_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS, 'tenant-profile.cassandra-read-tenant-rule-engine-limits-configuration'],
[RateLimitsType.TENANT_ENTITY_EXPORT_RATE_LIMIT, 'tenant-profile.tenant-entity-export-rate-limit'],
[RateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT, 'tenant-profile.tenant-entity-import-rate-limit'],
[RateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT, 'tenant-profile.tenant-notification-request-rate-limit'],
@ -96,7 +102,10 @@ export const rateLimitsDialogTitleTranslationMap = new Map<RateLimitsType, strin
[RateLimitsType.GATEWAY_DEVICE_TELEMETRY_DATA_POINTS, 'tenant-profile.rate-limits.edit-transport-gateway-device-telemetry-data-points-title'],
[RateLimitsType.CUSTOMER_SERVER_REST_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-customer-rest-limits-title'],
[RateLimitsType.WS_UPDATE_PER_SESSION_RATE_LIMIT, 'tenant-profile.rate-limits.edit-ws-limit-updates-per-session-title'],
[RateLimitsType.CASSANDRA_QUERY_TENANT_RATE_LIMITS_CONFIGURATION, 'tenant-profile.rate-limits.edit-cassandra-tenant-limits-configuration-title'],
[RateLimitsType.CASSANDRA_WRITE_QUERY_TENANT_CORE_RATE_LIMITS, 'tenant-profile.rate-limits.edit-cassandra-write-tenant-core-limits-configuration'],
[RateLimitsType.CASSANDRA_READ_QUERY_TENANT_CORE_RATE_LIMITS, 'tenant-profile.rate-limits.edit-cassandra-read-tenant-core-limits-configuration'],
[RateLimitsType.CASSANDRA_WRITE_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS, 'tenant-profile.rate-limits.edit-cassandra-write-tenant-rule-engine-limits-configuration'],
[RateLimitsType.CASSANDRA_READ_QUERY_TENANT_RULE_ENGINE_RATE_LIMITS, 'tenant-profile.rate-limits.edit-cassandra-read-tenant-rule-engine-limits-configuration'],
[RateLimitsType.TENANT_ENTITY_EXPORT_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-entity-export-rate-limit-title'],
[RateLimitsType.TENANT_ENTITY_IMPORT_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-entity-import-rate-limit-title'],
[RateLimitsType.TENANT_NOTIFICATION_REQUEST_RATE_LIMIT, 'tenant-profile.rate-limits.edit-tenant-notification-request-rate-limit-title'],

10
ui-ngx/src/app/shared/models/tenant.model.ts

@ -83,7 +83,10 @@ export interface DefaultTenantProfileConfiguration {
maxWsSubscriptionsPerPublicUser: number;
wsUpdatesPerSessionRateLimit: string;
cassandraQueryTenantRateLimitsConfiguration: string;
cassandraWriteQueryTenantCoreRateLimits: string;
cassandraReadQueryTenantCoreRateLimits: string;
cassandraWriteQueryTenantRuleEngineRateLimits: string;
cassandraReadQueryTenantRuleEngineRateLimits: string;
edgeEventRateLimits?: string;
edgeEventRateLimitsPerEdge?: string;
@ -150,7 +153,10 @@ export function createTenantProfileConfiguration(type: TenantProfileType): Tenan
maxWsSubscriptionsPerRegularUser: 0,
maxWsSubscriptionsPerPublicUser: 0,
wsUpdatesPerSessionRateLimit: '',
cassandraQueryTenantRateLimitsConfiguration: '',
cassandraWriteQueryTenantCoreRateLimits: '',
cassandraReadQueryTenantCoreRateLimits: '',
cassandraWriteQueryTenantRuleEngineRateLimits: '',
cassandraReadQueryTenantRuleEngineRateLimits: '',
defaultStorageTtlDays: 0,
alarmsTtlDays: 0,
rpcTtlDays: 0,

10
ui-ngx/src/assets/locale/locale.constant-en_US.json

@ -5668,7 +5668,10 @@
"too-small-value-zero": "The value must be bigger than 0",
"too-small-value-one": "The value must be bigger than 1",
"queue-size-is-limited-by-system-configuration": "The size of the queue is also limited by the system configuration.",
"cassandra-tenant-limits-configuration": "Cassandra query for tenant",
"cassandra-write-tenant-core-limits-configuration": "Rest API Cassandra write queries",
"cassandra-read-tenant-core-limits-configuration": "Rest API and WS telemetry Cassandra read queries",
"cassandra-write-tenant-rule-engine-limits-configuration": "Rule Engine telemetry Cassandra write queries",
"cassandra-read-tenant-rule-engine-limits-configuration": "Rule Engine telemetry Cassandra read queries",
"ws-limit-max-sessions-per-tenant": "Sessions per tenant maximum number",
"ws-limit-max-sessions-per-customer": "Sessions per customer maximum number",
"ws-limit-max-sessions-per-regular-user": "Sessions per regular user maximum number",
@ -5701,7 +5704,10 @@
"edit-tenant-rest-limits-title": "Edit REST requests for tenant rate limits",
"edit-customer-rest-limits-title": "Edit REST requests for customer rate limits",
"edit-ws-limit-updates-per-session-title": "Edit WS updates per session rate limits",
"edit-cassandra-tenant-limits-configuration-title": "Edit Cassandra query for tenant rate limits",
"edit-cassandra-write-tenant-core-limits-configuration": "Edit Rest API Cassandra write queries",
"edit-cassandra-read-tenant-core-limits-configuration": "Edit Rest API and WS telemetry Cassandra read queries",
"edit-cassandra-write-tenant-rule-engine-limits-configuration": "Edit Rule Engine telemetry Cassandra write queries",
"edit-cassandra-read-tenant-rule-engine-limits-configuration": "Edit Rule Engine telemetry Cassandra read queries",
"edit-tenant-entity-export-rate-limit-title": "Edit entity version creation rate limits",
"edit-tenant-entity-import-rate-limit-title": "Edit entity version load rate limits",
"edit-tenant-notification-request-rate-limit-title": "Edit notification requests rate limits",

Loading…
Cancel
Save