diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index f945cc30b7..1dafef871a 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -70,6 +70,7 @@ import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.EntityViewId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.id.RpcId; +import org.thingsboard.server.common.data.id.QueueId; import org.thingsboard.server.common.data.id.RuleChainId; import org.thingsboard.server.common.data.id.RuleNodeId; import org.thingsboard.server.common.data.id.TbResourceId; @@ -86,6 +87,7 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.rpc.Rpc; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.rule.RuleNode; @@ -108,6 +110,7 @@ import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.oauth2.OAuth2ConfigTemplateService; import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.relation.RelationService; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -271,6 +274,9 @@ public abstract class BaseController { @Autowired protected EntityActionService entityActionService; + @Autowired + protected QueueService queueService; + @Value("${server.log_controller_error_stack_trace}") @Getter private boolean logControllerErrorStackTrace; @@ -516,6 +522,9 @@ public abstract class BaseController { case OTA_PACKAGE: checkOtaPackageId(new OtaPackageId(entityId.getId()), operation); return; + case QUEUE: + checkQueueId(new QueueId(entityId.getId()), operation); + return; default: throw new IllegalArgumentException("Unsupported entity type: " + entityId.getEntityType()); } @@ -807,7 +816,14 @@ public abstract class BaseController { } } - @SuppressWarnings("unchecked") + protected Queue checkQueueId(QueueId queueId, Operation operation) throws ThingsboardException { + validateId(queueId, "Incorrect queueId " + queueId); + Queue queue = queueService.findQueueById(getCurrentUser().getTenantId(), queueId); + checkNotNull(queue); + accessControlService.checkPermission(getCurrentUser(), Resource.QUEUE, operation, queueId, queue); + return queue; + } + protected I emptyId(EntityType entityType) { return (I) EntityIdFactory.getByTypeAndUuid(entityType, ModelConstants.NULL_UUID); } diff --git a/application/src/main/java/org/thingsboard/server/controller/QueueController.java b/application/src/main/java/org/thingsboard/server/controller/QueueController.java index 33cf7b5a0f..fef6fb9027 100644 --- a/application/src/main/java/org/thingsboard/server/controller/QueueController.java +++ b/application/src/main/java/org/thingsboard/server/controller/QueueController.java @@ -20,17 +20,26 @@ import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import org.springframework.http.MediaType; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; import org.thingsboard.server.common.msg.queue.ServiceType; -import org.thingsboard.server.queue.QueueService; +import org.thingsboard.server.queue.TbQueueService; import org.thingsboard.server.queue.util.TbCoreComponent; +import org.thingsboard.server.service.security.permission.Operation; +import org.thingsboard.server.service.security.permission.Resource; import java.util.Set; +import java.util.UUID; import static org.thingsboard.server.controller.ControllerConstants.QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES; import static org.thingsboard.server.controller.ControllerConstants.QUEUE_SERVICE_TYPE_DESCRIPTION; @@ -42,7 +51,7 @@ import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHO @RequiredArgsConstructor public class QueueController extends BaseController { - private final QueueService queueService; + private final TbQueueService tbQueueService; @ApiOperation(value = "Get queue names (getTenantQueuesByServiceType)", notes = "Returns a set of unique queue names based on service type. " + TENANT_AUTHORITY_PARAGRAPH) @@ -53,7 +62,86 @@ public class QueueController extends BaseController { @RequestParam String serviceType) throws ThingsboardException { checkParameter("serviceType", serviceType); try { - return queueService.getQueuesByServiceType(ServiceType.valueOf(serviceType)); + //TODO: replace for using new QueueService + return tbQueueService.getQueuesByServiceType(ServiceType.valueOf(serviceType)); + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/queues", params = {"serviceType", "pageSize", "page"}, method = RequestMethod.GET) + @ResponseBody + public PageData getTenantQueuesByServiceType(@RequestParam String serviceType, + @RequestParam int pageSize, + @RequestParam int page, + @RequestParam(required = false) String textSearch, + @RequestParam(required = false) String sortProperty, + @RequestParam(required = false) String sortOrder) throws ThingsboardException { + checkParameter("serviceType", serviceType); + try { + PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder); + ServiceType type = ServiceType.valueOf(serviceType); + switch (type) { + case TB_RULE_ENGINE: + return queueService.findQueuesByTenantId(getTenantId(), pageLink); + default: + return new PageData<>(); + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/queues/{queueId}", method = RequestMethod.GET) + @ResponseBody + public Queue getQueueById(@PathVariable("queueId") String queueIdStr) throws ThingsboardException { + checkParameter("queueId", queueIdStr); + try { + QueueId queueId = new QueueId(UUID.fromString(queueIdStr)); + return checkNotNull(queueService.findQueueById(getTenantId(), queueId)); + } catch ( + Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/queues", params = {"serviceType"}, method = RequestMethod.POST) + @ResponseBody + public Queue saveQueue(@RequestBody Queue queue, + @RequestParam String serviceType) throws ThingsboardException { + checkParameter("serviceType", serviceType); + try { + queue.setTenantId(getCurrentUser().getTenantId()); + + checkEntity(queue.getId(), queue, Resource.QUEUE); + + ServiceType type = ServiceType.valueOf(serviceType); + switch (type) { + case TB_RULE_ENGINE: + queue.setTenantId(getTenantId()); + Queue savedQueue = queueService.saveQueue(queue); + checkNotNull(savedQueue); + return savedQueue; + default: + return null; + } + } catch (Exception e) { + throw handleException(e); + } + } + + @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") + @RequestMapping(value = "/tenant/queues/{queueId}", method = RequestMethod.DELETE) + @ResponseBody + public void deleteQueue(@PathVariable("queueId") String queueIdStr) throws ThingsboardException { + checkParameter("queueId", queueIdStr); + try { + QueueId queueId = new QueueId(toUUID(queueIdStr)); + checkQueueId(queueId, Operation.DELETE); + queueService.deleteQueue(getTenantId(), queueId); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java index 1414afe232..f86680f156 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java @@ -40,7 +40,8 @@ public enum Resource { TB_RESOURCE(EntityType.TB_RESOURCE), OTA_PACKAGE(EntityType.OTA_PACKAGE), EDGE(EntityType.EDGE), - RPC(EntityType.RPC); + RPC(EntityType.RPC), + QUEUE(EntityType.QUEUE); private final EntityType entityType; diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java index ca33716692..20e2001c87 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java @@ -39,6 +39,7 @@ public class SysAdminPermissions extends AbstractPermissions { put(Resource.OAUTH2_CONFIGURATION_TEMPLATE, PermissionChecker.allowAllPermissionChecker); put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker); put(Resource.TB_RESOURCE, systemEntityPermissionChecker); + put(Resource.QUEUE, systemEntityPermissionChecker); } private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java index 4ff884f0e1..bcc6251315 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java +++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java @@ -45,6 +45,7 @@ public class TenantAdminPermissions extends AbstractPermissions { put(Resource.OTA_PACKAGE, tenantEntityPermissionChecker); put(Resource.EDGE, tenantEntityPermissionChecker); put(Resource.RPC, tenantEntityPermissionChecker); + put(Resource.QUEUE, tenantEntityPermissionChecker); } public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() { diff --git a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java index d304a567ab..09fe580661 100644 --- a/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/cluster/routing/HashPartitionServiceTest.java @@ -21,13 +21,12 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.context.ApplicationEventPublisher; import org.springframework.test.util.ReflectionTestUtils; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.queue.QueueService; +import org.thingsboard.server.queue.TbQueueService; import org.thingsboard.server.queue.discovery.HashPartitionService; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; @@ -59,7 +58,7 @@ public class HashPartitionServiceTest { private TenantRoutingInfoService routingInfoService; private ApplicationEventPublisher applicationEventPublisher; private TbQueueRuleEngineSettings ruleEngineSettings; - private QueueService queueService; + private TbQueueService queueService; private String hashFunctionName = "sha256"; @@ -70,7 +69,7 @@ public class HashPartitionServiceTest { applicationEventPublisher = mock(ApplicationEventPublisher.class); routingInfoService = mock(TenantRoutingInfoService.class); ruleEngineSettings = mock(TbQueueRuleEngineSettings.class); - queueService = mock(QueueService.class); + queueService = mock(TbQueueService.class); clusterRoutingService = new HashPartitionService(discoveryService, routingInfoService, applicationEventPublisher, diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java index 6a581d522e..37c99dfccb 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueAdmin.java @@ -20,4 +20,6 @@ public interface TbQueueAdmin { void createTopicIfNotExists(String topic); void destroy(); + + default void deleteTopic(String topic) { } } diff --git a/common/cluster-api/src/main/java/org/thingsboard/server/queue/QueueService.java b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java similarity index 96% rename from common/cluster-api/src/main/java/org/thingsboard/server/queue/QueueService.java rename to common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java index 08155caf60..bc7964df7e 100644 --- a/common/cluster-api/src/main/java/org/thingsboard/server/queue/QueueService.java +++ b/common/cluster-api/src/main/java/org/thingsboard/server/queue/TbQueueService.java @@ -19,7 +19,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import java.util.Set; -public interface QueueService { +public interface TbQueueService { Set getQueuesByServiceType(ServiceType serviceType); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java new file mode 100644 index 0000000000..edb51fa2a3 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/queue/QueueService.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2022 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.queue; + +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; + +import java.util.List; + +public interface QueueService { + + Queue saveQueue(Queue queue); + + void deleteQueue(TenantId tenantId, QueueId queueId); + + List findQueuesByTenantId(TenantId tenantId); + + PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink); + + List findAllMainQueues(); + + List findAllQueues(); + + Queue findQueueById(TenantId tenantId, QueueId queueId); + + Queue findQueueByTenantIdAndName(TenantId tenantId, String name); + + Queue createDefaultMainQueue(TenantProfile tenantProfile, TenantId tenantId); + + void deleteQueuesByTenantId(TenantId tenantId); +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 33a5753465..1ef5bb433a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,5 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC; + TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC, QUEUE; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java index 27cc52ba9f..32b6ba88ba 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java @@ -57,7 +57,11 @@ public class TenantProfile extends SearchTextBased implements H @ApiModelProperty(position = 7, value = "If enabled, will push all messages related to this tenant and processed by the rule engine into separate queue. " + "Useful for complex microservices deployments, to isolate processing of the data for specific tenants", example = "true") private boolean isolatedTbRuleEngine; - @ApiModelProperty(position = 8, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") + @ApiModelProperty(position = 8, value = "Max amount of queues configurable by isolated tenant.", example = "5") + private Integer maxNumberOfQueues; + @ApiModelProperty(position = 9, value = "Max amount of partitions per queue configurable by isolated tenant.", example = "10") + private Integer maxNumberOfPartitionsPerQueue; + @ApiModelProperty(position = 10, value = "Complex JSON object that contains profile settings: max devices, max assets, rate limits, etc.") private transient TenantProfileData profileData; @JsonIgnore private byte[] profileDataBytes; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index ad91dc8d88..8bd13b7c3f 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -77,6 +77,8 @@ public class EntityIdFactory { return new EdgeId(uuid); case RPC: return new RpcId(uuid); + case QUEUE: + return new QueueId(uuid); } throw new IllegalArgumentException("EntityType " + type + " is not supported!"); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java new file mode 100644 index 0000000000..37a4136d46 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/QueueId.java @@ -0,0 +1,43 @@ +/** + * Copyright © 2016-2022 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.id; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.thingsboard.server.common.data.EntityType; + +import java.util.UUID; + +public class QueueId extends UUIDBased implements EntityId { + + private static final long serialVersionUID = 1L; + + @JsonCreator + public QueueId(@JsonProperty("id") UUID id) { + super(id); + } + + public static QueueId fromString(String queueId) { + return new QueueId(UUID.fromString(queueId)); + } + + @JsonIgnore + @Override + public EntityType getEntityType() { + return EntityType.QUEUE; + } +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java new file mode 100644 index 0000000000..8a315f14ad --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategy.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2022 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.queue; + +import lombok.Data; + +@Data +public class ProcessingStrategy { + private ProcessingStrategyType type; + private int retries; + private double failurePercentage; + private long pauseBetweenRetries; + private long maxPauseBetweenRetries; +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java new file mode 100644 index 0000000000..7a367a2141 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/ProcessingStrategyType.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2022 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. + */ + +/** + * Copyright © 2016-2020 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.queue; + +public enum ProcessingStrategyType { + SKIP_ALL_FAILURES, SKIP_ALL_FAILURES_AND_TIMED_OUT, RETRY_ALL, RETRY_FAILED, RETRY_TIMED_OUT, RETRY_FAILED_AND_TIMED_OUT +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java new file mode 100644 index 0000000000..84776c88f4 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/Queue.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2022 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.queue; + +import lombok.Data; +import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; + +@Data +public class Queue extends BaseData implements HasName, HasTenantId { + private TenantId tenantId; + private String name; + private String topic; + private int pollInterval; + private int partitions; + private long packProcessingTimeout; + private SubmitStrategy submitStrategy; + private ProcessingStrategy processingStrategy; + + public Queue() { + } + + public Queue(QueueId id) { + super(id); + } +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java new file mode 100644 index 0000000000..972433b038 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategy.java @@ -0,0 +1,24 @@ +/** + * Copyright © 2016-2022 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.queue; + +import lombok.Data; + +@Data +public class SubmitStrategy { + private SubmitStrategyType type; + private int batchSize; +} \ No newline at end of file diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategyType.java b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategyType.java new file mode 100644 index 0000000000..cb6e2989e5 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/queue/SubmitStrategyType.java @@ -0,0 +1,20 @@ +/** + * Copyright © 2016-2022 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.queue; + +public enum SubmitStrategyType { + BURST, BATCH, SEQUENTIAL_BY_ORIGINATOR, SEQUENTIAL_BY_TENANT, SEQUENTIAL +} \ No newline at end of file diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/DefaultQueueService.java b/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java similarity index 97% rename from common/queue/src/main/java/org/thingsboard/server/queue/DefaultQueueService.java rename to common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java index 427e2c1f1a..5a0e540ac4 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/DefaultQueueService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/DefaultTbQueueService.java @@ -31,7 +31,7 @@ import java.util.stream.Collectors; @Service @RequiredArgsConstructor -public class DefaultQueueService implements QueueService { +public class DefaultTbQueueService implements TbQueueService { private final TbQueueRuleEngineSettings ruleEngineSettings; private Set ruleEngineQueues; diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java index 7478d21816..4882a96f72 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java @@ -16,13 +16,11 @@ package org.thingsboard.server.queue.discovery; import com.google.common.hash.HashFunction; -import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.msg.queue.ServiceQueue; @@ -31,7 +29,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; -import org.thingsboard.server.queue.QueueService; +import org.thingsboard.server.queue.TbQueueService; import org.thingsboard.server.queue.discovery.event.ClusterTopologyChangeEvent; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import org.thingsboard.server.queue.discovery.event.ServiceListChangedEvent; @@ -66,7 +64,7 @@ public class HashPartitionService implements PartitionService { private final TbServiceInfoProvider serviceInfoProvider; private final TenantRoutingInfoService tenantRoutingInfoService; private final TbQueueRuleEngineSettings tbQueueRuleEngineSettings; - private final QueueService queueService; + private final TbQueueService queueService; private final ConcurrentMap partitionTopics = new ConcurrentHashMap<>(); private final ConcurrentMap partitionSizes = new ConcurrentHashMap<>(); private final ConcurrentMap tenantRoutingInfoMap = new ConcurrentHashMap<>(); @@ -85,7 +83,7 @@ public class HashPartitionService implements PartitionService { TenantRoutingInfoService tenantRoutingInfoService, ApplicationEventPublisher applicationEventPublisher, TbQueueRuleEngineSettings tbQueueRuleEngineSettings, - QueueService queueService) { + TbQueueService queueService) { this.serviceInfoProvider = serviceInfoProvider; this.tenantRoutingInfoService = tenantRoutingInfoService; this.applicationEventPublisher = applicationEventPublisher; diff --git a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java index 9a272b4739..35d4dc2899 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/device/DeviceProfileServiceImpl.java @@ -82,7 +82,7 @@ import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.dao.service.PaginatedRemover; import org.thingsboard.server.dao.service.Validator; import org.thingsboard.server.dao.tenant.TenantDao; -import org.thingsboard.server.queue.QueueService; +import org.thingsboard.server.queue.TbQueueService; import java.util.Arrays; import java.util.Collections; @@ -115,7 +115,7 @@ public class DeviceProfileServiceImpl extends AbstractEntityService implements D } @Autowired(required = false) - private QueueService queueService; + private TbQueueService queueService; @Autowired private DeviceProfileDao deviceProfileDao; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index 8f692d5a12..cb209c2ee7 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -128,6 +128,8 @@ public class ModelConstants { public static final String TENANT_PROFILE_IS_DEFAULT_PROPERTY = "is_default"; public static final String TENANT_PROFILE_ISOLATED_TB_CORE = "isolated_tb_core"; public static final String TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE = "isolated_tb_rule_engine"; + public static final String TENANT_PROFILE_MAX_NUMBER_OF_QUEUES = "max_number_of_queues"; + public static final String TENANT_PROFILE_MAX_NUMBER_OF_PARTITIONS_PER_QUEUE = "max_number_of_partitions_per_queue"; /** * Cassandra customer constants. @@ -581,6 +583,20 @@ public class ModelConstants { public static final String DOUBLE_VALUE_COLUMN = "dbl_v"; public static final String JSON_VALUE_COLUMN = "json_v"; + /** + * Queue constants. + */ + + public static final String QUEUE_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; + public static final String QUEUE_NAME_PROPERTY = "name"; + public static final String QUEUE_TOPIC_PROPERTY = "topic"; + public static final String QUEUE_POLL_INTERVAL_PROPERTY = "poll_interval"; + public static final String QUEUE_PARTITIONS_PROPERTY = "partitions"; + public static final String QUEUE_PACK_PROCESSING_TIMEOUT_PROPERTY = "pack_processing_timeout"; + public static final String QUEUE_SUBMIT_STRATEGY_PROPERTY = "submit_strategy"; + public static final String QUEUE_PROCESSING_STRATEGY_PROPERTY = "processing_strategy"; + public static final String QUEUE_COLUMN_FAMILY_NAME = "queue"; + protected static final String[] NONE_AGGREGATION_COLUMNS = new String[]{LONG_VALUE_COLUMN, DOUBLE_VALUE_COLUMN, BOOLEAN_VALUE_COLUMN, STRING_VALUE_COLUMN, JSON_VALUE_COLUMN, KEY_COLUMN, TS_COLUMN}; protected static final String[] COUNT_AGGREGATION_COLUMNS = new String[]{count(LONG_VALUE_COLUMN), count(DOUBLE_VALUE_COLUMN), count(BOOLEAN_VALUE_COLUMN), count(STRING_VALUE_COLUMN), count(JSON_VALUE_COLUMN)}; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java new file mode 100644 index 0000000000..3791ab4ac3 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/QueueEntity.java @@ -0,0 +1,105 @@ +/** + * Copyright © 2016-2022 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.model.sql; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.BaseSqlEntity; +import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.dao.util.mapping.JsonStringType; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Table; +import java.util.UUID; + +@Data +@EqualsAndHashCode(callSuper = true) +@Entity +@TypeDef(name = "json", typeClass = JsonStringType.class) +@Table(name = ModelConstants.QUEUE_COLUMN_FAMILY_NAME) +public class QueueEntity extends BaseSqlEntity { + + private static final ObjectMapper mapper = new ObjectMapper(); + + @Column(name = ModelConstants.QUEUE_TENANT_ID_PROPERTY) + private UUID tenantId; + + @Column(name = ModelConstants.QUEUE_NAME_PROPERTY) + private String name; + + @Column(name = ModelConstants.QUEUE_TOPIC_PROPERTY) + private String topic; + @Column(name = ModelConstants.QUEUE_POLL_INTERVAL_PROPERTY) + private int pollInterval; + + @Column(name = ModelConstants.QUEUE_PARTITIONS_PROPERTY) + private int partitions; + + @Column(name = ModelConstants.QUEUE_PACK_PROCESSING_TIMEOUT_PROPERTY) + private long packProcessingTimeout; + + @Type(type = "json") + @Column(name = ModelConstants.QUEUE_SUBMIT_STRATEGY_PROPERTY) + private JsonNode submitStrategy; + + @Type(type = "json") + @Column(name = ModelConstants.QUEUE_PROCESSING_STRATEGY_PROPERTY) + private JsonNode processingStrategy; + + public QueueEntity() { + } + + public QueueEntity(Queue queue) { + if (queue.getId() != null) { + this.setId(queue.getId().getId()); + } + this.createdTime = queue.getCreatedTime(); + this.tenantId = DaoUtil.getId(queue.getTenantId()); + this.name = queue.getName(); + this.topic = queue.getTopic(); + this.pollInterval = queue.getPollInterval(); + this.partitions = queue.getPartitions(); + this.packProcessingTimeout = queue.getPackProcessingTimeout(); + this.submitStrategy = mapper.valueToTree(queue.getSubmitStrategy()); + this.processingStrategy = mapper.valueToTree(queue.getProcessingStrategy()); + } + + @Override + public Queue toData() { + Queue queue = new Queue(new QueueId(getUuid())); + queue.setCreatedTime(createdTime); + queue.setTenantId(new TenantId(tenantId)); + queue.setName(name); + queue.setTopic(topic); + queue.setPollInterval(pollInterval); + queue.setPartitions(partitions); + queue.setPackProcessingTimeout(packProcessingTimeout); + queue.setSubmitStrategy(mapper.convertValue(this.submitStrategy, SubmitStrategy.class)); + queue.setProcessingStrategy(mapper.convertValue(this.processingStrategy, ProcessingStrategy.class)); + return queue; + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java index 8613c7ced5..2b62588c76 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/TenantProfileEntity.java @@ -21,13 +21,13 @@ import lombok.Data; import lombok.EqualsAndHashCode; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.SearchTextEntity; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.dao.util.mapping.JsonBinaryType; import javax.persistence.Column; @@ -59,6 +59,12 @@ public final class TenantProfileEntity extends BaseSqlEntity impl @Column(name = ModelConstants.TENANT_PROFILE_ISOLATED_TB_RULE_ENGINE) private boolean isolatedTbRuleEngine; + @Column(name = ModelConstants.TENANT_PROFILE_MAX_NUMBER_OF_QUEUES) + private Integer maxNumberOfQueues; + + @Column(name = ModelConstants.TENANT_PROFILE_MAX_NUMBER_OF_PARTITIONS_PER_QUEUE) + private Integer maxNumberOfPartitionsPerQueue; + @Type(type = "jsonb") @Column(name = ModelConstants.TENANT_PROFILE_PROFILE_DATA_PROPERTY, columnDefinition = "jsonb") private JsonNode profileData; @@ -77,6 +83,8 @@ public final class TenantProfileEntity extends BaseSqlEntity impl this.isDefault = tenantProfile.isDefault(); this.isolatedTbCore = tenantProfile.isIsolatedTbCore(); this.isolatedTbRuleEngine = tenantProfile.isIsolatedTbRuleEngine(); + this.maxNumberOfPartitionsPerQueue = tenantProfile.getMaxNumberOfPartitionsPerQueue(); + this.maxNumberOfQueues = tenantProfile.getMaxNumberOfQueues(); this.profileData = JacksonUtil.convertValue(tenantProfile.getProfileData(), ObjectNode.class); } @@ -103,6 +111,8 @@ public final class TenantProfileEntity extends BaseSqlEntity impl tenantProfile.setDefault(isDefault); tenantProfile.setIsolatedTbCore(isolatedTbCore); tenantProfile.setIsolatedTbRuleEngine(isolatedTbRuleEngine); + tenantProfile.setMaxNumberOfPartitionsPerQueue(maxNumberOfPartitionsPerQueue); + tenantProfile.setMaxNumberOfQueues(maxNumberOfQueues); tenantProfile.setProfileData(JacksonUtil.convertValue(profileData, TenantProfileData.class)); return tenantProfile; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java new file mode 100644 index 0000000000..cad7f9cb48 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/BaseQueueService.java @@ -0,0 +1,333 @@ +/** + * Copyright © 2016-2022 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.queue; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.id.QueueId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.ProcessingStrategy; +import org.thingsboard.server.common.data.queue.ProcessingStrategyType; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.common.data.queue.SubmitStrategy; +import org.thingsboard.server.common.data.queue.SubmitStrategyType; +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.service.PaginatedRemover; +import org.thingsboard.server.dao.service.Validator; +import org.thingsboard.server.dao.tenant.TbTenantProfileCache; +import org.thingsboard.server.dao.tenant.TenantDao; +import org.thingsboard.server.queue.TbQueueAdmin; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class BaseQueueService extends AbstractEntityService implements QueueService { + + @Autowired + private QueueDao queueDao; + + @Autowired + private TenantDao tenantDao; + + @Autowired + private TbTenantProfileCache tenantProfileCache; + + @Autowired(required = false) + private TbQueueAdmin tbQueueAdmin; + +// @Autowired(required = false) +// private TbQueueClusterService queueClusterService; + +// @Autowired +// private QueueStatsService queueStatsService; + + @Override + @Transactional + public Queue saveQueue(Queue queue) { + log.trace("Executing createOrUpdateQueue [{}]", queue); + queueValidator.validate(queue, Queue::getTenantId); + Queue savedQueue; + if (queue.getId() == null) { + savedQueue = createQueue(queue); + } else { + savedQueue = updateQueue(queue); + } + +// if (queueClusterService != null) { +// queueClusterService.onQueueChange(savedQueue, null); +// } + return savedQueue; + } + + private Queue createQueue(Queue queue) { + Queue createdQueue = queueDao.save(queue.getTenantId(), queue); + if (tbQueueAdmin != null) { + for (int i = 0; i < queue.getPartitions(); i++) { + tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); + } + } + return createdQueue; + } + + private Queue updateQueue(Queue queue) { + Queue oldQueue = queueDao.findById(queue.getTenantId(), queue.getUuidId()); + Queue updatedQueue = queueDao.save(queue.getTenantId(), queue); + + int oldPartitions = oldQueue.getPartitions(); + int currentPartitions = queue.getPartitions(); + + //TODO: 3.2 remove if partitions can't be deleted. +// if (currentPartitions != oldPartitions && tbQueueAdmin != null) { +// queueClusterService.onQueueDelete(queue, null); +// if (currentPartitions > oldPartitions) { +// log.info("Added [{}] new partitions to [{}] queue", currentPartitions - oldPartitions, queue.getName()); +// for (int i = oldPartitions; i < currentPartitions; i++) { +// tbQueueAdmin.createTopicIfNotExists(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); +// } +// } else { +// log.info("Removed [{}] partitions from [{}] queue", oldPartitions - currentPartitions, queue.getName()); +// for (int i = currentPartitions; i < oldPartitions; i++) { +// tbQueueAdmin.deleteTopic(new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName()); +// } +// } +// } + + return updatedQueue; + } + + @Override + @Transactional + public void deleteQueue(TenantId tenantId, QueueId queueId) { + log.trace("Executing deleteQueue, queueId: [{}]", queueId); + Queue queue = findQueueById(tenantId, queueId); +// if (queueClusterService != null) { +// queueClusterService.onQueueDelete(queue, null); +// } +// queueStatsService.deleteQueueStatsByQueueId(tenantId, queueId); + boolean result = queueDao.removeById(tenantId, queueId.getId()); + if (result && tbQueueAdmin != null) { + for (int i = 0; i < queue.getPartitions(); i++) { + String fullTopicName = new TopicPartitionInfo(queue.getTopic(), queue.getTenantId(), i, false).getFullTopicName(); + log.debug("Deleting queue [{}]", fullTopicName); + try { + tbQueueAdmin.deleteTopic(fullTopicName); + } catch (Exception e) { + log.error("Failed to delete queue [{}]", fullTopicName); + } + } + } + } + + @Override + public List findQueuesByTenantId(TenantId tenantId) { + log.trace("Executing findQueues, tenantId: [{}]", tenantId); + return queueDao.findAllByTenantId(getSystemOrIsolatedTenantId(tenantId)); + } + + @Override + public PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink) { + log.trace("Executing findQueues pageLink [{}]", pageLink); + Validator.validatePageLink(pageLink); + return queueDao.findQueuesByTenantId(getSystemOrIsolatedTenantId(tenantId), pageLink); + } + + @Override + public List findAllMainQueues() { + log.trace("Executing findAllMainQueues"); + return queueDao.findAllMainQueues(); + } + + @Override + public List findAllQueues() { + log.trace("Executing findAllQueues"); + return queueDao.findAllQueues(); + } + + @Override + public Queue findQueueById(TenantId tenantId, QueueId queueId) { + log.trace("Executing findQueueById, queueId: [{}]", queueId); + return queueDao.findById(tenantId, queueId.getId()); + } + + @Override + public Queue findQueueByTenantIdAndName(TenantId tenantId, String queueName) { + log.trace("Executing findQueueByTenantIdAndName, tenantId: [{}] queueName: [{}]", tenantId, queueName); + return queueDao.findQueueByTenantIdAndName(getSystemOrIsolatedTenantId(tenantId), queueName); + } + + @Override + public void deleteQueuesByTenantId(TenantId tenantId) { + Validator.validateId(tenantId, "Incorrect tenant id for delete queues request."); + tenantQueuesRemover.removeEntities(tenantId, tenantId); + } + + @Override + @Transactional + public Queue createDefaultMainQueue(TenantProfile tenantProfile, TenantId tenantId) { + Queue mainQueue = new Queue(); + mainQueue.setTenantId(tenantId); + mainQueue.setName("Main"); + mainQueue.setTopic("tb_rule_engine.main"); + mainQueue.setPollInterval(25); + mainQueue.setPartitions(Math.max(tenantProfile.getMaxNumberOfPartitionsPerQueue(), 1)); + mainQueue.setPackProcessingTimeout(60000); + SubmitStrategy mainQueueSubmitStrategy = new SubmitStrategy(); + mainQueueSubmitStrategy.setType(SubmitStrategyType.BURST); + mainQueueSubmitStrategy.setBatchSize(1000); + mainQueue.setSubmitStrategy(mainQueueSubmitStrategy); + ProcessingStrategy mainQueueProcessingStrategy = new ProcessingStrategy(); + mainQueueProcessingStrategy.setType(ProcessingStrategyType.SKIP_ALL_FAILURES); + mainQueueProcessingStrategy.setRetries(3); + mainQueueProcessingStrategy.setFailurePercentage(0); + mainQueueProcessingStrategy.setPauseBetweenRetries(3); + mainQueueProcessingStrategy.setMaxPauseBetweenRetries(3); + mainQueue.setProcessingStrategy(mainQueueProcessingStrategy); + return saveQueue(mainQueue); + } + + private DataValidator queueValidator = + new DataValidator<>() { + + @Override + protected void validateCreate(TenantId tenantId, Queue queue) { + if (queueDao.findQueueByTenantIdAndTopic(tenantId, queue.getTopic()) != null) { + throw new DataValidationException(String.format("Queue with topic: %s already exists!", queue.getTopic())); + } + if (queueDao.findQueueByTenantIdAndName(tenantId, queue.getName()) != null) { + throw new DataValidationException(String.format("Queue with name: %s already exists!", queue.getName())); + } + } + + @Override + protected void validateUpdate(TenantId tenantId, Queue queue) { + Queue foundQueue = queueDao.findById(tenantId, queue.getUuidId()); + if (queueDao.findById(tenantId, queue.getUuidId()) == null) { + throw new DataValidationException(String.format("Queue with id: %s does not exists!", queue.getId())); + } + if (!foundQueue.getName().equals(queue.getName())) { + throw new DataValidationException("Queue name can't be changed!"); + } + if (!foundQueue.getTopic().equals(queue.getTopic())) { + throw new DataValidationException("Queue topic can't be changed!"); + } + } + + @Override + protected void validateDataImpl(TenantId tenantId, Queue queue) { + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + + if (!tenantProfile.isIsolatedTbRuleEngine()) { + throw new DataValidationException("Tenant should be isolated!"); + } + + if (queue.getId() == null) { + List existingQueues = findQueuesByTenantId(tenantId); + if (existingQueues.size() >= tenantProfile.getMaxNumberOfQueues()) { + throw new DataValidationException("The limit for creating new queue has been exceeded!"); + } + } + + if (queue.getPartitions() > tenantProfile.getMaxNumberOfPartitionsPerQueue()) { + throw new DataValidationException(String.format("Queue partitions can't be more then %d", tenantProfile.getMaxNumberOfPartitionsPerQueue())); + } + } + + if (StringUtils.isEmpty(queue.getName())) { + throw new DataValidationException("Queue name should be specified!"); + } + if (StringUtils.isBlank(queue.getTopic())) { + throw new DataValidationException("Queue topic should be non empty and without spaces!"); + } + if (queue.getPollInterval() < 1) { + throw new DataValidationException("Queue poll interval should be more then 0!"); + } + if (queue.getPartitions() < 1) { + throw new DataValidationException("Queue partitions should be more then 0!"); + } + if (queue.getPackProcessingTimeout() < 1) { + throw new DataValidationException("Queue pack processing timeout should be more then 0!"); + } + + SubmitStrategy submitStrategy = queue.getSubmitStrategy(); + if (submitStrategy == null) { + throw new DataValidationException("Queue submit strategy can't be null!"); + } + if (submitStrategy.getType() == null) { + throw new DataValidationException("Queue submit strategy type can't be null!"); + } + if (submitStrategy.getType() == SubmitStrategyType.BATCH && submitStrategy.getBatchSize() < 1) { + throw new DataValidationException("Queue submit strategy batch size should be more then 0!"); + } + ProcessingStrategy processingStrategy = queue.getProcessingStrategy(); + if (processingStrategy == null) { + throw new DataValidationException("Queue processing strategy can't be null!"); + } + if (processingStrategy.getType() == null) { + throw new DataValidationException("Queue processing strategy type can't be null!"); + } + if (processingStrategy.getRetries() < 0) { + throw new DataValidationException("Queue processing strategy retries can't be less then 0!"); + } + if (processingStrategy.getFailurePercentage() < 0 || processingStrategy.getFailurePercentage() > 100) { + throw new DataValidationException("Queue processing strategy failure percentage should be in a range from 0 to 100!"); + } + if (processingStrategy.getPauseBetweenRetries() < 0) { + throw new DataValidationException("Queue processing strategy pause between retries can't be less then 0!"); + } + if (processingStrategy.getMaxPauseBetweenRetries() < processingStrategy.getPauseBetweenRetries()) { + throw new DataValidationException("Queue processing strategy MAX pause between retries can't be less then pause between retries!"); + } + } + }; + + private PaginatedRemover tenantQueuesRemover = + new PaginatedRemover<>() { + + @Override + protected PageData findEntities(TenantId tenantId, TenantId id, PageLink pageLink) { + return queueDao.findQueuesByTenantId(id, pageLink); + } + + @Override + protected void removeEntity(TenantId tenantId, Queue entity) { + deleteQueue(tenantId, entity.getId()); + } + }; + + private TenantId getSystemOrIsolatedTenantId(TenantId tenantId) { + if (!tenantId.equals(TenantId.SYS_TENANT_ID)) { + TenantProfile tenantProfile = tenantProfileCache.get(tenantId); + if (tenantProfile.isIsolatedTbRuleEngine()) { + return tenantId; + } + } + + return TenantId.SYS_TENANT_ID; + } +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/queue/QueueDao.java b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueDao.java new file mode 100644 index 0000000000..5cc7a5d7ba --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/queue/QueueDao.java @@ -0,0 +1,38 @@ +/** + * Copyright © 2016-2022 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.queue; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.dao.Dao; + +import java.util.List; + +public interface QueueDao extends Dao { + Queue findQueueByTenantIdAndTopic(TenantId tenantId, String topic); + + Queue findQueueByTenantIdAndName(TenantId tenantId, String name); + + List findAllMainQueues(); + + List findAllQueues(); + + List findAllByTenantId(TenantId tenantId); + + PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink); +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java new file mode 100644 index 0000000000..4e544f35cd --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/JpaQueueDao.java @@ -0,0 +1,86 @@ +/** + * Copyright © 2016-2022 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.sql.queue; + +import com.google.common.collect.Lists; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.repository.CrudRepository; +import org.springframework.stereotype.Component; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; +import org.thingsboard.server.common.data.queue.Queue; +import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.model.sql.QueueEntity; +import org.thingsboard.server.dao.queue.QueueDao; +import org.thingsboard.server.dao.sql.JpaAbstractDao; + +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@Slf4j +@Component +public class JpaQueueDao extends JpaAbstractDao implements QueueDao { + + @Autowired + private QueueRepository queueRepository; + + @Override + protected Class getEntityClass() { + return QueueEntity.class; + } + + @Override + protected CrudRepository getCrudRepository() { + return queueRepository; + } + + @Override + public Queue findQueueByTenantIdAndTopic(TenantId tenantId, String topic) { + return DaoUtil.getData(queueRepository.findByTenantIdAndTopic(tenantId.getId(), topic)); + } + + @Override + public Queue findQueueByTenantIdAndName(TenantId tenantId, String name) { + return DaoUtil.getData(queueRepository.findByTenantIdAndName(tenantId.getId(), name)); + } + + @Override + public List findAllByTenantId(TenantId tenantId) { + List entities = queueRepository.findByTenantId(tenantId.getId()); + return DaoUtil.convertDataList(entities); + } + + @Override + public List findAllMainQueues() { + List entities = Lists.newArrayList(queueRepository.findAllByName("Main")); + return DaoUtil.convertDataList(entities); + } + + @Override + public List findAllQueues() { + List entities = Lists.newArrayList(queueRepository.findAll()); + return DaoUtil.convertDataList(entities); + } + + @Override + public PageData findQueuesByTenantId(TenantId tenantId, PageLink pageLink) { + return DaoUtil.toPageData(queueRepository + .findByTenantId(tenantId.getId(), Objects.toString(pageLink.getTextSearch(), ""), DaoUtil.toPageable(pageLink))); + } +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueRepository.java new file mode 100644 index 0000000000..d531072a39 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/queue/QueueRepository.java @@ -0,0 +1,42 @@ +/** + * Copyright © 2016-2022 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.sql.queue; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.query.Param; +import org.thingsboard.server.dao.model.sql.QueueEntity; + +import java.util.List; +import java.util.UUID; + +public interface QueueRepository extends CrudRepository { + QueueEntity findByTenantIdAndTopic(UUID tenantId, String topic); + + QueueEntity findByTenantIdAndName(UUID tenantId, String name); + + List findByTenantId(UUID tenantId); + + @Query("SELECT q FROM QueueEntity q WHERE q.tenantId = :tenantId " + + "AND LOWER(q.name) LIKE LOWER(CONCAT(:textSearch, '%'))") + Page findByTenantId(@Param("tenantId") UUID tenantId, + @Param("textSearch") String textSearch, + Pageable pageable); + + List findAllByName(String name); +} \ No newline at end of file diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 056f5987d8..2ae46df19a 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -37,6 +37,7 @@ import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.ota.OtaPackageService; +import org.thingsboard.server.dao.queue.QueueService; import org.thingsboard.server.dao.resource.ResourceService; import org.thingsboard.server.dao.rpc.RpcService; import org.thingsboard.server.dao.rule.RuleChainService; @@ -104,6 +105,9 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe @Autowired private RpcService rpcService; + @Autowired + private QueueService queueService; + @Override public Tenant findTenantById(TenantId tenantId) { log.trace("Executing findTenantById [{}]", tenantId); @@ -160,6 +164,7 @@ public class TenantServiceImpl extends AbstractEntityService implements TenantSe resourceService.deleteResourcesByTenantId(tenantId); otaPackageService.deleteOtaPackagesByTenantId(tenantId); rpcService.deleteAllRpcByTenantId(tenantId); + queueService.deleteQueuesByTenantId(tenantId); tenantDao.removeById(tenantId, tenantId.getId()); deleteEntityRelations(tenantId, tenantId); } diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index cdae84fe15..5335ce6f5f 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -337,6 +337,8 @@ CREATE TABLE IF NOT EXISTS tenant_profile ( is_default boolean, isolated_tb_core boolean, isolated_tb_rule_engine boolean, + max_number_of_queues int , + max_number_of_partitions_per_queue int, CONSTRAINT tenant_profile_name_unq_key UNIQUE (name) ); @@ -632,6 +634,19 @@ CREATE TABLE IF NOT EXISTS rpc ( status varchar(255) NOT NULL ); +CREATE TABLE IF NOT EXISTS queue( + id uuid NOT NULL CONSTRAINT queue_pkey PRIMARY KEY, + created_time bigint NOT NULL, + tenant_id uuid, + name varchar(255), + topic varchar(255), + poll_interval int, + partitions int, + pack_processing_timeout bigint, + submit_strategy varchar(255), + processing_strategy varchar(255) +); + CREATE OR REPLACE PROCEDURE cleanup_events_by_ttl(IN ttl bigint, IN debug_ttl bigint, INOUT deleted bigint) LANGUAGE plpgsql AS $$ diff --git a/ui-ngx/src/app/app.component.ts b/ui-ngx/src/app/app.component.ts index 9f914dda59..0864e2e8ed 100644 --- a/ui-ngx/src/app/app.component.ts +++ b/ui-ngx/src/app/app.component.ts @@ -78,6 +78,17 @@ export class AppComponent implements OnInit { ) ); + this.matIconRegistry.addSvgIconLiteral( + 'queues-list', + this.domSanitizer.bypassSecurityTrustHtml( + '' + + '' + + '' + + '' + + '' + ) + ); + this.storageService.testLocalStorage(); this.setupTranslate(); diff --git a/ui-ngx/src/app/core/http/queue.service.ts b/ui-ngx/src/app/core/http/queue.service.ts index 5aae5fe561..dc98c15d39 100644 --- a/ui-ngx/src/app/core/http/queue.service.ts +++ b/ui-ngx/src/app/core/http/queue.service.ts @@ -18,7 +18,9 @@ import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { defaultHttpOptionsFromConfig, RequestConfig } from '@core/http/http-utils'; import { Observable } from 'rxjs'; -import { ServiceType } from '@shared/models/queue.models'; +import { QueueInfo, ServiceType } from '@shared/models/queue.models'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; @Injectable({ providedIn: 'root' @@ -29,8 +31,27 @@ export class QueueService { private http: HttpClient ) { } - public getTenantQueuesByServiceType(serviceType: ServiceType, config?: RequestConfig): Observable> { + public getTenantQueuesNamesByServiceType(serviceType: ServiceType, config?: RequestConfig): Observable> { return this.http.get>(`/api/tenant/queues?serviceType=${serviceType}`, defaultHttpOptionsFromConfig(config)); } + + public getQueueById(queueId: string): Observable { + return this.http.get(`/api/tenant/queues/${queueId}`); + } + + public getTenantQueuesByServiceType(pageLink: PageLink, + serviceType: ServiceType, + config?: RequestConfig): Observable> { + return this.http.get>(`/api/tenant/queues${pageLink.toQuery()}&serviceType=${serviceType}`, + defaultHttpOptionsFromConfig(config)); + } + + public saveQueue(queue: QueueInfo, serviceType: ServiceType, config?: RequestConfig): Observable { + return this.http.post(`/api/tenant/queues?serviceType=${serviceType}`, queue, defaultHttpOptionsFromConfig(config)); + } + + public deleteQueue(queueId: string) { + return this.http.delete(`/api/tenant/queues/${queueId}`); + } } diff --git a/ui-ngx/src/app/core/services/menu.service.ts b/ui-ngx/src/app/core/services/menu.service.ts index 4a52fc66f6..215693f318 100644 --- a/ui-ngx/src/app/core/services/menu.service.ts +++ b/ui-ngx/src/app/core/services/menu.service.ts @@ -108,7 +108,7 @@ export class MenuService { name: 'admin.system-settings', type: 'toggle', path: '/settings', - height: '240px', + height: '280px', icon: 'settings', pages: [ { @@ -152,7 +152,14 @@ export class MenuService { type: 'link', path: '/settings/resources-library', icon: 'folder' - } + }, + { + id: guid(), + name: 'admin.queues', + type: 'link', + path: '/settings/queues', + icon: 'swap_calls' + }, ] } ); @@ -220,7 +227,12 @@ export class MenuService { name: 'resource.resources-library', icon: 'folder', path: '/settings/resources-library' - } + }, + { + name: 'admin.queues', + icon: 'swap_calls', + path: '/settings/queues' + }, ] } ); diff --git a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html index e2eb776e5b..03ff1ff921 100644 --- a/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html +++ b/ui-ngx/src/app/modules/home/components/profile/tenant-profile.component.html @@ -67,6 +67,36 @@
{{ 'tenant.isolated-tb-rule-engine' | translate }}
{{'tenant.isolated-tb-rule-engine-details' | translate}}
+ + tenant.max-number-of-queues + + + {{ 'tenant.max-number-of-queues-required' | translate }} + + + {{ 'tenant.max-number-of-queues-min-length' | translate }} + + + + tenant.max-number-of-partitions-per-queue + + + {{ 'tenant.max-number-of-partitions-per-queue-required' | translate }} + + + {{ 'tenant.max-number-of-partitions-per-queue-min-length' | translate }} + + { name: [entity ? entity.name : '', [Validators.required, Validators.maxLength(255)]], isolatedTbCore: [entity ? entity.isolatedTbCore : false, []], isolatedTbRuleEngine: [entity ? entity.isolatedTbRuleEngine : false, []], + maxNumberOfQueues: [entity ? entity.maxNumberOfQueues : 1, [Validators.required, Validators.min(1)]], + maxNumberOfPartitionsPerQueue: [entity ? entity.maxNumberOfPartitionsPerQueue : 1, [Validators.required, Validators.min(1)]], profileData: [entity && !this.isAdd ? entity.profileData : { configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) } as TenantProfileData, []], @@ -74,12 +76,26 @@ export class TenantProfileComponent extends EntityComponent { this.entityForm.patchValue({name: entity.name}); this.entityForm.patchValue({isolatedTbCore: entity.isolatedTbCore}); this.entityForm.patchValue({isolatedTbRuleEngine: entity.isolatedTbRuleEngine}); + this.entityForm.patchValue({maxNumberOfQueues: entity.maxNumberOfQueues}); + this.entityForm.patchValue({maxNumberOfPartitionsPerQueue: entity.maxNumberOfPartitionsPerQueue}); this.entityForm.patchValue({profileData: !this.isAdd ? entity.profileData : { configuration: createTenantProfileConfiguration(TenantProfileType.DEFAULT) } as TenantProfileData}); this.entityForm.patchValue({description: entity.description}); } + showQueueParams(): boolean { + let isolatedTbRuleEngine: boolean = this.entityForm.get('isolatedTbRuleEngine').value; + if (isolatedTbRuleEngine) { + this.entityForm.controls['maxNumberOfQueues'].enable(); + this.entityForm.controls['maxNumberOfPartitionsPerQueue'].enable(); + } else { + this.entityForm.controls['maxNumberOfQueues'].disable(); + this.entityForm.controls['maxNumberOfPartitionsPerQueue'].disable(); + } + return isolatedTbRuleEngine; + } + updateFormState() { if (this.entityForm) { if (this.isEditValue) { @@ -87,6 +103,8 @@ export class TenantProfileComponent extends EntityComponent { if (!this.isAdd) { this.entityForm.get('isolatedTbCore').disable({emitEvent: false}); this.entityForm.get('isolatedTbRuleEngine').disable({emitEvent: false}); + this.entityForm.get('maxNumberOfQueues').disable({emitEvent: false}); + this.entityForm.get('maxNumberOfPartitionsPerQueue').disable({emitEvent: false}); } } else { this.entityForm.disable({emitEvent: false}); diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts index ddccbb4da4..3f8b26f105 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin-routing.module.ts @@ -32,6 +32,7 @@ import { ResourcesLibraryTableConfigResolver } from '@home/pages/admin/resource/ import { EntityDetailsPageComponent } from '@home/components/entity/entity-details-page.component'; import { entityDetailsPageBreadcrumbLabelFunction } from '@home/pages/home-pages.models'; import { BreadCrumbConfig } from '@shared/components/breadcrumb'; +import { QueuesTableConfigResolver } from '@home/pages/admin/queue/queues-table-config.resolver'; @Injectable() export class OAuth2LoginProcessingUrlResolver implements Resolve { @@ -183,6 +184,22 @@ const routes: Routes = [ } } ] + }, + { + path: 'queues', + component: EntitiesTableComponent, + canDeactivate: [ConfirmOnExitGuard], + data: { + auth: [Authority.SYS_ADMIN], + title: 'admin.queues', + breadcrumb: { + label: 'admin.queues', + icon: 'swap_calls' + } + }, + resolve: { + entitiesTableConfig: QueuesTableConfigResolver + } } ] } @@ -193,7 +210,8 @@ const routes: Routes = [ exports: [RouterModule], providers: [ OAuth2LoginProcessingUrlResolver, - ResourcesLibraryTableConfigResolver + ResourcesLibraryTableConfigResolver, + QueuesTableConfigResolver ] }) export class AdminRoutingModule { } diff --git a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts index 326b16f950..6f5e91bd61 100644 --- a/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts +++ b/ui-ngx/src/app/modules/home/pages/admin/admin.module.ts @@ -28,6 +28,7 @@ import { SmsProviderComponent } from '@home/pages/admin/sms-provider.component'; import { SendTestSmsDialogComponent } from '@home/pages/admin/send-test-sms-dialog.component'; import { HomeSettingsComponent } from '@home/pages/admin/home-settings.component'; import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources-library.component'; +import { QueueComponent} from '@home/pages/admin/queue/queue.component'; @NgModule({ declarations: @@ -39,7 +40,8 @@ import { ResourcesLibraryComponent } from '@home/pages/admin/resource/resources- SecuritySettingsComponent, OAuth2SettingsComponent, HomeSettingsComponent, - ResourcesLibraryComponent + ResourcesLibraryComponent, + QueueComponent ], imports: [ CommonModule, diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html new file mode 100644 index 0000000000..ae163d9ba7 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.html @@ -0,0 +1,182 @@ + +
+ +
+ +
+
+
+ + admin.queue-name + + + {{ 'queue.name-required' | translate }} + + + + queue.poll-interval + + + {{ 'queue.poll-interval-required' | translate }} + + + {{ 'queue.poll-interval-min-value' | translate }} + + + + queue.partitions + + + {{ 'queue.partitions-required' | translate }} + + + {{ 'queue.partitions-min-value' | translate }} + + + + queue.processing-timeout + + + {{ 'queue.pack-processing-timeout-required' | translate }} + + + {{ 'queue.pack-processing-timeout-min-value' | translate }} + + + +
+ + + + + + queue.submit-strategy + {{panel1.expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }} + + +
+ + queue.submit-strategy + + + {{ strategy }} + + + + {{ 'queue.submit-strategy-type-required' | translate }} + + + + queue.batch-size + + + {{ 'queue.batch-size-required' | translate }} + + + {{ 'queue.batch-size-min-value' | translate }} + + +
+
+ + + + queue.processing-strategy + {{panel2.expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down' }} + + + +
+ + queue.processing-strategy + + + {{ strategy }} + + + + {{ 'queue.processing-strategy-type-required' | translate }} + + + + queue.retries + + + {{ 'queue.retries-required' | translate }} + + + {{ 'queue.retries-min-value' | translate }} + + + + queue.failure-percentage + + + {{ 'queue.failure-percentage-required' | translate }} + + + {{ 'queue.failure-percentage-min-value' | translate }} + + + {{ 'queue.failure-percentage-max-value' | translate }} + + + + queue.pause-between-retries + + + {{ 'queue.pause-between-retries-required' | translate }} + + + {{ 'queue.pause-between-retries-min-value' | translate }} + + + + queue.max-pause-between-retries + + + {{ 'queue.max-pause-between-retries-required' | translate }} + + + {{ 'queue.max-pause-between-retries-min-value' | translate }} + + +
+
+
+
+
+
+
diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss new file mode 100644 index 0000000000..852e391b28 --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.scss @@ -0,0 +1,59 @@ +/** + * Copyright © 2016-2022 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. + */ +:host ::ng-deep { + + .mat-expansion-panel:not([class*='mat-elevation-z']) { + box-shadow: none; + } + + .mat-accordion-container { + margin-bottom: 16px; + } + + .mat-expansion-panel { + + &:hover { + box-shadow: 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + } + + &-header { + height: 56px; + border: 1px solid #f2f2f2; + + &-title { + align-items: center; + justify-content: space-between; + margin-right: 0; + } + } + + &.mat-expanded { + border: none; + box-shadow: 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 5px 5px -3px rgba(0, 0, 0, 0.2); + + .mat-expansion-panel { + + &-content { + margin-top: 20px; + } + + &-body { + padding-bottom: 10px; + } + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts new file mode 100644 index 0000000000..b694e21ddf --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queue.component.ts @@ -0,0 +1,154 @@ +/// +/// Copyright © 2016-2022 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. +/// + +import { ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { EntityType } from '@shared/models/entity-type.models'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { EntityComponent } from '@home/components/entity/entity.component'; +import { QueueInfo, QueueProcessingStrategyTypes, QueueSubmitStrategyTypes } from '@shared/models/queue.models'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { EntityTableConfig } from '@home/models/entity/entities-table-config.models'; +import set = Reflect.set; +import { distinctUntilChanged } from 'rxjs/operators'; + +@Component({ + selector: 'tb-queue', + templateUrl: './queue.component.html', + styleUrls: ['./queue.component.scss'] +}) +export class QueueComponent extends EntityComponent { + entityForm: FormGroup; + + entityType = EntityType; + submitStrategies: string[] = []; + processingStrategies: string[] = []; + + QueueSubmitStrategyTypes = QueueSubmitStrategyTypes; + hideBatchSize: boolean = false; + + constructor(protected store: Store, + protected translate: TranslateService, + @Inject('entity') protected entityValue: QueueInfo, + @Inject('entitiesTableConfig') protected entitiesTableConfigValue: EntityTableConfig, + protected cd: ChangeDetectorRef, + public fb: FormBuilder) { + super(store, fb, entityValue, entitiesTableConfigValue, cd); + this.submitStrategies = Object.values(QueueSubmitStrategyTypes); + this.processingStrategies = Object.values(QueueProcessingStrategyTypes); + } + + ngOnInit() { + super.ngOnInit(); + this.entityForm.get('submitStrategy').get('type').valueChanges.subscribe(() => { + this.submitStrategyTypeChanged(); + }); + } + + buildForm(entity: QueueInfo): FormGroup { + return this.fb.group( + { + name: [entity ? entity.name : '', [Validators.required]], + pollInterval: [ + entity && entity.pollInterval ? entity.pollInterval : 25, + [Validators.min(1), Validators.required] + ], + partitions: [ + entity && entity.partitions ? entity.partitions : 10, + [Validators.min(1), Validators.required] + ], + packProcessingTimeout: [ + entity && entity.packProcessingTimeout ? entity.packProcessingTimeout : 2000, + [Validators.min(1), Validators.required] + ], + submitStrategy: this.fb.group({ + type: [entity ? entity.submitStrategy?.type : null, [Validators.required]], + batchSize: [ + entity && entity.submitStrategy?.batchSize ? entity.submitStrategy?.batchSize : 1000, + [Validators.min(1), Validators.required] + ], + }), + processingStrategy: this.fb.group({ + type: [entity ? entity.processingStrategy?.type : null, [Validators.required]], + retries: [ + entity && entity.processingStrategy?.retries ? entity.processingStrategy?.retries : 3, + [Validators.min(0), Validators.required] + ], + failurePercentage: [ + entity && entity.processingStrategy?.failurePercentage ? entity.processingStrategy?.failurePercentage : 0, + [Validators.min(0), Validators.required, Validators.max(100)] + ], + pauseBetweenRetries: [ + entity && entity.processingStrategy?.pauseBetweenRetries ? entity.processingStrategy?.pauseBetweenRetries : 3, + [Validators.min(1), Validators.required] + ], + maxPauseBetweenRetries: [ + entity && entity.processingStrategy?.maxPauseBetweenRetries ? entity.processingStrategy?.maxPauseBetweenRetries : 3, + [Validators.min(1), Validators.required] + ], + }) + } + ); + } + + hideDelete() { + if (this.entitiesTableConfig) { + return !this.entitiesTableConfig.deleteEnabled(this.entity); + } else { + return false; + } + } + + updateForm(entity: QueueInfo) { + this.entityForm.patchValue({ + name: entity.name, + pollInterval: entity.pollInterval, + partitions: entity.partitions, + packProcessingTimeout: entity.packProcessingTimeout, + submitStrategy: { + type: entity.submitStrategy?.type, + batchSize: entity.submitStrategy?.batchSize, + }, + processingStrategy: { + type: entity.processingStrategy?.type, + retries: entity.processingStrategy?.retries, + failurePercentage: entity.processingStrategy?.failurePercentage, + pauseBetweenRetries: entity.processingStrategy?.pauseBetweenRetries, + maxPauseBetweenRetries: entity.processingStrategy?.maxPauseBetweenRetries, + } + }, {emitEvent: true}); + + if (!this.isAdd) { + this.entityForm.get('name').disable({emitEvent: false}); + } + } + + submitStrategyTypeChanged() { + const form = this.entityForm.get("submitStrategy") as FormGroup; + const type: QueueSubmitStrategyTypes = form.get('type').value; + const batchSizeField = form.get('batchSize'); + if (type === QueueSubmitStrategyTypes.BATCH) { + batchSizeField.enable(); + batchSizeField.patchValue(1000); + this.hideBatchSize = true; + } else { + batchSizeField.patchValue(null); + batchSizeField.disable(); + this.hideBatchSize = false; + } + } +} diff --git a/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts b/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts new file mode 100644 index 0000000000..aaaac2168f --- /dev/null +++ b/ui-ngx/src/app/modules/home/pages/admin/queue/queues-table-config.resolver.ts @@ -0,0 +1,115 @@ +/// +/// Copyright © 2016-2022 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. +/// + +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve } from '@angular/router'; +import { + EntityTableColumn, + EntityTableConfig +} from '@home/models/entity/entities-table-config.models'; +import { QueueInfo, ServiceType } from '@shared/models/queue.models'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { BroadcastService } from '@core/services/broadcast.service'; +import { CustomerService } from '@core/http/customer.service'; +import { DialogService } from '@core/services/dialog.service'; +import { HomeDialogsService } from '@home/dialogs/home-dialogs.service'; +import { map, mergeMap, take } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { EntityType, entityTypeResources, entityTypeTranslations } from '@app/shared/models/entity-type.models'; +import { TranslateService } from '@ngx-translate/core'; +import { QueueComponent } from './queue.component'; +import { QueueService } from '@core/http/queue.service'; +import { selectAuthUser } from '@core/auth/auth.selectors'; + +@Injectable() +export class QueuesTableConfigResolver implements Resolve> { + + readonly queueType = ServiceType.TB_RULE_ENGINE; + + private readonly config: EntityTableConfig = new EntityTableConfig(); + + constructor(private store: Store, + private broadcast: BroadcastService, + private queueService: QueueService, + private customerService: CustomerService, + private dialogService: DialogService, + private homeDialogs: HomeDialogsService, + private translate: TranslateService) { + + this.config.entityType = EntityType.QUEUE; + this.config.entityComponent = QueueComponent; + this.config.entityTranslations = entityTypeTranslations.get(EntityType.QUEUE); + this.config.entityResources = entityTypeResources.get(EntityType.QUEUE); + + this.config.deleteEntityTitle = queue => this.translate.instant('queue.delete-queue-title', {queueName: queue.name}); + this.config.deleteEntityContent = () => this.translate.instant('queue.delete-queue-text'); + this.config.deleteEntitiesTitle = count => this.translate.instant('queue.delete-queues-title', {count}); + this.config.deleteEntitiesContent = () => this.translate.instant('queue.delete-queues-text'); + } + + resolve(route: ActivatedRouteSnapshot): Observable> { + this.config.componentsData = { + queueType: this.queueType + }; + + return this.store.pipe(select(selectAuthUser), take(1)).pipe( + map(() => { + this.config.tableTitle = this.translate.instant('admin.queues'); + this.config.columns = this.configureColumns(); + this.configureEntityFunctions(); + return this.config; + }) + ); + } + + configureColumns(): Array> { + return [ + new EntityTableColumn('name', 'admin.queue-name', '25%'), + new EntityTableColumn('partitions', 'admin.queue-partitions', '25%'), + new EntityTableColumn('submitStrategy', 'admin.queue-submit-strategy', '25%', + (entity: QueueInfo) => { + return entity.submitStrategy.type; + }, + () => ({}), + false + ), + new EntityTableColumn('processingStrategy', 'admin.queue-processing-strategy', '25%', + (entity: QueueInfo) => { + return entity.processingStrategy.type; + }, + () => ({}), + false + ) + ]; + } + + configureEntityFunctions(): void { + this.config.entitiesFetchFunction = pageLink => this.queueService.getTenantQueuesByServiceType(pageLink, this.queueType); + this.config.loadEntity = id => this.queueService.getQueueById(id.id); + this.config.saveEntity = queue => this.queueService.saveQueue(this.addTopicForQueue(queue), this.queueType).pipe( + mergeMap((savedQueue) => this.queueService.getQueueById(savedQueue.id.id) + )); + this.config.deleteEntity = id => this.queueService.deleteQueue(id.id); + this.config.deleteEnabled = (queue) => queue && queue.name !== 'Main'; + } + + private addTopicForQueue(queue: QueueInfo): QueueInfo { + const modifiedQueue = Object.assign({}, queue); + modifiedQueue.topic = `tb_rule_engine.${queue.name}`; + return modifiedQueue; + } +} diff --git a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html index e773c16743..6abd58dda9 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html +++ b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.html @@ -17,7 +17,7 @@ --> {{ 'queue.name' | translate }} - - {{ 'queue.name_required' | translate }} + {{ 'queue.name-required' | translate }} device-profile.select-queue-hint diff --git a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts index 14538c9fc7..28a3d94e9e 100644 --- a/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts +++ b/ui-ngx/src/app/shared/components/queue/queue-type-list.component.ts @@ -175,7 +175,7 @@ export class QueueTypeListComponent implements ControlValueAccessor, OnInit, Aft getQueues(): Observable> { if (!this.queues) { this.queues = this.queueService. - getTenantQueuesByServiceType(this.queueType, {ignoreLoading: true}).pipe( + getTenantQueuesNamesByServiceType(this.queueType, {ignoreLoading: true}).pipe( map((queues) => { return queues.map((queueName) => { return { queueName }; diff --git a/ui-ngx/src/app/shared/models/constants.ts b/ui-ngx/src/app/shared/models/constants.ts index 4bfce6414a..29f6ccc020 100644 --- a/ui-ngx/src/app/shared/models/constants.ts +++ b/ui-ngx/src/app/shared/models/constants.ts @@ -133,7 +133,8 @@ export const HelpLinks = { widgetsConfigAlarm: helpBaseUrl + '/docs/user-guide/ui/dashboards#alarm', widgetsConfigStatic: helpBaseUrl + '/docs/user-guide/ui/dashboards#static', ruleNodePushToCloud: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-cloud', - ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge' + ruleNodePushToEdge: helpBaseUrl + '/docs/user-guide/rule-engine-2-0/action-nodes/#push-to-edge', + queue: helpBaseUrl + '/docs/user-guide/queue' } }; diff --git a/ui-ngx/src/app/shared/models/entity-type.models.ts b/ui-ngx/src/app/shared/models/entity-type.models.ts index 2604787b0e..9c97cdaa82 100644 --- a/ui-ngx/src/app/shared/models/entity-type.models.ts +++ b/ui-ngx/src/app/shared/models/entity-type.models.ts @@ -36,7 +36,8 @@ export enum EntityType { API_USAGE_STATE = 'API_USAGE_STATE', TB_RESOURCE = 'TB_RESOURCE', OTA_PACKAGE = 'OTA_PACKAGE', - RPC = 'RPC' + RPC = 'RPC', + QUEUE = 'QUEUE' } export enum AliasEntityType { @@ -306,6 +307,15 @@ export const entityTypeTranslations = new Map { + packProcessingTimeout: number; + partitions: number; + pollInterval: number; + processingStrategy: { + type: QueueProcessingStrategyTypes, + retries: number, + failurePercentage: number, + pauseBetweenRetries: number, + maxPauseBetweenRetries: number + }; + submitStrategy: { + type: QueueSubmitStrategyTypes, + batchSize: number, + }; + tenantId: TenantId; + topic: string; +} diff --git a/ui-ngx/src/app/shared/models/tenant.model.ts b/ui-ngx/src/app/shared/models/tenant.model.ts index 027c8cdce0..dc3c5200d3 100644 --- a/ui-ngx/src/app/shared/models/tenant.model.ts +++ b/ui-ngx/src/app/shared/models/tenant.model.ts @@ -106,6 +106,8 @@ export interface TenantProfile extends BaseData { default?: boolean; isolatedTbCore?: boolean; isolatedTbRuleEngine?: boolean; + maxNumberOfQueues?: number; + maxNumberOfPartitionsPerQueue?: number; profileData?: TenantProfileData; } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index d593fd0c08..75dbdf4f5f 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -252,7 +252,14 @@ "platform-ios": "iOS", "all-platforms": "All platforms", "allowed-platforms": "Allowed platforms" - } + }, + "queue-select-name": "Select queue name", + "queue-name": "Name", + "queue-name-required": "Queue name is required!", + "queues": "Queues", + "queue-partitions": "Partitions", + "queue-submit-strategy": "Submit strategy", + "queue-processing-strategy": "Processing strategy" }, "alarm": { "alarm": "Alarm", @@ -2670,10 +2677,50 @@ "browser-time": "Browser Time" }, "queue": { - "select_name": "Select queue name", - "name": "Queue Name", - "name_required": "Queue name is required!", - "no-queues-matching": "No queues matching '{{queue}}' were found." + "select-name": "Select queue name", + "name": "Name", + "name-required": "Queue name is required!", + "topic-required": "Queue topic is required!", + "poll-interval-required": "Poll interval is required!", + "poll-interval-min-value": "Poll interval value can't be less then 1", + "partitions-required": "Partitions is required!", + "partitions-min-value": "Partitions value can't be less then 1", + "pack-processing-timeout-required": "Processing timeout is required", + "pack-processing-timeout-min-value": "Processing timeout value can't be less then 1", + "batch-size-required": "Batch size is required!", + "batch-size-min-value": "Batch size value can't be less then 1", + "retries-required": "Retries is required!", + "retries-min-value": "Retries value can't be negative", + "failure-percentage-required": "Failure percentage is required!", + "failure-percentage-min-value": "Failure percentage value can't be less then 0", + "failure-percentage-max-value": "Failure percentage value can't be more then 100", + "pause-between-retries-required": "Pause between retries is required!", + "pause-between-retries-min-value": "Pause between retries value can't be less then 1", + "max-pause-between-retries-required": "Max pause between retries is required!", + "max-pause-between-retries-min-value": "Max pause between retries value can't be less then 1", + "submit-strategy-type-required": "Submit strategy type is required!", + "processing-strategy-type-required": "Processing strategy type is required!", + "queues": "Queues", + "selected-queues": "{ count, plural, 1 {1 queue} other {# queues} } selected", + "delete-queue-title": "Are you sure you want to delete the queue '{{queueName}}'?", + "delete-queues-title": "Are you sure you want to delete { count, plural, 1 {1 queue} other {# queues} }?", + "delete-queue-text": "Be careful, after the confirmation the queue and all related data will become unrecoverable.", + "delete-queues-text": "After the confirmation all selected queues will be deleted and won't be accessible.", + "search": "Search queue", + "add" : "Add queue", + "details": "Queue details", + "topic": "Topic", + "submit-strategy": "Submit Strategy", + "processing-strategy": "Processing Strategy", + "poll-interval": "Poll interval", + "partitions": "Partitions", + "processing-timeout": "Processing timeout", + "batch-size": "Batch size", + "retries": "Retries (0 - unlimited)", + "failure-percentage": "Failure Percentage", + "pause-between-retries": "Pause between retries", + "max-pause-between-retries": "Maximal pause between retries", + "delete": "Delete queue" }, "tenant": { "tenant": "Tenant", @@ -2707,7 +2754,13 @@ "isolated-tb-core": "Processing in isolated ThingsBoard Core container", "isolated-tb-rule-engine": "Processing in isolated ThingsBoard Rule Engine container", "isolated-tb-core-details": "Requires separate microservice(s) per isolated Tenant", - "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant" + "isolated-tb-rule-engine-details": "Requires separate microservice(s) per isolated Tenant", + "max-number-of-queues": "Max number of ThingsBoard Rule Engine queues", + "max-number-of-queues-required": "Max number of queues is required", + "max-number-of-queues-min-length": "Max number of queues can't be less then 1", + "max-number-of-partitions-per-queue": "Max number of partitions per ThingsBoard Rule Engine queue", + "max-number-of-partitions-per-queue-required": "Max number of partitions per queue is required", + "max-number-of-partitions-per-queue-min-length": "Max number of partitions per queue can't be less then 1" }, "tenant-profile": { "tenant-profile": "Tenant profile",