Commit 2553cf6b6fcfb1ccfa8f8d0a5cfbfe1466990f8c
Committed by
GitHub
1 parent
c4269023
created Aws Sqs Queue (#2534)
* created Aws Sqs Queue * improvement AwsSqs providers * revert package-lock.json * Aws sqs improvements * Aws sqs improvements * Aws sqs improvements * Aws sqs improvements * aws improvements * aws improvements * aws improvements * added visibility timeout to aws queue
Showing
25 changed files
with
1074 additions
and
10 deletions
... | ... | @@ -88,7 +88,6 @@ public class RpcController extends BaseController { |
88 | 88 | return handleDeviceRPCRequest(false, new DeviceId(UUID.fromString(deviceIdStr)), requestBody); |
89 | 89 | } |
90 | 90 | |
91 | - | |
92 | 91 | private DeferredResult<ResponseEntity> handleDeviceRPCRequest(boolean oneWay, DeviceId deviceId, String requestBody) throws ThingsboardException { |
93 | 92 | try { |
94 | 93 | JsonNode rpcRequestBody = jsonMapper.readTree(requestBody); | ... | ... |
... | ... | @@ -136,6 +136,15 @@ public abstract class AbstractConsumerService<T extends com.google.protobuf.Gene |
136 | 136 | @PreDestroy |
137 | 137 | public void destroy() { |
138 | 138 | stopped = true; |
139 | + | |
140 | + if (mainConsumer != null) { | |
141 | + mainConsumer.unsubscribe(); | |
142 | + } | |
143 | + | |
144 | + if (nfConsumer != null) { | |
145 | + nfConsumer.unsubscribe(); | |
146 | + } | |
147 | + | |
139 | 148 | if (mainConsumerExecutor != null) { |
140 | 149 | mainConsumerExecutor.shutdownNow(); |
141 | 150 | } | ... | ... |
... | ... | @@ -517,7 +517,7 @@ swagger: |
517 | 517 | version: "${SWAGGER_VERSION:2.0}" |
518 | 518 | |
519 | 519 | queue: |
520 | - type: "${TB_QUEUE_TYPE:in-memory}" # kafka or in-memory | |
520 | + type: "${TB_QUEUE_TYPE:in-memory}" # kafka or in-memory or aws-sqs | |
521 | 521 | kafka: |
522 | 522 | bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" |
523 | 523 | acks: "${TB_KAFKA_ACKS:all}" |
... | ... | @@ -525,6 +525,12 @@ queue: |
525 | 525 | batch.size: "${TB_KAFKA_BATCH_SIZE:16384}" |
526 | 526 | linger.ms: "${TB_KAFKA_LINGER_MS:1}" |
527 | 527 | buffer.memory: "${TB_BUFFER_MEMORY:33554432}" |
528 | + aws_sqs: | |
529 | + access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" | |
530 | + secret_access_key: "${TB_QUEUE_AWS_SQS_SECRET_ACCESS_KEY:YOUR_SECRET}" | |
531 | + region: "${TB_QUEUE_AWS_SQS_REGION:YOUR_REGION}" | |
532 | + threads_per_topic: "${TB_QUEUE_AWS_SQS_THREADS_PER_TOPIC:1}" | |
533 | + visibility_timeout: "${TB_QUEUE_AWS_SQS_VISIBILITY_TIMEOUT:30}" #in seconds | |
528 | 534 | partitions: |
529 | 535 | hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" |
530 | 536 | virtual_nodes_size: "${TB_QUEUE_PARTITIONS_VIRTUAL_NODES_SIZE:16}" | ... | ... |
... | ... | @@ -53,6 +53,11 @@ |
53 | 53 | <artifactId>kafka-clients</artifactId> |
54 | 54 | </dependency> |
55 | 55 | <dependency> |
56 | + <groupId>com.amazonaws</groupId> | |
57 | + <artifactId>aws-java-sdk-sqs</artifactId> | |
58 | + </dependency> | |
59 | + | |
60 | + <dependency> | |
56 | 61 | <groupId>org.springframework</groupId> |
57 | 62 | <artifactId>spring-context-support</artifactId> |
58 | 63 | </dependency> | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue; | |
17 | + | |
18 | +import com.google.protobuf.InvalidProtocolBufferException; | |
19 | +import org.thingsboard.server.queue.TbQueueMsg; | |
20 | + | |
21 | +public interface TbQueueMsgDecoder<T extends TbQueueMsg> { | |
22 | + | |
23 | + T decode(TbQueueMsg msg) throws InvalidProtocolBufferException; | |
24 | +} | ... | ... |
... | ... | @@ -92,6 +92,8 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response |
92 | 92 | List<Response> responses = responseTemplate.poll(pollInterval); |
93 | 93 | if (responses.size() > 0) { |
94 | 94 | log.trace("Polling responses completed, consumer records count [{}]", responses.size()); |
95 | + } else { | |
96 | + continue; | |
95 | 97 | } |
96 | 98 | responses.forEach(response -> { |
97 | 99 | log.trace("Received response to Kafka Template request: {}", response); |
... | ... | @@ -145,6 +147,15 @@ public class DefaultTbQueueRequestTemplate<Request extends TbQueueMsg, Response |
145 | 147 | @Override |
146 | 148 | public void stop() { |
147 | 149 | stopped = true; |
150 | + | |
151 | + if (responseTemplate != null) { | |
152 | + responseTemplate.unsubscribe(); | |
153 | + } | |
154 | + | |
155 | + if (requestTemplate != null) { | |
156 | + requestTemplate.stop(); | |
157 | + } | |
158 | + | |
148 | 159 | if (internalExecutor) { |
149 | 160 | executor.shutdownNow(); |
150 | 161 | } | ... | ... |
... | ... | @@ -18,12 +18,12 @@ package org.thingsboard.server.queue.common; |
18 | 18 | import lombok.Builder; |
19 | 19 | import lombok.extern.slf4j.Slf4j; |
20 | 20 | import org.apache.kafka.common.errors.InterruptException; |
21 | +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; | |
21 | 22 | import org.thingsboard.server.queue.TbQueueConsumer; |
22 | 23 | import org.thingsboard.server.queue.TbQueueHandler; |
23 | 24 | import org.thingsboard.server.queue.TbQueueMsg; |
24 | 25 | import org.thingsboard.server.queue.TbQueueProducer; |
25 | 26 | import org.thingsboard.server.queue.TbQueueResponseTemplate; |
26 | -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; | |
27 | 27 | |
28 | 28 | import java.util.List; |
29 | 29 | import java.util.UUID; |
... | ... | @@ -87,6 +87,10 @@ public class DefaultTbQueueResponseTemplate<Request extends TbQueueMsg, Response |
87 | 87 | } |
88 | 88 | List<Request> requests = requestTemplate.poll(pollInterval); |
89 | 89 | |
90 | + if (requests.isEmpty()) { | |
91 | + continue; | |
92 | + } | |
93 | + | |
90 | 94 | requests.forEach(request -> { |
91 | 95 | long currentTime = System.currentTimeMillis(); |
92 | 96 | long requestTime = bytesToLong(request.getHeaders().get(REQUEST_TIME)); |
... | ... | @@ -147,6 +151,12 @@ public class DefaultTbQueueResponseTemplate<Request extends TbQueueMsg, Response |
147 | 151 | |
148 | 152 | public void stop() { |
149 | 153 | stopped = true; |
154 | + if (requestTemplate != null) { | |
155 | + requestTemplate.unsubscribe(); | |
156 | + } | |
157 | + if (responseTemplate != null) { | |
158 | + responseTemplate.stop(); | |
159 | + } | |
150 | 160 | if (timeoutExecutor != null) { |
151 | 161 | timeoutExecutor.shutdownNow(); |
152 | 162 | } | ... | ... |
... | ... | @@ -22,9 +22,9 @@ import org.apache.kafka.clients.consumer.ConsumerConfig; |
22 | 22 | import org.apache.kafka.clients.consumer.ConsumerRecord; |
23 | 23 | import org.apache.kafka.clients.consumer.ConsumerRecords; |
24 | 24 | import org.apache.kafka.clients.consumer.KafkaConsumer; |
25 | +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; | |
25 | 26 | import org.thingsboard.server.queue.TbQueueConsumer; |
26 | 27 | import org.thingsboard.server.queue.TbQueueMsg; |
27 | -import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; | |
28 | 28 | |
29 | 29 | import java.io.IOException; |
30 | 30 | import java.time.Duration; |
... | ... | @@ -96,7 +96,7 @@ public class TBKafkaConsumerTemplate<T extends TbQueueMsg> implements TbQueueCon |
96 | 96 | } else { |
97 | 97 | if (!subscribed) { |
98 | 98 | List<String> topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); |
99 | - topicNames.forEach(this::createTopicIfNotExists); | |
99 | + topicNames.forEach(admin::createTopicIfNotExists); | |
100 | 100 | consumer.subscribe(topicNames); |
101 | 101 | subscribed = true; |
102 | 102 | } |
... | ... | @@ -130,7 +130,4 @@ public class TBKafkaConsumerTemplate<T extends TbQueueMsg> implements TbQueueCon |
130 | 130 | return decoder.decode(new KafkaTbQueueMsg(record)); |
131 | 131 | } |
132 | 132 | |
133 | - private void createTopicIfNotExists(String topic) { | |
134 | - admin.createTopicIfNotExists(topic); | |
135 | - } | |
136 | 133 | } | ... | ... |
common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsMonolithQueueProvider.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.provider; | |
17 | + | |
18 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | |
19 | +import org.springframework.stereotype.Component; | |
20 | +import org.thingsboard.server.common.msg.queue.ServiceType; | |
21 | +import org.thingsboard.server.gen.transport.TransportProtos; | |
22 | +import org.thingsboard.server.queue.TbQueueAdmin; | |
23 | +import org.thingsboard.server.queue.TbQueueConsumer; | |
24 | +import org.thingsboard.server.queue.TbQueueCoreSettings; | |
25 | +import org.thingsboard.server.queue.TbQueueProducer; | |
26 | +import org.thingsboard.server.queue.TbQueueRuleEngineSettings; | |
27 | +import org.thingsboard.server.queue.TbQueueTransportApiSettings; | |
28 | +import org.thingsboard.server.queue.TbQueueTransportNotificationSettings; | |
29 | +import org.thingsboard.server.queue.common.TbProtoQueueMsg; | |
30 | +import org.thingsboard.server.queue.discovery.PartitionService; | |
31 | +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; | |
32 | +import org.thingsboard.server.queue.sqs.TbAwsSqsAdmin; | |
33 | +import org.thingsboard.server.queue.sqs.TbAwsSqsConsumerTemplate; | |
34 | +import org.thingsboard.server.queue.sqs.TbAwsSqsProducerTemplate; | |
35 | +import org.thingsboard.server.queue.sqs.TbAwsSqsSettings; | |
36 | + | |
37 | +@Component | |
38 | +@ConditionalOnExpression("'${queue.type:null}'=='aws-sqs' && '${service.type:null}'=='monolith'") | |
39 | +public class AwsSqsMonolithQueueProvider implements TbCoreQueueProvider, TbRuleEngineQueueProvider { | |
40 | + | |
41 | + private final PartitionService partitionService; | |
42 | + private final TbQueueCoreSettings coreSettings; | |
43 | + private final TbServiceInfoProvider serviceInfoProvider; | |
44 | + private final TbQueueRuleEngineSettings ruleEngineSettings; | |
45 | + private final TbQueueTransportApiSettings transportApiSettings; | |
46 | + private final TbQueueTransportNotificationSettings transportNotificationSettings; | |
47 | + private final TbAwsSqsSettings sqsSettings; | |
48 | + private final TbQueueAdmin admin; | |
49 | + | |
50 | + public AwsSqsMonolithQueueProvider(PartitionService partitionService, TbQueueCoreSettings coreSettings, | |
51 | + TbQueueRuleEngineSettings ruleEngineSettings, | |
52 | + TbServiceInfoProvider serviceInfoProvider, | |
53 | + TbQueueTransportApiSettings transportApiSettings, | |
54 | + TbQueueTransportNotificationSettings transportNotificationSettings, | |
55 | + TbAwsSqsSettings sqsSettings) { | |
56 | + this.partitionService = partitionService; | |
57 | + this.coreSettings = coreSettings; | |
58 | + this.serviceInfoProvider = serviceInfoProvider; | |
59 | + this.ruleEngineSettings = ruleEngineSettings; | |
60 | + this.transportApiSettings = transportApiSettings; | |
61 | + this.transportNotificationSettings = transportNotificationSettings; | |
62 | + this.sqsSettings = sqsSettings; | |
63 | + admin = new TbAwsSqsAdmin(sqsSettings); | |
64 | + } | |
65 | + | |
66 | + @Override | |
67 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToTransportMsg>> getTransportNotificationsMsgProducer() { | |
68 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, transportNotificationSettings.getNotificationsTopic()); | |
69 | + } | |
70 | + | |
71 | + @Override | |
72 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> getRuleEngineMsgProducer() { | |
73 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, ruleEngineSettings.getTopic()); | |
74 | + } | |
75 | + | |
76 | + @Override | |
77 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> getRuleEngineNotificationsMsgProducer() { | |
78 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, ruleEngineSettings.getTopic()); | |
79 | + } | |
80 | + | |
81 | + @Override | |
82 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToCoreMsg>> getTbCoreMsgProducer() { | |
83 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
84 | + } | |
85 | + | |
86 | + @Override | |
87 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToCoreNotificationMsg>> getTbCoreNotificationsMsgProducer() { | |
88 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
89 | + } | |
90 | + | |
91 | + @Override | |
92 | + public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> getToRuleEngineMsgConsumer() { | |
93 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, ruleEngineSettings.getTopic(), | |
94 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
95 | + } | |
96 | + | |
97 | + @Override | |
98 | + public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> getToRuleEngineNotificationsMsgConsumer() { | |
99 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, | |
100 | + partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), | |
101 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
102 | + } | |
103 | + | |
104 | + @Override | |
105 | + public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToCoreMsg>> getToCoreMsgConsumer() { | |
106 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, coreSettings.getTopic(), | |
107 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
108 | + } | |
109 | + | |
110 | + @Override | |
111 | + public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToCoreNotificationMsg>> getToCoreNotificationsMsgConsumer() { | |
112 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, | |
113 | + partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), | |
114 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
115 | + } | |
116 | + | |
117 | + @Override | |
118 | + public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.TransportApiRequestMsg>> getTransportApiRequestConsumer() { | |
119 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, transportApiSettings.getRequestsTopic(), | |
120 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
121 | + } | |
122 | + | |
123 | + @Override | |
124 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.TransportApiResponseMsg>> getTransportApiResponseProducer() { | |
125 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, transportApiSettings.getResponsesTopic()); | |
126 | + } | |
127 | +} | ... | ... |
common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTbCoreQueueProvider.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.provider; | |
17 | + | |
18 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | |
19 | +import org.springframework.stereotype.Component; | |
20 | +import org.thingsboard.server.common.msg.queue.ServiceType; | |
21 | +import org.thingsboard.server.gen.transport.TransportProtos; | |
22 | +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; | |
23 | +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; | |
24 | +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; | |
25 | +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiRequestMsg; | |
26 | +import org.thingsboard.server.gen.transport.TransportProtos.TransportApiResponseMsg; | |
27 | +import org.thingsboard.server.queue.TbQueueAdmin; | |
28 | +import org.thingsboard.server.queue.TbQueueConsumer; | |
29 | +import org.thingsboard.server.queue.TbQueueCoreSettings; | |
30 | +import org.thingsboard.server.queue.TbQueueProducer; | |
31 | +import org.thingsboard.server.queue.TbQueueRuleEngineSettings; | |
32 | +import org.thingsboard.server.queue.TbQueueTransportApiSettings; | |
33 | +import org.thingsboard.server.queue.common.TbProtoQueueMsg; | |
34 | +import org.thingsboard.server.queue.discovery.PartitionService; | |
35 | +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; | |
36 | +import org.thingsboard.server.queue.sqs.TbAwsSqsAdmin; | |
37 | +import org.thingsboard.server.queue.sqs.TbAwsSqsConsumerTemplate; | |
38 | +import org.thingsboard.server.queue.sqs.TbAwsSqsProducerTemplate; | |
39 | +import org.thingsboard.server.queue.sqs.TbAwsSqsSettings; | |
40 | + | |
41 | +@Component | |
42 | +@ConditionalOnExpression("'${queue.type:null}'=='aws-sqs' && '${service.type:null}'=='tb-core'") | |
43 | +public class AwsSqsTbCoreQueueProvider implements TbCoreQueueProvider { | |
44 | + | |
45 | + private final TbAwsSqsSettings sqsSettings; | |
46 | + private final TbQueueRuleEngineSettings ruleEngineSettings; | |
47 | + private final TbQueueCoreSettings coreSettings; | |
48 | + private final TbQueueTransportApiSettings transportApiSettings; | |
49 | + private final PartitionService partitionService; | |
50 | + private final TbServiceInfoProvider serviceInfoProvider; | |
51 | + | |
52 | + | |
53 | + private final TbQueueAdmin admin; | |
54 | + | |
55 | + public AwsSqsTbCoreQueueProvider(TbAwsSqsSettings sqsSettings, | |
56 | + TbQueueCoreSettings coreSettings, | |
57 | + TbQueueTransportApiSettings transportApiSettings, | |
58 | + TbQueueRuleEngineSettings ruleEngineSettings, | |
59 | + PartitionService partitionService, | |
60 | + TbServiceInfoProvider serviceInfoProvider) { | |
61 | + this.sqsSettings = sqsSettings; | |
62 | + this.coreSettings = coreSettings; | |
63 | + this.transportApiSettings = transportApiSettings; | |
64 | + this.ruleEngineSettings = ruleEngineSettings; | |
65 | + this.partitionService = partitionService; | |
66 | + this.serviceInfoProvider = serviceInfoProvider; | |
67 | + this.admin = new TbAwsSqsAdmin(sqsSettings); | |
68 | + } | |
69 | + | |
70 | + @Override | |
71 | + public TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> getTransportNotificationsMsgProducer() { | |
72 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
73 | + } | |
74 | + | |
75 | + @Override | |
76 | + public TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> getRuleEngineMsgProducer() { | |
77 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
78 | + } | |
79 | + | |
80 | + @Override | |
81 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> getRuleEngineNotificationsMsgProducer() { | |
82 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, ruleEngineSettings.getTopic()); | |
83 | + } | |
84 | + | |
85 | + @Override | |
86 | + public TbQueueProducer<TbProtoQueueMsg<ToCoreMsg>> getTbCoreMsgProducer() { | |
87 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
88 | + } | |
89 | + | |
90 | + @Override | |
91 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToCoreNotificationMsg>> getTbCoreNotificationsMsgProducer() { | |
92 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
93 | + } | |
94 | + | |
95 | + @Override | |
96 | + public TbQueueConsumer<TbProtoQueueMsg<ToCoreMsg>> getToCoreMsgConsumer() { | |
97 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, coreSettings.getTopic(), | |
98 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), ToCoreMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
99 | + } | |
100 | + | |
101 | + @Override | |
102 | + public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToCoreNotificationMsg>> getToCoreNotificationsMsgConsumer() { | |
103 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, | |
104 | + partitionService.getNotificationsTopic(ServiceType.TB_CORE, serviceInfoProvider.getServiceId()).getFullTopicName(), | |
105 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToCoreNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
106 | + } | |
107 | + | |
108 | + @Override | |
109 | + public TbQueueConsumer<TbProtoQueueMsg<TransportApiRequestMsg>> getTransportApiRequestConsumer() { | |
110 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, transportApiSettings.getRequestsTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportApiRequestMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
111 | + } | |
112 | + | |
113 | + @Override | |
114 | + public TbQueueProducer<TbProtoQueueMsg<TransportApiResponseMsg>> getTransportApiResponseProducer() { | |
115 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
116 | + } | |
117 | +} | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.provider; | |
17 | + | |
18 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | |
19 | +import org.springframework.stereotype.Component; | |
20 | +import org.thingsboard.server.common.msg.queue.ServiceType; | |
21 | +import org.thingsboard.server.gen.transport.TransportProtos; | |
22 | +import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; | |
23 | +import org.thingsboard.server.gen.transport.TransportProtos.ToRuleEngineMsg; | |
24 | +import org.thingsboard.server.gen.transport.TransportProtos.ToTransportMsg; | |
25 | +import org.thingsboard.server.queue.TbQueueAdmin; | |
26 | +import org.thingsboard.server.queue.TbQueueConsumer; | |
27 | +import org.thingsboard.server.queue.TbQueueCoreSettings; | |
28 | +import org.thingsboard.server.queue.TbQueueProducer; | |
29 | +import org.thingsboard.server.queue.TbQueueRuleEngineSettings; | |
30 | +import org.thingsboard.server.queue.common.TbProtoQueueMsg; | |
31 | +import org.thingsboard.server.queue.discovery.PartitionService; | |
32 | +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; | |
33 | +import org.thingsboard.server.queue.sqs.TbAwsSqsAdmin; | |
34 | +import org.thingsboard.server.queue.sqs.TbAwsSqsConsumerTemplate; | |
35 | +import org.thingsboard.server.queue.sqs.TbAwsSqsProducerTemplate; | |
36 | +import org.thingsboard.server.queue.sqs.TbAwsSqsSettings; | |
37 | + | |
38 | +@Component | |
39 | +@ConditionalOnExpression("'${queue.type:null}'=='aws-sqs' && '${service.type:null}'=='tb-rule-engine'") | |
40 | +public class AwsSqsTbRuleEngineQueueProvider implements TbRuleEngineQueueProvider { | |
41 | + | |
42 | + private final PartitionService partitionService; | |
43 | + private final TbQueueCoreSettings coreSettings; | |
44 | + private final TbServiceInfoProvider serviceInfoProvider; | |
45 | + private final TbQueueRuleEngineSettings ruleEngineSettings; | |
46 | + private final TbAwsSqsSettings sqsSettings; | |
47 | + private final TbQueueAdmin admin; | |
48 | + | |
49 | + public AwsSqsTbRuleEngineQueueProvider(PartitionService partitionService, TbQueueCoreSettings coreSettings, | |
50 | + TbQueueRuleEngineSettings ruleEngineSettings, | |
51 | + TbServiceInfoProvider serviceInfoProvider, | |
52 | + TbAwsSqsSettings sqsSettings) { | |
53 | + this.partitionService = partitionService; | |
54 | + this.coreSettings = coreSettings; | |
55 | + this.serviceInfoProvider = serviceInfoProvider; | |
56 | + this.ruleEngineSettings = ruleEngineSettings; | |
57 | + this.sqsSettings = sqsSettings; | |
58 | + admin = new TbAwsSqsAdmin(sqsSettings); | |
59 | + } | |
60 | + | |
61 | + @Override | |
62 | + public TbQueueProducer<TbProtoQueueMsg<ToTransportMsg>> getTransportNotificationsMsgProducer() { | |
63 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
64 | + } | |
65 | + | |
66 | + @Override | |
67 | + public TbQueueProducer<TbProtoQueueMsg<ToRuleEngineMsg>> getRuleEngineMsgProducer() { | |
68 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
69 | + } | |
70 | + | |
71 | + @Override | |
72 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> getRuleEngineNotificationsMsgProducer() { | |
73 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, ruleEngineSettings.getTopic()); | |
74 | + } | |
75 | + | |
76 | + @Override | |
77 | + public TbQueueProducer<TbProtoQueueMsg<ToCoreMsg>> getTbCoreMsgProducer() { | |
78 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
79 | + } | |
80 | + | |
81 | + @Override | |
82 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToCoreNotificationMsg>> getTbCoreNotificationsMsgProducer() { | |
83 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, coreSettings.getTopic()); | |
84 | + } | |
85 | + | |
86 | + @Override | |
87 | + public TbQueueConsumer<TbProtoQueueMsg<ToRuleEngineMsg>> getToRuleEngineMsgConsumer() { | |
88 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, ruleEngineSettings.getTopic(), msg -> new TbProtoQueueMsg<>(msg.getKey(), ToRuleEngineMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
89 | + } | |
90 | + | |
91 | + @Override | |
92 | + public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToRuleEngineNotificationMsg>> getToRuleEngineNotificationsMsgConsumer() { | |
93 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, | |
94 | + partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceInfoProvider.getServiceId()).getFullTopicName(), | |
95 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToRuleEngineNotificationMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
96 | + } | |
97 | +} | ... | ... |
common/queue/src/main/java/org/thingsboard/server/queue/provider/AwsSqsTransportQueueProvider.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.provider; | |
17 | + | |
18 | +import lombok.extern.slf4j.Slf4j; | |
19 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | |
20 | +import org.springframework.stereotype.Component; | |
21 | +import org.thingsboard.server.gen.transport.TransportProtos; | |
22 | +import org.thingsboard.server.queue.TbQueueAdmin; | |
23 | +import org.thingsboard.server.queue.TbQueueConsumer; | |
24 | +import org.thingsboard.server.queue.TbQueueProducer; | |
25 | +import org.thingsboard.server.queue.TbQueueRequestTemplate; | |
26 | +import org.thingsboard.server.queue.TbQueueTransportApiSettings; | |
27 | +import org.thingsboard.server.queue.TbQueueTransportNotificationSettings; | |
28 | +import org.thingsboard.server.queue.common.DefaultTbQueueRequestTemplate; | |
29 | +import org.thingsboard.server.queue.common.TbProtoQueueMsg; | |
30 | +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; | |
31 | +import org.thingsboard.server.queue.sqs.TbAwsSqsAdmin; | |
32 | +import org.thingsboard.server.queue.sqs.TbAwsSqsConsumerTemplate; | |
33 | +import org.thingsboard.server.queue.sqs.TbAwsSqsProducerTemplate; | |
34 | +import org.thingsboard.server.queue.sqs.TbAwsSqsSettings; | |
35 | + | |
36 | +@Component | |
37 | +@ConditionalOnExpression("'${queue.type:null}'=='aws-sqs' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-transport')") | |
38 | +@Slf4j | |
39 | +public class AwsSqsTransportQueueProvider implements TbTransportQueueProvider { | |
40 | + private final TbQueueTransportApiSettings transportApiSettings; | |
41 | + private final TbQueueTransportNotificationSettings transportNotificationSettings; | |
42 | + private final TbAwsSqsSettings sqsSettings; | |
43 | + private final TbQueueAdmin admin; | |
44 | + private final TbServiceInfoProvider serviceInfoProvider; | |
45 | + | |
46 | + public AwsSqsTransportQueueProvider(TbQueueTransportApiSettings transportApiSettings, | |
47 | + TbQueueTransportNotificationSettings transportNotificationSettings, | |
48 | + TbAwsSqsSettings sqsSettings, | |
49 | + TbServiceInfoProvider serviceInfoProvider) { | |
50 | + this.transportApiSettings = transportApiSettings; | |
51 | + this.transportNotificationSettings = transportNotificationSettings; | |
52 | + this.sqsSettings = sqsSettings; | |
53 | + admin = new TbAwsSqsAdmin(sqsSettings); | |
54 | + this.serviceInfoProvider = serviceInfoProvider; | |
55 | + } | |
56 | + | |
57 | + @Override | |
58 | + public TbQueueRequestTemplate<TbProtoQueueMsg<TransportProtos.TransportApiRequestMsg>, TbProtoQueueMsg<TransportProtos.TransportApiResponseMsg>> getTransportApiRequestTemplate() { | |
59 | + TbAwsSqsProducerTemplate<TbProtoQueueMsg<TransportProtos.TransportApiRequestMsg>> producerTemplate = | |
60 | + new TbAwsSqsProducerTemplate<>(admin, sqsSettings, transportApiSettings.getRequestsTopic()); | |
61 | + | |
62 | + TbAwsSqsConsumerTemplate<TbProtoQueueMsg<TransportProtos.TransportApiResponseMsg>> consumerTemplate = | |
63 | + new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, | |
64 | + transportApiSettings.getResponsesTopic() + "_" + serviceInfoProvider.getServiceId(), | |
65 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.TransportApiResponseMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
66 | + | |
67 | + DefaultTbQueueRequestTemplate.DefaultTbQueueRequestTemplateBuilder | |
68 | + <TbProtoQueueMsg<TransportProtos.TransportApiRequestMsg>, TbProtoQueueMsg<TransportProtos.TransportApiResponseMsg>> templateBuilder = DefaultTbQueueRequestTemplate.builder(); | |
69 | + templateBuilder.queueAdmin(admin); | |
70 | + templateBuilder.requestTemplate(producerTemplate); | |
71 | + templateBuilder.responseTemplate(consumerTemplate); | |
72 | + templateBuilder.maxPendingRequests(transportApiSettings.getMaxPendingRequests()); | |
73 | + templateBuilder.maxRequestTimeout(transportApiSettings.getMaxRequestsTimeout()); | |
74 | + templateBuilder.pollInterval(transportApiSettings.getResponsePollInterval()); | |
75 | + return templateBuilder.build(); | |
76 | + } | |
77 | + | |
78 | + @Override | |
79 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToRuleEngineMsg>> getRuleEngineMsgProducer() { | |
80 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, transportApiSettings.getRequestsTopic()); | |
81 | + } | |
82 | + | |
83 | + @Override | |
84 | + public TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToCoreMsg>> getTbCoreMsgProducer() { | |
85 | + return new TbAwsSqsProducerTemplate<>(admin, sqsSettings, transportApiSettings.getRequestsTopic()); | |
86 | + } | |
87 | + | |
88 | + @Override | |
89 | + public TbQueueConsumer<TbProtoQueueMsg<TransportProtos.ToTransportMsg>> getTransportNotificationsConsumer() { | |
90 | + return new TbAwsSqsConsumerTemplate<>(admin, sqsSettings, transportNotificationSettings.getNotificationsTopic() + "_" + serviceInfoProvider.getServiceId(), | |
91 | + msg -> new TbProtoQueueMsg<>(msg.getKey(), TransportProtos.ToTransportMsg.parseFrom(msg.getData()), msg.getHeaders())); | |
92 | + } | |
93 | +} | ... | ... |
common/queue/src/main/java/org/thingsboard/server/queue/sqs/AwsSqsTbQueueMsgMetadata.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.sqs; | |
17 | + | |
18 | +import com.amazonaws.http.SdkHttpMetadata; | |
19 | +import lombok.AllArgsConstructor; | |
20 | +import lombok.Data; | |
21 | +import org.thingsboard.server.queue.TbQueueMsgMetadata; | |
22 | + | |
23 | +@Data | |
24 | +@AllArgsConstructor | |
25 | +public class AwsSqsTbQueueMsgMetadata implements TbQueueMsgMetadata { | |
26 | + | |
27 | + private final SdkHttpMetadata metadata; | |
28 | +} | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.sqs; | |
17 | + | |
18 | +import com.amazonaws.auth.AWSCredentials; | |
19 | +import com.amazonaws.auth.AWSStaticCredentialsProvider; | |
20 | +import com.amazonaws.auth.BasicAWSCredentials; | |
21 | +import com.amazonaws.services.sqs.AmazonSQS; | |
22 | +import com.amazonaws.services.sqs.AmazonSQSClientBuilder; | |
23 | +import com.amazonaws.services.sqs.model.CreateQueueRequest; | |
24 | +import com.amazonaws.services.sqs.model.QueueAttributeName; | |
25 | +import org.thingsboard.server.queue.TbQueueAdmin; | |
26 | + | |
27 | +import java.util.HashMap; | |
28 | +import java.util.Map; | |
29 | + | |
30 | +public class TbAwsSqsAdmin implements TbQueueAdmin { | |
31 | + | |
32 | + private final TbAwsSqsSettings sqsSettings; | |
33 | + private final Map<String, String> attributes = new HashMap<>(); | |
34 | + private final AWSStaticCredentialsProvider credProvider; | |
35 | + | |
36 | + public TbAwsSqsAdmin(TbAwsSqsSettings sqsSettings) { | |
37 | + this.sqsSettings = sqsSettings; | |
38 | + | |
39 | + AWSCredentials awsCredentials = new BasicAWSCredentials(sqsSettings.getAccessKeyId(), sqsSettings.getSecretAccessKey()); | |
40 | + this.credProvider = new AWSStaticCredentialsProvider(awsCredentials); | |
41 | + | |
42 | + attributes.put("FifoQueue", "true"); | |
43 | + attributes.put("ContentBasedDeduplication", "true"); | |
44 | + attributes.put(QueueAttributeName.VisibilityTimeout.toString(), sqsSettings.getVisibilityTimeout()); | |
45 | + } | |
46 | + | |
47 | + @Override | |
48 | + public void createTopicIfNotExists(String topic) { | |
49 | + AmazonSQS sqsClient = AmazonSQSClientBuilder.standard() | |
50 | + .withCredentials(credProvider) | |
51 | + .withRegion(sqsSettings.getRegion()) | |
52 | + .build(); | |
53 | + | |
54 | + final CreateQueueRequest createQueueRequest = | |
55 | + new CreateQueueRequest(topic.replaceAll("\\.", "_") + ".fifo") | |
56 | + .withAttributes(attributes); | |
57 | + try { | |
58 | + sqsClient.createQueue(createQueueRequest); | |
59 | + } finally { | |
60 | + if (sqsClient != null) { | |
61 | + sqsClient.shutdown(); | |
62 | + } | |
63 | + } | |
64 | + } | |
65 | +} | ... | ... |
common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsConsumerTemplate.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.sqs; | |
17 | + | |
18 | +import com.amazonaws.auth.AWSCredentials; | |
19 | +import com.amazonaws.auth.AWSStaticCredentialsProvider; | |
20 | +import com.amazonaws.auth.BasicAWSCredentials; | |
21 | +import com.amazonaws.services.sqs.AmazonSQS; | |
22 | +import com.amazonaws.services.sqs.AmazonSQSClientBuilder; | |
23 | +import com.amazonaws.services.sqs.model.DeleteMessageBatchRequestEntry; | |
24 | +import com.amazonaws.services.sqs.model.Message; | |
25 | +import com.amazonaws.services.sqs.model.ReceiveMessageRequest; | |
26 | +import com.google.common.reflect.TypeToken; | |
27 | +import com.google.common.util.concurrent.Futures; | |
28 | +import com.google.common.util.concurrent.ListenableFuture; | |
29 | +import com.google.common.util.concurrent.ListeningExecutorService; | |
30 | +import com.google.common.util.concurrent.MoreExecutors; | |
31 | +import com.google.gson.Gson; | |
32 | +import com.google.protobuf.InvalidProtocolBufferException; | |
33 | +import lombok.Data; | |
34 | +import lombok.extern.slf4j.Slf4j; | |
35 | +import org.springframework.util.CollectionUtils; | |
36 | +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; | |
37 | +import org.thingsboard.server.queue.TbQueueAdmin; | |
38 | +import org.thingsboard.server.queue.TbQueueConsumer; | |
39 | +import org.thingsboard.server.queue.TbQueueMsg; | |
40 | +import org.thingsboard.server.queue.TbQueueMsgDecoder; | |
41 | +import org.thingsboard.server.queue.TbQueueMsgHeaders; | |
42 | +import org.thingsboard.server.queue.common.DefaultTbQueueMsgHeaders; | |
43 | + | |
44 | +import java.io.IOException; | |
45 | +import java.util.ArrayList; | |
46 | +import java.util.Collections; | |
47 | +import java.util.List; | |
48 | +import java.util.Map; | |
49 | +import java.util.Objects; | |
50 | +import java.util.Set; | |
51 | +import java.util.concurrent.CopyOnWriteArrayList; | |
52 | +import java.util.concurrent.ExecutionException; | |
53 | +import java.util.concurrent.Executors; | |
54 | +import java.util.concurrent.TimeUnit; | |
55 | +import java.util.stream.Collectors; | |
56 | +import java.util.stream.Stream; | |
57 | + | |
58 | +@Slf4j | |
59 | +public class TbAwsSqsConsumerTemplate<T extends TbQueueMsg> implements TbQueueConsumer<T> { | |
60 | + | |
61 | + private static final int MAX_NUM_MSGS = 10; | |
62 | + | |
63 | + private final Gson gson = new Gson(); | |
64 | + private final TbQueueAdmin admin; | |
65 | + private final AmazonSQS sqsClient; | |
66 | + private final String topic; | |
67 | + private final TbQueueMsgDecoder<T> decoder; | |
68 | + private final TbAwsSqsSettings sqsSettings; | |
69 | + | |
70 | + private final List<AwsSqsMsgWrapper> pendingMessages = new CopyOnWriteArrayList<>(); | |
71 | + private volatile Set<String> queueUrls; | |
72 | + private volatile Set<TopicPartitionInfo> partitions; | |
73 | + private ListeningExecutorService consumerExecutor; | |
74 | + private volatile boolean subscribed; | |
75 | + private volatile boolean stopped = false; | |
76 | + | |
77 | + public TbAwsSqsConsumerTemplate(TbQueueAdmin admin, TbAwsSqsSettings sqsSettings, String topic, TbQueueMsgDecoder<T> decoder) { | |
78 | + this.admin = admin; | |
79 | + this.decoder = decoder; | |
80 | + this.topic = topic; | |
81 | + this.sqsSettings = sqsSettings; | |
82 | + | |
83 | + AWSCredentials awsCredentials = new BasicAWSCredentials(sqsSettings.getAccessKeyId(), sqsSettings.getSecretAccessKey()); | |
84 | + AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials); | |
85 | + | |
86 | + this.sqsClient = AmazonSQSClientBuilder.standard() | |
87 | + .withCredentials(credProvider) | |
88 | + .withRegion(sqsSettings.getRegion()) | |
89 | + .build(); | |
90 | + } | |
91 | + | |
92 | + @Override | |
93 | + public String getTopic() { | |
94 | + return topic; | |
95 | + } | |
96 | + | |
97 | + @Override | |
98 | + public void subscribe() { | |
99 | + partitions = Collections.singleton(new TopicPartitionInfo(topic, null, null, true)); | |
100 | + subscribed = false; | |
101 | + } | |
102 | + | |
103 | + @Override | |
104 | + public void subscribe(Set<TopicPartitionInfo> partitions) { | |
105 | + this.partitions = partitions; | |
106 | + subscribed = false; | |
107 | + } | |
108 | + | |
109 | + @Override | |
110 | + public void unsubscribe() { | |
111 | + stopped = true; | |
112 | + | |
113 | + if (sqsClient != null) { | |
114 | + sqsClient.shutdown(); | |
115 | + } | |
116 | + if (consumerExecutor != null) { | |
117 | + consumerExecutor.shutdownNow(); | |
118 | + } | |
119 | + } | |
120 | + | |
121 | + @Override | |
122 | + public List<T> poll(long durationInMillis) { | |
123 | + if (!subscribed && partitions == null) { | |
124 | + try { | |
125 | + Thread.sleep(durationInMillis); | |
126 | + } catch (InterruptedException e) { | |
127 | + log.debug("Failed to await subscription", e); | |
128 | + } | |
129 | + } else { | |
130 | + if (!subscribed) { | |
131 | + List<String> topicNames = partitions.stream().map(TopicPartitionInfo::getFullTopicName).collect(Collectors.toList()); | |
132 | + queueUrls = topicNames.stream().map(this::getQueueUrl).collect(Collectors.toSet()); | |
133 | + consumerExecutor = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(queueUrls.size() * sqsSettings.getThreadsPerTopic() + 1)); | |
134 | + subscribed = true; | |
135 | + } | |
136 | + | |
137 | + if (!pendingMessages.isEmpty()) { | |
138 | + log.warn("Present {} non committed messages.", pendingMessages.size()); | |
139 | + return Collections.emptyList(); | |
140 | + } | |
141 | + | |
142 | + List<ListenableFuture<List<Message>>> futureList = queueUrls | |
143 | + .stream() | |
144 | + .map(url -> poll(url, (int) TimeUnit.MILLISECONDS.toSeconds(durationInMillis))) | |
145 | + .collect(Collectors.toList()); | |
146 | + ListenableFuture<List<List<Message>>> futureResult = Futures.allAsList(futureList); | |
147 | + try { | |
148 | + return futureResult.get().stream() | |
149 | + .flatMap(List::stream) | |
150 | + .map(msg -> { | |
151 | + try { | |
152 | + return decode(msg); | |
153 | + } catch (IOException e) { | |
154 | + log.error("Failed to decode message: [{}]", msg); | |
155 | + return null; | |
156 | + } | |
157 | + }).filter(Objects::nonNull) | |
158 | + .collect(Collectors.toList()); | |
159 | + } catch (InterruptedException | ExecutionException e) { | |
160 | + if (stopped) { | |
161 | + log.info("[{}] Aws SQS consumer is stopped.", topic); | |
162 | + } else { | |
163 | + log.error("Failed to pool messages.", e); | |
164 | + } | |
165 | + } | |
166 | + } | |
167 | + return Collections.emptyList(); | |
168 | + } | |
169 | + | |
170 | + private ListenableFuture<List<Message>> poll(String url, int waitTimeSeconds) { | |
171 | + List<ListenableFuture<List<Message>>> result = new ArrayList<>(); | |
172 | + | |
173 | + for (int i = 0; i < sqsSettings.getThreadsPerTopic(); i++) { | |
174 | + result.add(consumerExecutor.submit(() -> { | |
175 | + ReceiveMessageRequest request = new ReceiveMessageRequest(); | |
176 | + request | |
177 | + .withWaitTimeSeconds(waitTimeSeconds) | |
178 | + .withMessageAttributeNames("headers") | |
179 | + .withQueueUrl(url) | |
180 | + .withMaxNumberOfMessages(MAX_NUM_MSGS); | |
181 | + return sqsClient.receiveMessage(request).getMessages(); | |
182 | + })); | |
183 | + } | |
184 | + return Futures.transform(Futures.allAsList(result), list -> { | |
185 | + if (!CollectionUtils.isEmpty(list)) { | |
186 | + return list.stream() | |
187 | + .flatMap(messageList -> { | |
188 | + if (!messageList.isEmpty()) { | |
189 | + this.pendingMessages.add(new AwsSqsMsgWrapper(url, messageList)); | |
190 | + return messageList.stream(); | |
191 | + } | |
192 | + return Stream.empty(); | |
193 | + }) | |
194 | + .collect(Collectors.toList()); | |
195 | + } | |
196 | + return Collections.emptyList(); | |
197 | + }, consumerExecutor); | |
198 | + } | |
199 | + | |
200 | + @Override | |
201 | + public void commit() { | |
202 | + pendingMessages.forEach(msg -> | |
203 | + consumerExecutor.submit(() -> { | |
204 | + List<DeleteMessageBatchRequestEntry> entries = msg.getMessages() | |
205 | + .stream() | |
206 | + .map(message -> new DeleteMessageBatchRequestEntry(message.getMessageId(), message.getReceiptHandle())) | |
207 | + .collect(Collectors.toList()); | |
208 | + sqsClient.deleteMessageBatch(msg.getUrl(), entries); | |
209 | + })); | |
210 | + | |
211 | + pendingMessages.clear(); | |
212 | + } | |
213 | + | |
214 | + public T decode(Message message) throws InvalidProtocolBufferException { | |
215 | + TbAwsSqsMsg msg = gson.fromJson(message.getBody(), TbAwsSqsMsg.class); | |
216 | + TbQueueMsgHeaders headers = new DefaultTbQueueMsgHeaders(); | |
217 | + Map<String, byte[]> headerMap = gson.fromJson(message.getMessageAttributes().get("headers").getStringValue(), new TypeToken<Map<String, byte[]>>() { | |
218 | + }.getType()); | |
219 | + headerMap.forEach(headers::put); | |
220 | + msg.setHeaders(headers); | |
221 | + return decoder.decode(msg); | |
222 | + } | |
223 | + | |
224 | + @Data | |
225 | + private static class AwsSqsMsgWrapper { | |
226 | + private final String url; | |
227 | + private final List<Message> messages; | |
228 | + | |
229 | + public AwsSqsMsgWrapper(String url, List<Message> messages) { | |
230 | + this.url = url; | |
231 | + this.messages = messages; | |
232 | + } | |
233 | + } | |
234 | + | |
235 | + private String getQueueUrl(String topic) { | |
236 | + admin.createTopicIfNotExists(topic); | |
237 | + return sqsClient.getQueueUrl(topic.replaceAll("\\.", "_") + ".fifo").getQueueUrl(); | |
238 | + } | |
239 | +} | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.sqs; | |
17 | + | |
18 | +import com.google.gson.annotations.Expose; | |
19 | +import lombok.Data; | |
20 | +import org.thingsboard.server.queue.TbQueueMsg; | |
21 | +import org.thingsboard.server.queue.TbQueueMsgHeaders; | |
22 | + | |
23 | +import java.util.UUID; | |
24 | + | |
25 | +@Data | |
26 | +public class TbAwsSqsMsg implements TbQueueMsg { | |
27 | + private final UUID key; | |
28 | + private final byte[] data; | |
29 | + | |
30 | + public TbAwsSqsMsg(UUID key, byte[] data) { | |
31 | + this.key = key; | |
32 | + this.data = data; | |
33 | + } | |
34 | + | |
35 | + @Expose(serialize = false, deserialize = false) | |
36 | + private TbQueueMsgHeaders headers; | |
37 | + | |
38 | +} | ... | ... |
common/queue/src/main/java/org/thingsboard/server/queue/sqs/TbAwsSqsProducerTemplate.java
0 → 100644
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.sqs; | |
17 | + | |
18 | +import com.amazonaws.auth.AWSCredentials; | |
19 | +import com.amazonaws.auth.AWSStaticCredentialsProvider; | |
20 | +import com.amazonaws.auth.BasicAWSCredentials; | |
21 | +import com.amazonaws.services.sqs.AmazonSQS; | |
22 | +import com.amazonaws.services.sqs.AmazonSQSClientBuilder; | |
23 | +import com.amazonaws.services.sqs.model.MessageAttributeValue; | |
24 | +import com.amazonaws.services.sqs.model.SendMessageRequest; | |
25 | +import com.amazonaws.services.sqs.model.SendMessageResult; | |
26 | +import com.google.common.util.concurrent.FutureCallback; | |
27 | +import com.google.common.util.concurrent.Futures; | |
28 | +import com.google.common.util.concurrent.ListenableFuture; | |
29 | +import com.google.common.util.concurrent.ListeningExecutorService; | |
30 | +import com.google.common.util.concurrent.MoreExecutors; | |
31 | +import com.google.gson.Gson; | |
32 | +import lombok.extern.slf4j.Slf4j; | |
33 | +import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; | |
34 | +import org.thingsboard.server.queue.TbQueueAdmin; | |
35 | +import org.thingsboard.server.queue.TbQueueCallback; | |
36 | +import org.thingsboard.server.queue.TbQueueMsg; | |
37 | +import org.thingsboard.server.queue.TbQueueProducer; | |
38 | + | |
39 | +import java.util.HashMap; | |
40 | +import java.util.Map; | |
41 | +import java.util.concurrent.ConcurrentHashMap; | |
42 | +import java.util.concurrent.Executors; | |
43 | + | |
44 | +@Slf4j | |
45 | +public class TbAwsSqsProducerTemplate<T extends TbQueueMsg> implements TbQueueProducer<T> { | |
46 | + private final String defaultTopic; | |
47 | + private final AmazonSQS sqsClient; | |
48 | + private final Gson gson = new Gson(); | |
49 | + private final Map<String, String> queueUrlMap = new ConcurrentHashMap<>(); | |
50 | + private final TbQueueAdmin admin; | |
51 | + private ListeningExecutorService producerExecutor; | |
52 | + | |
53 | + public TbAwsSqsProducerTemplate(TbQueueAdmin admin, TbAwsSqsSettings sqsSettings, String defaultTopic) { | |
54 | + this.admin = admin; | |
55 | + this.defaultTopic = defaultTopic; | |
56 | + | |
57 | + AWSCredentials awsCredentials = new BasicAWSCredentials(sqsSettings.getAccessKeyId(), sqsSettings.getSecretAccessKey()); | |
58 | + AWSStaticCredentialsProvider credProvider = new AWSStaticCredentialsProvider(awsCredentials); | |
59 | + | |
60 | + this.sqsClient = AmazonSQSClientBuilder.standard() | |
61 | + .withCredentials(credProvider) | |
62 | + .withRegion(sqsSettings.getRegion()) | |
63 | + .build(); | |
64 | + | |
65 | + producerExecutor = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); | |
66 | + } | |
67 | + | |
68 | + @Override | |
69 | + public void init() { | |
70 | + | |
71 | + } | |
72 | + | |
73 | + @Override | |
74 | + public String getDefaultTopic() { | |
75 | + return defaultTopic; | |
76 | + } | |
77 | + | |
78 | + @Override | |
79 | + public void send(TopicPartitionInfo tpi, T msg, TbQueueCallback callback) { | |
80 | + SendMessageRequest sendMsgRequest = new SendMessageRequest(); | |
81 | + sendMsgRequest.withQueueUrl(getQueueUrl(tpi.getFullTopicName())); | |
82 | + sendMsgRequest.withMessageBody(gson.toJson(new TbAwsSqsMsg(msg.getKey(), msg.getData()))); | |
83 | + | |
84 | + Map<String, MessageAttributeValue> attributes = new HashMap<>(); | |
85 | + | |
86 | + attributes.put("headers", new MessageAttributeValue() | |
87 | + .withStringValue(gson.toJson(msg.getHeaders().getData())) | |
88 | + .withDataType("String")); | |
89 | + | |
90 | + sendMsgRequest.withMessageAttributes(attributes); | |
91 | + sendMsgRequest.withMessageGroupId(msg.getKey().toString()); | |
92 | + ListenableFuture<SendMessageResult> future = producerExecutor.submit(() -> sqsClient.sendMessage(sendMsgRequest)); | |
93 | + | |
94 | + Futures.addCallback(future, new FutureCallback<SendMessageResult>() { | |
95 | + @Override | |
96 | + public void onSuccess(SendMessageResult result) { | |
97 | + if (callback != null) { | |
98 | + callback.onSuccess(new AwsSqsTbQueueMsgMetadata(result.getSdkHttpMetadata())); | |
99 | + } | |
100 | + } | |
101 | + | |
102 | + @Override | |
103 | + public void onFailure(Throwable t) { | |
104 | + if (callback != null) { | |
105 | + callback.onFailure(t); | |
106 | + } | |
107 | + } | |
108 | + }); | |
109 | + } | |
110 | + | |
111 | + @Override | |
112 | + public void stop() { | |
113 | + if (producerExecutor != null) { | |
114 | + producerExecutor.shutdownNow(); | |
115 | + } | |
116 | + if (sqsClient != null) { | |
117 | + sqsClient.shutdown(); | |
118 | + } | |
119 | + } | |
120 | + | |
121 | + private String getQueueUrl(String topic) { | |
122 | + return queueUrlMap.computeIfAbsent(topic, k -> { | |
123 | + admin.createTopicIfNotExists(topic); | |
124 | + return sqsClient.getQueueUrl(topic.replaceAll("\\.", "_") + ".fifo").getQueueUrl(); | |
125 | + }); | |
126 | + } | |
127 | +} | ... | ... |
1 | +/** | |
2 | + * Copyright © 2016-2020 The Thingsboard Authors | |
3 | + * | |
4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
5 | + * you may not use this file except in compliance with the License. | |
6 | + * You may obtain a copy of the License at | |
7 | + * | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | + * | |
10 | + * Unless required by applicable law or agreed to in writing, software | |
11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
13 | + * See the License for the specific language governing permissions and | |
14 | + * limitations under the License. | |
15 | + */ | |
16 | +package org.thingsboard.server.queue.sqs; | |
17 | + | |
18 | +import lombok.Data; | |
19 | +import lombok.extern.slf4j.Slf4j; | |
20 | +import org.springframework.beans.factory.annotation.Value; | |
21 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; | |
22 | +import org.springframework.stereotype.Component; | |
23 | + | |
24 | +@Slf4j | |
25 | +@ConditionalOnExpression("'${queue.type:null}'=='aws-sqs'") | |
26 | +@Component | |
27 | +@Data | |
28 | +public class TbAwsSqsSettings { | |
29 | + | |
30 | + @Value("${queue.aws_sqs.access_key_id}") | |
31 | + private String accessKeyId; | |
32 | + | |
33 | + @Value("${queue.aws_sqs.secret_access_key}") | |
34 | + private String secretAccessKey; | |
35 | + | |
36 | + @Value("${queue.aws_sqs.region}") | |
37 | + private String region; | |
38 | + | |
39 | + @Value("${queue.aws_sqs.threads_per_topic}") | |
40 | + private int threadsPerTopic; | |
41 | + | |
42 | + @Value("${queue.aws_sqs.visibility_timeout}") | |
43 | + private String visibilityTimeout; | |
44 | +} | ... | ... |
... | ... | @@ -138,6 +138,9 @@ public class DefaultTransportService implements TransportService { |
138 | 138 | while (!stopped) { |
139 | 139 | try { |
140 | 140 | List<TbProtoQueueMsg<ToTransportMsg>> records = transportNotificationsConsumer.poll(notificationsPollDuration); |
141 | + if (records.size() == 0) { | |
142 | + continue; | |
143 | + } | |
141 | 144 | records.forEach(record -> { |
142 | 145 | try { |
143 | 146 | ToTransportMsg toTransportMsg = record.getValue(); |
... | ... | @@ -170,6 +173,10 @@ public class DefaultTransportService implements TransportService { |
170 | 173 | perDeviceLimits.clear(); |
171 | 174 | } |
172 | 175 | stopped = true; |
176 | + | |
177 | + if (transportNotificationsConsumer != null) { | |
178 | + transportNotificationsConsumer.unsubscribe(); | |
179 | + } | |
173 | 180 | if (schedulerExecutor != null) { |
174 | 181 | schedulerExecutor.shutdownNow(); |
175 | 182 | } | ... | ... |
... | ... | @@ -92,6 +92,7 @@ |
92 | 92 | <fst.version>2.57</fst.version> |
93 | 93 | <antlr.version>2.7.7</antlr.version> |
94 | 94 | <snakeyaml.version>1.23</snakeyaml.version> |
95 | + <amazonaws.sqs.version>1.11.747</amazonaws.sqs.version> | |
95 | 96 | <passay.version>1.5.0</passay.version> |
96 | 97 | <ua-parser.version>1.4.3</ua-parser.version> |
97 | 98 | </properties> |
... | ... | @@ -887,6 +888,11 @@ |
887 | 888 | <version>${jts.version}</version> |
888 | 889 | </dependency> |
889 | 890 | <dependency> |
891 | + <groupId>com.amazonaws</groupId> | |
892 | + <artifactId>aws-java-sdk-sqs</artifactId> | |
893 | + <version>${amazonaws.sqs.version}</version> | |
894 | + </dependency> | |
895 | + <dependency> | |
890 | 896 | <groupId>org.passay</groupId> |
891 | 897 | <artifactId>passay</artifactId> |
892 | 898 | <version>${passay.version}</version> | ... | ... |
... | ... | @@ -35,7 +35,7 @@ |
35 | 35 | <properties> |
36 | 36 | <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> |
37 | 37 | <main.dir>${basedir}/../..</main.dir> |
38 | - <aws.sdk.version>1.11.323</aws.sdk.version> | |
38 | + <aws.sdk.version>1.11.747</aws.sdk.version> | |
39 | 39 | <pubsub.client.version>1.83.0</pubsub.client.version> |
40 | 40 | <google.common.protos.version>1.16.0</google.common.protos.version> |
41 | 41 | </properties> | ... | ... |
... | ... | @@ -63,7 +63,7 @@ transport: |
63 | 63 | max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" |
64 | 64 | |
65 | 65 | queue: |
66 | - type: "${TB_QUEUE_TYPE:kafka}" # kafka or ? | |
66 | + type: "${TB_QUEUE_TYPE:kafka}" # kafka or aws-sqs | |
67 | 67 | kafka: |
68 | 68 | bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}" |
69 | 69 | acks: "${TB_KAFKA_ACKS:all}" |
... | ... | @@ -71,6 +71,10 @@ queue: |
71 | 71 | batch.size: "${TB_KAFKA_BATCH_SIZE:16384}" |
72 | 72 | linger.ms: "${TB_KAFKA_LINGER_MS:1}" |
73 | 73 | buffer.memory: "${TB_BUFFER_MEMORY:33554432}" |
74 | + aws_sqs: | |
75 | + access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}" | |
76 | + secret_access_key: "${TB_QUEUE_AWS_SQS_SECRET_ACCESS_KEY:YOUR_SECRET}" | |
77 | + region: "${TB_QUEUE_AWS_SQS_REGION:YOUR_REGION}" | |
74 | 78 | partitions: |
75 | 79 | hash_function_name: "${TB_QUEUE_PARTITIONS_HASH_FUNCTION_NAME:murmur3_128}" |
76 | 80 | virtual_nodes_size: "${TB_QUEUE_PARTITIONS_VIRTUAL_NODES_SIZE:16}" | ... | ... |