Commit 3b5a4941ff6161acfc2fba95fc6a91af059231cc
1 parent
1da32793
Cassandra MsqQueue initial implementation
Showing
25 changed files
with
1268 additions
and
81 deletions
@@ -251,6 +251,9 @@ spring: | @@ -251,6 +251,9 @@ spring: | ||
251 | username: "${SPRING_DATASOURCE_USERNAME:sa}" | 251 | username: "${SPRING_DATASOURCE_USERNAME:sa}" |
252 | password: "${SPRING_DATASOURCE_PASSWORD:}" | 252 | password: "${SPRING_DATASOURCE_PASSWORD:}" |
253 | 253 | ||
254 | +rule: | ||
255 | + queue: | ||
256 | + msg_partitioning: "${QUEUE_MSG_PARTITIONING:HOURS}" | ||
254 | 257 | ||
255 | # PostgreSQL DAO Configuration | 258 | # PostgreSQL DAO Configuration |
256 | #spring: | 259 | #spring: |
@@ -555,48 +555,45 @@ CREATE TABLE IF NOT EXISTS thingsboard.msg_queue ( | @@ -555,48 +555,45 @@ CREATE TABLE IF NOT EXISTS thingsboard.msg_queue ( | ||
555 | partition bigint, | 555 | partition bigint, |
556 | ts bigint, | 556 | ts bigint, |
557 | msg blob, | 557 | msg blob, |
558 | - PRIMARY KEY ((node_id, cluster_hash, partition), ts) | ||
559 | - WITH CLUSTERING ORDER BY (ts DESC) | ||
560 | - AND compaction = { | ||
561 | - 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
562 | - 'min_threshold': '5', | ||
563 | - 'base_time_seconds': '43200', | ||
564 | - 'max_window_size_seconds': '43200' | ||
565 | - 'tombstone_threshold': '0.9', | ||
566 | - 'unchecked_tombstone_compaction': 'true', | ||
567 | - }; | ||
568 | -); | 558 | + PRIMARY KEY ((node_id, clustered_hash, partition), ts)) |
559 | +WITH CLUSTERING ORDER BY (ts DESC) | ||
560 | +AND compaction = { | ||
561 | + 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
562 | + 'min_threshold': '5', | ||
563 | + 'base_time_seconds': '43200', | ||
564 | + 'max_window_size_seconds': '43200', | ||
565 | + 'tombstone_threshold': '0.9', | ||
566 | + 'unchecked_tombstone_compaction': 'true' | ||
567 | +}; | ||
568 | + | ||
569 | 569 | ||
570 | CREATE TABLE IF NOT EXISTS thingsboard.msg_ack_queue ( | 570 | CREATE TABLE IF NOT EXISTS thingsboard.msg_ack_queue ( |
571 | node_id timeuuid, | 571 | node_id timeuuid, |
572 | clustered_hash bigint, | 572 | clustered_hash bigint, |
573 | partition bigint, | 573 | partition bigint, |
574 | - ts bigint, | ||
575 | msg_id timeuuid, | 574 | msg_id timeuuid, |
576 | - PRIMARY KEY ((node_id, cluster_hash, partition), ts) | ||
577 | - WITH CLUSTERING ORDER BY (ts DESC) | ||
578 | - AND compaction = { | ||
579 | - 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
580 | - 'min_threshold': '5', | ||
581 | - 'base_time_seconds': '43200', | ||
582 | - 'max_window_size_seconds': '43200' | ||
583 | - 'tombstone_threshold': '0.9', | ||
584 | - 'unchecked_tombstone_compaction': 'true', | ||
585 | - }; | ||
586 | -); | 575 | + PRIMARY KEY ((node_id, clustered_hash, partition), msg_id)) |
576 | +WITH CLUSTERING ORDER BY (msg_id DESC) | ||
577 | +AND compaction = { | ||
578 | + 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
579 | + 'min_threshold': '5', | ||
580 | + 'base_time_seconds': '43200', | ||
581 | + 'max_window_size_seconds': '43200', | ||
582 | + 'tombstone_threshold': '0.9', | ||
583 | + 'unchecked_tombstone_compaction': 'true' | ||
584 | +}; | ||
587 | 585 | ||
588 | CREATE TABLE IF NOT EXISTS thingsboard.processed_msg_partitions ( | 586 | CREATE TABLE IF NOT EXISTS thingsboard.processed_msg_partitions ( |
589 | node_id timeuuid, | 587 | node_id timeuuid, |
590 | clustered_hash bigint, | 588 | clustered_hash bigint, |
591 | partition bigint, | 589 | partition bigint, |
592 | - PRIMARY KEY ((node_id, cluster_hash), partition) | ||
593 | - WITH CLUSTERING ORDER BY (partition DESC) | ||
594 | - AND compaction = { | ||
595 | - 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
596 | - 'min_threshold': '5', | ||
597 | - 'base_time_seconds': '43200', | ||
598 | - 'max_window_size_seconds': '43200' | ||
599 | - 'tombstone_threshold': '0.9', | ||
600 | - 'unchecked_tombstone_compaction': 'true', | ||
601 | - }; | ||
602 | -); | ||
590 | + PRIMARY KEY ((node_id, clustered_hash), partition)) | ||
591 | +WITH CLUSTERING ORDER BY (partition DESC) | ||
592 | +AND compaction = { | ||
593 | + 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
594 | + 'min_threshold': '5', | ||
595 | + 'base_time_seconds': '43200', | ||
596 | + 'max_window_size_seconds': '43200', | ||
597 | + 'tombstone_threshold': '0.9', | ||
598 | + 'unchecked_tombstone_compaction': 'true' | ||
599 | +}; |
@@ -19,6 +19,7 @@ import lombok.Data; | @@ -19,6 +19,7 @@ import lombok.Data; | ||
19 | 19 | ||
20 | import java.io.Serializable; | 20 | import java.io.Serializable; |
21 | import java.util.Map; | 21 | import java.util.Map; |
22 | +import java.util.concurrent.ConcurrentHashMap; | ||
22 | 23 | ||
23 | /** | 24 | /** |
24 | * Created by ashvayka on 13.01.18. | 25 | * Created by ashvayka on 13.01.18. |
@@ -26,7 +27,7 @@ import java.util.Map; | @@ -26,7 +27,7 @@ import java.util.Map; | ||
26 | @Data | 27 | @Data |
27 | public final class TbMsgMetaData implements Serializable { | 28 | public final class TbMsgMetaData implements Serializable { |
28 | 29 | ||
29 | - private Map<String, String> data; | 30 | + private Map<String, String> data = new ConcurrentHashMap<>(); |
30 | 31 | ||
31 | public String getValue(String key) { | 32 | public String getValue(String key) { |
32 | return data.get(key); | 33 | return data.get(key); |
@@ -63,5 +63,107 @@ | @@ -63,5 +63,107 @@ | ||
63 | <artifactId>rule-engine-api</artifactId> | 63 | <artifactId>rule-engine-api</artifactId> |
64 | <version>1.4.0-SNAPSHOT</version> | 64 | <version>1.4.0-SNAPSHOT</version> |
65 | </dependency> | 65 | </dependency> |
66 | + <dependency> | ||
67 | + <groupId>com.google.protobuf</groupId> | ||
68 | + <artifactId>protobuf-java</artifactId> | ||
69 | + <scope>provided</scope> | ||
70 | + </dependency> | ||
71 | + <dependency> | ||
72 | + <groupId>com.google.guava</groupId> | ||
73 | + <artifactId>guava</artifactId> | ||
74 | + </dependency> | ||
75 | + <dependency> | ||
76 | + <groupId>com.datastax.cassandra</groupId> | ||
77 | + <artifactId>cassandra-driver-core</artifactId> | ||
78 | + </dependency> | ||
79 | + <dependency> | ||
80 | + <groupId>com.datastax.cassandra</groupId> | ||
81 | + <artifactId>cassandra-driver-mapping</artifactId> | ||
82 | + </dependency> | ||
83 | + <dependency> | ||
84 | + <groupId>com.datastax.cassandra</groupId> | ||
85 | + <artifactId>cassandra-driver-extras</artifactId> | ||
86 | + </dependency> | ||
87 | + | ||
88 | + <dependency> | ||
89 | + <groupId>junit</groupId> | ||
90 | + <artifactId>junit</artifactId> | ||
91 | + <version>${junit.version}</version> | ||
92 | + <scope>test</scope> | ||
93 | + </dependency> | ||
94 | + <dependency> | ||
95 | + <groupId>org.cassandraunit</groupId> | ||
96 | + <artifactId>cassandra-unit</artifactId> | ||
97 | + <exclusions> | ||
98 | + <exclusion> | ||
99 | + <groupId>org.slf4j</groupId> | ||
100 | + <artifactId>slf4j-log4j12</artifactId> | ||
101 | + </exclusion> | ||
102 | + </exclusions> | ||
103 | + <scope>test</scope> | ||
104 | + </dependency> | ||
105 | + <dependency> | ||
106 | + <groupId>org.mockito</groupId> | ||
107 | + <artifactId>mockito-all</artifactId> | ||
108 | + <scope>test</scope> | ||
109 | + </dependency> | ||
110 | + <dependency> | ||
111 | + <groupId>org.junit.jupiter</groupId> | ||
112 | + <artifactId>junit-jupiter-api</artifactId> | ||
113 | + <version>RELEASE</version> | ||
114 | + </dependency> | ||
115 | + | ||
116 | + <!--<dependency>--> | ||
117 | + <!--<groupId>org.springframework.boot</groupId>--> | ||
118 | + <!--<artifactId>spring-boot-starter-web</artifactId>--> | ||
119 | + <!--</dependency>--> | ||
120 | + | ||
66 | </dependencies> | 121 | </dependencies> |
122 | + | ||
123 | + <build> | ||
124 | + <plugins> | ||
125 | + <plugin> | ||
126 | + <groupId>org.apache.maven.plugins</groupId> | ||
127 | + <artifactId>maven-dependency-plugin</artifactId> | ||
128 | + </plugin> | ||
129 | + <plugin> | ||
130 | + <groupId>org.xolstice.maven.plugins</groupId> | ||
131 | + <artifactId>protobuf-maven-plugin</artifactId> | ||
132 | + </plugin> | ||
133 | + <plugin> | ||
134 | + <groupId>org.codehaus.mojo</groupId> | ||
135 | + <artifactId>build-helper-maven-plugin</artifactId> | ||
136 | + </plugin> | ||
137 | + | ||
138 | + | ||
139 | + | ||
140 | + | ||
141 | + <plugin> | ||
142 | + <groupId>org.springframework.boot</groupId> | ||
143 | + <artifactId>spring-boot-maven-plugin</artifactId> | ||
144 | + <configuration> | ||
145 | + <mainClass>org.thingsboard.rule.engine.tool.QueueBenchmark</mainClass> | ||
146 | + <classifier>boot</classifier> | ||
147 | + <layout>ZIP</layout> | ||
148 | + <executable>true</executable> | ||
149 | + <excludeDevtools>true</excludeDevtools> | ||
150 | + <!--<embeddedLaunchScriptProperties>--> | ||
151 | + <!--<confFolder>${pkg.installFolder}/conf</confFolder>--> | ||
152 | + <!--<logFolder>${pkg.unixLogFolder}</logFolder>--> | ||
153 | + <!--<logFilename>${pkg.name}.out</logFilename>--> | ||
154 | + <!--<initInfoProvides>${pkg.name}</initInfoProvides>--> | ||
155 | + <!--</embeddedLaunchScriptProperties>--> | ||
156 | + </configuration> | ||
157 | + <executions> | ||
158 | + <execution> | ||
159 | + <goals> | ||
160 | + <goal>repackage</goal> | ||
161 | + </goals> | ||
162 | + </execution> | ||
163 | + </executions> | ||
164 | + </plugin> | ||
165 | + | ||
166 | + </plugins> | ||
167 | + </build> | ||
168 | + | ||
67 | </project> | 169 | </project> |
@@ -15,68 +15,65 @@ | @@ -15,68 +15,65 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.rule.engine.queue.cassandra; | 16 | package org.thingsboard.rule.engine.queue.cassandra; |
17 | 17 | ||
18 | +import com.datastax.driver.core.utils.UUIDs; | ||
18 | import com.google.common.collect.Lists; | 19 | import com.google.common.collect.Lists; |
19 | import com.google.common.util.concurrent.ListenableFuture; | 20 | import com.google.common.util.concurrent.ListenableFuture; |
20 | -import org.springframework.beans.factory.annotation.Autowired; | 21 | +import lombok.extern.slf4j.Slf4j; |
21 | import org.springframework.stereotype.Component; | 22 | import org.springframework.stereotype.Component; |
22 | import org.thingsboard.rule.engine.api.MsqQueue; | 23 | import org.thingsboard.rule.engine.api.MsqQueue; |
23 | import org.thingsboard.rule.engine.api.TbMsg; | 24 | import org.thingsboard.rule.engine.api.TbMsg; |
24 | import org.thingsboard.rule.engine.queue.cassandra.repository.AckRepository; | 25 | import org.thingsboard.rule.engine.queue.cassandra.repository.AckRepository; |
25 | import org.thingsboard.rule.engine.queue.cassandra.repository.MsgRepository; | 26 | import org.thingsboard.rule.engine.queue.cassandra.repository.MsgRepository; |
26 | -import org.thingsboard.rule.engine.queue.cassandra.repository.ProcessedPartitionRepository; | 27 | +import org.thingsboard.server.common.data.UUIDConverter; |
27 | 28 | ||
28 | -import java.util.Collections; | ||
29 | import java.util.List; | 29 | import java.util.List; |
30 | -import java.util.Optional; | ||
31 | import java.util.UUID; | 30 | import java.util.UUID; |
32 | 31 | ||
33 | @Component | 32 | @Component |
33 | +@Slf4j | ||
34 | public class CassandraMsqQueue implements MsqQueue { | 34 | public class CassandraMsqQueue implements MsqQueue { |
35 | 35 | ||
36 | - @Autowired | ||
37 | - private MsgRepository msgRepository; | 36 | + private final MsgRepository msgRepository; |
37 | + private final AckRepository ackRepository; | ||
38 | + private final UnprocessedMsgFilter unprocessedMsgFilter; | ||
39 | + private final QueuePartitioner queuePartitioner; | ||
38 | 40 | ||
39 | - @Autowired | ||
40 | - private AckRepository ackRepository; | ||
41 | - | ||
42 | - @Autowired | ||
43 | - private AckBuilder ackBuilder; | ||
44 | - | ||
45 | - @Autowired | ||
46 | - private UnprocessedMsgFilter unprocessedMsgFilter; | 41 | + public CassandraMsqQueue(MsgRepository msgRepository, AckRepository ackRepository, |
42 | + UnprocessedMsgFilter unprocessedMsgFilter, QueuePartitioner queuePartitioner) { | ||
43 | + this.msgRepository = msgRepository; | ||
44 | + this.ackRepository = ackRepository; | ||
45 | + this.unprocessedMsgFilter = unprocessedMsgFilter; | ||
46 | + this.queuePartitioner = queuePartitioner; | ||
47 | + } | ||
47 | 48 | ||
48 | - @Autowired | ||
49 | - private ProcessedPartitionRepository processedPartitionRepository; | ||
50 | 49 | ||
51 | @Override | 50 | @Override |
52 | public ListenableFuture<Void> put(TbMsg msg, UUID nodeId, long clusteredHash) { | 51 | public ListenableFuture<Void> put(TbMsg msg, UUID nodeId, long clusteredHash) { |
53 | - return msgRepository.save(msg, nodeId, clusteredHash, getPartition(msg)); | 52 | + long msgTime = getMsgTime(msg); |
53 | + long partition = queuePartitioner.getPartition(msgTime); | ||
54 | + return msgRepository.save(msg, nodeId, clusteredHash, partition, msgTime); | ||
54 | } | 55 | } |
55 | 56 | ||
56 | @Override | 57 | @Override |
57 | public ListenableFuture<Void> ack(TbMsg msg, UUID nodeId, long clusteredHash) { | 58 | public ListenableFuture<Void> ack(TbMsg msg, UUID nodeId, long clusteredHash) { |
58 | - MsgAck ack = ackBuilder.build(msg, nodeId, clusteredHash); | 59 | + long partition = queuePartitioner.getPartition(getMsgTime(msg)); |
60 | + MsgAck ack = new MsgAck(msg.getId(), nodeId, clusteredHash, partition); | ||
59 | return ackRepository.ack(ack); | 61 | return ackRepository.ack(ack); |
60 | } | 62 | } |
61 | 63 | ||
62 | @Override | 64 | @Override |
63 | public Iterable<TbMsg> findUnprocessed(UUID nodeId, long clusteredHash) { | 65 | public Iterable<TbMsg> findUnprocessed(UUID nodeId, long clusteredHash) { |
64 | List<TbMsg> unprocessedMsgs = Lists.newArrayList(); | 66 | List<TbMsg> unprocessedMsgs = Lists.newArrayList(); |
65 | - for (Long partition : findUnprocessedPartitions(nodeId, clusteredHash)) { | ||
66 | - Iterable<TbMsg> msgs = msgRepository.findMsgs(nodeId, clusteredHash, partition); | ||
67 | - Iterable<MsgAck> acks = ackRepository.findAcks(nodeId, clusteredHash, partition); | 67 | + for (Long partition : queuePartitioner.findUnprocessedPartitions(nodeId, clusteredHash)) { |
68 | + List<TbMsg> msgs = msgRepository.findMsgs(nodeId, clusteredHash, partition); | ||
69 | + List<MsgAck> acks = ackRepository.findAcks(nodeId, clusteredHash, partition); | ||
68 | unprocessedMsgs.addAll(unprocessedMsgFilter.filter(msgs, acks)); | 70 | unprocessedMsgs.addAll(unprocessedMsgFilter.filter(msgs, acks)); |
69 | } | 71 | } |
70 | return unprocessedMsgs; | 72 | return unprocessedMsgs; |
71 | } | 73 | } |
72 | 74 | ||
73 | - private List<Long> findUnprocessedPartitions(UUID nodeId, long clusteredHash) { | ||
74 | - Optional<Long> lastPartition = processedPartitionRepository.findLastProcessedPartition(nodeId, clusteredHash); | ||
75 | - return Collections.emptyList(); | ||
76 | - } | ||
77 | - | ||
78 | - private long getPartition(TbMsg msg) { | ||
79 | - return Long.MIN_VALUE; | 75 | + private long getMsgTime(TbMsg msg) { |
76 | + return UUIDs.unixTimestamp(msg.getId()); | ||
80 | } | 77 | } |
81 | 78 | ||
82 | } | 79 | } |
@@ -15,17 +15,21 @@ | @@ -15,17 +15,21 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.rule.engine.queue.cassandra; | 16 | package org.thingsboard.rule.engine.queue.cassandra; |
17 | 17 | ||
18 | +import com.datastax.driver.core.utils.UUIDs; | ||
18 | import lombok.Data; | 19 | import lombok.Data; |
20 | +import lombok.EqualsAndHashCode; | ||
21 | +import org.thingsboard.rule.engine.api.TbMsg; | ||
22 | +import org.thingsboard.server.common.data.UUIDConverter; | ||
19 | 23 | ||
20 | import java.util.UUID; | 24 | import java.util.UUID; |
21 | 25 | ||
22 | @Data | 26 | @Data |
27 | +@EqualsAndHashCode | ||
23 | public class MsgAck { | 28 | public class MsgAck { |
24 | 29 | ||
25 | private final UUID msgId; | 30 | private final UUID msgId; |
26 | private final UUID nodeId; | 31 | private final UUID nodeId; |
27 | private final long clusteredHash; | 32 | private final long clusteredHash; |
28 | private final long partition; | 33 | private final long partition; |
29 | - private final long ts; | ||
30 | 34 | ||
31 | } | 35 | } |
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * <p> | ||
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 | + * <p> | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * <p> | ||
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.rule.engine.queue.cassandra; | ||
17 | + | ||
18 | +import com.google.common.collect.Lists; | ||
19 | +import lombok.extern.slf4j.Slf4j; | ||
20 | +import org.springframework.beans.factory.annotation.Value; | ||
21 | +import org.springframework.stereotype.Component; | ||
22 | +import org.thingsboard.rule.engine.queue.cassandra.repository.ProcessedPartitionRepository; | ||
23 | +import org.thingsboard.server.dao.timeseries.TsPartitionDate; | ||
24 | + | ||
25 | +import java.time.Clock; | ||
26 | +import java.time.Instant; | ||
27 | +import java.time.LocalDateTime; | ||
28 | +import java.time.ZoneOffset; | ||
29 | +import java.util.List; | ||
30 | +import java.util.Optional; | ||
31 | +import java.util.UUID; | ||
32 | + | ||
33 | +@Component | ||
34 | +@Slf4j | ||
35 | +public class QueuePartitioner { | ||
36 | + | ||
37 | + private ProcessedPartitionRepository processedPartitionRepository; | ||
38 | + | ||
39 | + private final TsPartitionDate tsFormat; | ||
40 | + private Clock clock = Clock.systemUTC(); | ||
41 | + | ||
42 | + public QueuePartitioner(@Value("${rule.queue.msg_partitioning}") String partitioning, | ||
43 | + ProcessedPartitionRepository processedPartitionRepository) { | ||
44 | + this.processedPartitionRepository = processedPartitionRepository; | ||
45 | + Optional<TsPartitionDate> partition = TsPartitionDate.parse(partitioning); | ||
46 | + if (partition.isPresent()) { | ||
47 | + tsFormat = partition.get(); | ||
48 | + } else { | ||
49 | + log.warn("Incorrect configuration of partitioning {}", "MINUTES"); | ||
50 | + throw new RuntimeException("Failed to parse partitioning property: " + "MINUTES" + "!"); | ||
51 | + } | ||
52 | + } | ||
53 | + | ||
54 | + public long getPartition(long ts) { | ||
55 | + LocalDateTime time = LocalDateTime.ofInstant(Instant.ofEpochMilli(ts), ZoneOffset.UTC); | ||
56 | + return tsFormat.truncatedTo(time).toInstant(ZoneOffset.UTC).toEpochMilli(); | ||
57 | + } | ||
58 | + | ||
59 | + public List<Long> findUnprocessedPartitions(UUID nodeId, long clusteredHash) { | ||
60 | + Optional<Long> lastPartitionOption = processedPartitionRepository.findLastProcessedPartition(nodeId, clusteredHash); | ||
61 | + long lastPartition = lastPartitionOption.orElse(System.currentTimeMillis() - 7 * 24 * 60 * 60 * 100); | ||
62 | + List<Long> unprocessedPartitions = Lists.newArrayList(); | ||
63 | + | ||
64 | + LocalDateTime current = LocalDateTime.ofInstant(Instant.ofEpochMilli(lastPartition), ZoneOffset.UTC); | ||
65 | + LocalDateTime end = LocalDateTime.ofInstant(Instant.now(clock), ZoneOffset.UTC) | ||
66 | + .plus(1L, tsFormat.getTruncateUnit()); | ||
67 | + | ||
68 | + while (current.isBefore(end)) { | ||
69 | + current = current.plus(1L, tsFormat.getTruncateUnit()); | ||
70 | + unprocessedPartitions.add(tsFormat.truncatedTo(current).toInstant(ZoneOffset.UTC).toEpochMilli()); | ||
71 | + } | ||
72 | + | ||
73 | + return unprocessedPartitions; | ||
74 | + } | ||
75 | + | ||
76 | + public void setClock(Clock clock) { | ||
77 | + this.clock = clock; | ||
78 | + } | ||
79 | + | ||
80 | + public void checkProcessedPartitions() { | ||
81 | + //todo-vp: we need to implement this | ||
82 | + } | ||
83 | +} |
@@ -15,14 +15,20 @@ | @@ -15,14 +15,20 @@ | ||
15 | */ | 15 | */ |
16 | package org.thingsboard.rule.engine.queue.cassandra; | 16 | package org.thingsboard.rule.engine.queue.cassandra; |
17 | 17 | ||
18 | +import org.springframework.stereotype.Component; | ||
18 | import org.thingsboard.rule.engine.api.TbMsg; | 19 | import org.thingsboard.rule.engine.api.TbMsg; |
19 | 20 | ||
20 | import java.util.Collection; | 21 | import java.util.Collection; |
21 | -import java.util.Collections; | 22 | +import java.util.List; |
23 | +import java.util.Set; | ||
24 | +import java.util.UUID; | ||
25 | +import java.util.stream.Collectors; | ||
22 | 26 | ||
27 | +@Component | ||
23 | public class UnprocessedMsgFilter { | 28 | public class UnprocessedMsgFilter { |
24 | 29 | ||
25 | - public Collection<TbMsg> filter(Iterable<TbMsg> msgs, Iterable<MsgAck> acks) { | ||
26 | - return Collections.emptyList(); | 30 | + public Collection<TbMsg> filter(List<TbMsg> msgs, List<MsgAck> acks) { |
31 | + Set<UUID> processedIds = acks.stream().map(MsgAck::getMsgId).collect(Collectors.toSet()); | ||
32 | + return msgs.stream().filter(i -> !processedIds.contains(i.getId())).collect(Collectors.toList()); | ||
27 | } | 33 | } |
28 | } | 34 | } |
@@ -18,11 +18,12 @@ package org.thingsboard.rule.engine.queue.cassandra.repository; | @@ -18,11 +18,12 @@ package org.thingsboard.rule.engine.queue.cassandra.repository; | ||
18 | import com.google.common.util.concurrent.ListenableFuture; | 18 | import com.google.common.util.concurrent.ListenableFuture; |
19 | import org.thingsboard.rule.engine.queue.cassandra.MsgAck; | 19 | import org.thingsboard.rule.engine.queue.cassandra.MsgAck; |
20 | 20 | ||
21 | +import java.util.List; | ||
21 | import java.util.UUID; | 22 | import java.util.UUID; |
22 | 23 | ||
23 | public interface AckRepository { | 24 | public interface AckRepository { |
24 | 25 | ||
25 | ListenableFuture<Void> ack(MsgAck msgAck); | 26 | ListenableFuture<Void> ack(MsgAck msgAck); |
26 | 27 | ||
27 | - Iterable<MsgAck> findAcks(UUID nodeId, long clusteredHash, long partition); | 28 | + List<MsgAck> findAcks(UUID nodeId, long clusteredHash, long partition); |
28 | } | 29 | } |
@@ -18,12 +18,13 @@ package org.thingsboard.rule.engine.queue.cassandra.repository; | @@ -18,12 +18,13 @@ package org.thingsboard.rule.engine.queue.cassandra.repository; | ||
18 | import com.google.common.util.concurrent.ListenableFuture; | 18 | import com.google.common.util.concurrent.ListenableFuture; |
19 | import org.thingsboard.rule.engine.api.TbMsg; | 19 | import org.thingsboard.rule.engine.api.TbMsg; |
20 | 20 | ||
21 | +import java.util.List; | ||
21 | import java.util.UUID; | 22 | import java.util.UUID; |
22 | 23 | ||
23 | public interface MsgRepository { | 24 | public interface MsgRepository { |
24 | 25 | ||
25 | - ListenableFuture<Void> save(TbMsg msg, UUID nodeId, long clusteredHash, long partition); | 26 | + ListenableFuture<Void> save(TbMsg msg, UUID nodeId, long clusteredHash, long partition, long msgTs); |
26 | 27 | ||
27 | - Iterable<TbMsg> findMsgs(UUID nodeId, long clusteredHash, long partition); | 28 | + List<TbMsg> findMsgs(UUID nodeId, long clusteredHash, long partition); |
28 | 29 | ||
29 | } | 30 | } |
1 | +/** | ||
2 | + * Copyright © 2016-2017 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 | + */ | ||
1 | package org.thingsboard.rule.engine.queue.cassandra.repository; | 16 | package org.thingsboard.rule.engine.queue.cassandra.repository; |
2 | 17 | ||
18 | +import com.google.common.util.concurrent.ListenableFuture; | ||
19 | + | ||
3 | import java.util.Optional; | 20 | import java.util.Optional; |
4 | import java.util.UUID; | 21 | import java.util.UUID; |
5 | 22 | ||
6 | public interface ProcessedPartitionRepository { | 23 | public interface ProcessedPartitionRepository { |
7 | 24 | ||
8 | - void partitionProcessed(UUID nodeId, long clusteredHash, long partition); | 25 | + ListenableFuture<Void> partitionProcessed(UUID nodeId, long clusteredHash, long partition); |
9 | 26 | ||
10 | Optional<Long> findLastProcessedPartition(UUID nodeId, long clusteredHash); | 27 | Optional<Long> findLastProcessedPartition(UUID nodeId, long clusteredHash); |
11 | 28 |
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * <p> | ||
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 | + * <p> | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * <p> | ||
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.rule.engine.queue.cassandra.repository.impl; | ||
17 | + | ||
18 | +import com.datastax.driver.core.*; | ||
19 | +import com.google.common.base.Function; | ||
20 | +import com.google.common.util.concurrent.Futures; | ||
21 | +import com.google.common.util.concurrent.ListenableFuture; | ||
22 | +import org.springframework.stereotype.Component; | ||
23 | +import org.thingsboard.rule.engine.queue.cassandra.MsgAck; | ||
24 | +import org.thingsboard.rule.engine.queue.cassandra.repository.AckRepository; | ||
25 | + | ||
26 | +import java.util.ArrayList; | ||
27 | +import java.util.List; | ||
28 | +import java.util.UUID; | ||
29 | + | ||
30 | +@Component | ||
31 | +public class CassandraAckRepository extends SimpleAbstractCassandraDao implements AckRepository { | ||
32 | + | ||
33 | + private final int ackQueueTtl; | ||
34 | + | ||
35 | + public CassandraAckRepository(Session session, int ackQueueTtl) { | ||
36 | + super(session); | ||
37 | + this.ackQueueTtl = ackQueueTtl; | ||
38 | + } | ||
39 | + | ||
40 | + @Override | ||
41 | + public ListenableFuture<Void> ack(MsgAck msgAck) { | ||
42 | + String insert = "INSERT INTO msg_ack_queue (node_id, clustered_hash, partition, msg_id) VALUES (?, ?, ?, ?) USING TTL ?"; | ||
43 | + PreparedStatement statement = prepare(insert); | ||
44 | + BoundStatement boundStatement = statement.bind(msgAck.getNodeId(), msgAck.getClusteredHash(), | ||
45 | + msgAck.getPartition(), msgAck.getMsgId(), ackQueueTtl); | ||
46 | + ResultSetFuture resultSetFuture = executeAsyncWrite(boundStatement); | ||
47 | + return Futures.transform(resultSetFuture, (Function<ResultSet, Void>) input -> null); | ||
48 | + } | ||
49 | + | ||
50 | + @Override | ||
51 | + public List<MsgAck> findAcks(UUID nodeId, long clusteredHash, long partition) { | ||
52 | + String select = "SELECT msg_id FROM msg_ack_queue WHERE " + | ||
53 | + "node_id = ? AND clustered_hash = ? AND partition = ?"; | ||
54 | + PreparedStatement statement = prepare(select); | ||
55 | + BoundStatement boundStatement = statement.bind(nodeId, clusteredHash, partition); | ||
56 | + ResultSet rows = executeRead(boundStatement); | ||
57 | + List<MsgAck> msgs = new ArrayList<>(); | ||
58 | + for (Row row : rows) { | ||
59 | + msgs.add(new MsgAck(row.getUUID("msg_id"), nodeId, clusteredHash, partition)); | ||
60 | + } | ||
61 | + return msgs; | ||
62 | + } | ||
63 | + | ||
64 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * <p> | ||
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 | + * <p> | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * <p> | ||
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.rule.engine.queue.cassandra.repository.impl; | ||
17 | + | ||
18 | +import com.datastax.driver.core.*; | ||
19 | +import com.google.common.base.Function; | ||
20 | +import com.google.common.util.concurrent.Futures; | ||
21 | +import com.google.common.util.concurrent.ListenableFuture; | ||
22 | +import com.google.protobuf.ByteString; | ||
23 | +import com.google.protobuf.InvalidProtocolBufferException; | ||
24 | +import org.springframework.stereotype.Component; | ||
25 | +import org.thingsboard.rule.engine.api.TbMsg; | ||
26 | +import org.thingsboard.rule.engine.api.TbMsgMetaData; | ||
27 | +import org.thingsboard.rule.engine.queue.cassandra.repository.MsgRepository; | ||
28 | +import org.thingsboard.rule.engine.queue.cassandra.repository.gen.MsgQueueProtos; | ||
29 | +import org.thingsboard.server.common.data.id.EntityId; | ||
30 | +import org.thingsboard.server.common.data.id.EntityIdFactory; | ||
31 | + | ||
32 | +import java.nio.ByteBuffer; | ||
33 | +import java.util.ArrayList; | ||
34 | +import java.util.List; | ||
35 | +import java.util.UUID; | ||
36 | + | ||
37 | +@Component | ||
38 | +public class CassandraMsgRepository extends SimpleAbstractCassandraDao implements MsgRepository { | ||
39 | + | ||
40 | + private final int msqQueueTtl; | ||
41 | + | ||
42 | + | ||
43 | + public CassandraMsgRepository(Session session, int msqQueueTtl) { | ||
44 | + super(session); | ||
45 | + this.msqQueueTtl = msqQueueTtl; | ||
46 | + } | ||
47 | + | ||
48 | + @Override | ||
49 | + public ListenableFuture<Void> save(TbMsg msg, UUID nodeId, long clusteredHash, long partition, long msgTs) { | ||
50 | + String insert = "INSERT INTO msg_queue (node_id, clustered_hash, partition, ts, msg) VALUES (?, ?, ?, ?, ?) USING TTL ?"; | ||
51 | + PreparedStatement statement = prepare(insert); | ||
52 | + BoundStatement boundStatement = statement.bind(nodeId, clusteredHash, partition, msgTs, toBytes(msg), msqQueueTtl); | ||
53 | + ResultSetFuture resultSetFuture = executeAsyncWrite(boundStatement); | ||
54 | + return Futures.transform(resultSetFuture, (Function<ResultSet, Void>) input -> null); | ||
55 | + } | ||
56 | + | ||
57 | + @Override | ||
58 | + public List<TbMsg> findMsgs(UUID nodeId, long clusteredHash, long partition) { | ||
59 | + String select = "SELECT node_id, clustered_hash, partition, ts, msg FROM msg_queue WHERE " + | ||
60 | + "node_id = ? AND clustered_hash = ? AND partition = ?"; | ||
61 | + PreparedStatement statement = prepare(select); | ||
62 | + BoundStatement boundStatement = statement.bind(nodeId, clusteredHash, partition); | ||
63 | + ResultSet rows = executeRead(boundStatement); | ||
64 | + List<TbMsg> msgs = new ArrayList<>(); | ||
65 | + for (Row row : rows) { | ||
66 | + msgs.add(fromBytes(row.getBytes("msg"))); | ||
67 | + } | ||
68 | + return msgs; | ||
69 | + } | ||
70 | + | ||
71 | + private ByteBuffer toBytes(TbMsg msg) { | ||
72 | + MsgQueueProtos.TbMsgProto.Builder builder = MsgQueueProtos.TbMsgProto.newBuilder(); | ||
73 | + builder.setId(msg.getId().toString()); | ||
74 | + builder.setType(msg.getType()); | ||
75 | + if (msg.getOriginator() != null) { | ||
76 | + builder.setEntityType(msg.getOriginator().getEntityType().name()); | ||
77 | + builder.setEntityId(msg.getOriginator().getId().toString()); | ||
78 | + } | ||
79 | + | ||
80 | + if (msg.getMetaData() != null) { | ||
81 | + MsgQueueProtos.TbMsgProto.TbMsgMetaDataProto.Builder metadataBuilder = MsgQueueProtos.TbMsgProto.TbMsgMetaDataProto.newBuilder(); | ||
82 | + metadataBuilder.putAllData(msg.getMetaData().getData()); | ||
83 | + builder.addMetaData(metadataBuilder.build()); | ||
84 | + } | ||
85 | + | ||
86 | + builder.setData(ByteString.copyFrom(msg.getData())); | ||
87 | + byte[] bytes = builder.build().toByteArray(); | ||
88 | + return ByteBuffer.wrap(bytes); | ||
89 | + } | ||
90 | + | ||
91 | + private TbMsg fromBytes(ByteBuffer buffer) { | ||
92 | + try { | ||
93 | + MsgQueueProtos.TbMsgProto proto = MsgQueueProtos.TbMsgProto.parseFrom(buffer.array()); | ||
94 | + TbMsgMetaData metaData = new TbMsgMetaData(); | ||
95 | + if (proto.getMetaDataCount() > 0) { | ||
96 | + metaData.setData(proto.getMetaData(0).getDataMap()); | ||
97 | + } | ||
98 | + | ||
99 | + EntityId entityId = null; | ||
100 | + if (proto.getEntityId() != null) { | ||
101 | + entityId = EntityIdFactory.getByTypeAndId(proto.getEntityType(), proto.getEntityId()); | ||
102 | + } | ||
103 | + | ||
104 | + return new TbMsg(UUID.fromString(proto.getId()), proto.getType(), entityId, metaData, proto.getData().toByteArray()); | ||
105 | + } catch (InvalidProtocolBufferException e) { | ||
106 | + throw new IllegalStateException("Could not parse protobuf for TbMsg", e); | ||
107 | + } | ||
108 | + } | ||
109 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * <p> | ||
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 | + * <p> | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * <p> | ||
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.rule.engine.queue.cassandra.repository.impl; | ||
17 | + | ||
18 | +import com.datastax.driver.core.*; | ||
19 | +import com.google.common.base.Function; | ||
20 | +import com.google.common.util.concurrent.Futures; | ||
21 | +import com.google.common.util.concurrent.ListenableFuture; | ||
22 | +import org.springframework.stereotype.Component; | ||
23 | +import org.thingsboard.rule.engine.queue.cassandra.repository.ProcessedPartitionRepository; | ||
24 | + | ||
25 | +import java.util.Optional; | ||
26 | +import java.util.UUID; | ||
27 | + | ||
28 | +@Component | ||
29 | +public class CassandraProcessedPartitionRepository extends SimpleAbstractCassandraDao implements ProcessedPartitionRepository { | ||
30 | + | ||
31 | + private final int repositoryTtl; | ||
32 | + | ||
33 | + public CassandraProcessedPartitionRepository(Session session, int repositoryTtl) { | ||
34 | + super(session); | ||
35 | + this.repositoryTtl = repositoryTtl; | ||
36 | + } | ||
37 | + | ||
38 | + @Override | ||
39 | + public ListenableFuture<Void> partitionProcessed(UUID nodeId, long clusteredHash, long partition) { | ||
40 | + String insert = "INSERT INTO processed_msg_partitions (node_id, clustered_hash, partition) VALUES (?, ?, ?) USING TTL ?"; | ||
41 | + PreparedStatement prepared = prepare(insert); | ||
42 | + BoundStatement boundStatement = prepared.bind(nodeId, clusteredHash, partition, repositoryTtl); | ||
43 | + ResultSetFuture resultSetFuture = executeAsyncWrite(boundStatement); | ||
44 | + return Futures.transform(resultSetFuture, (Function<ResultSet, Void>) input -> null); | ||
45 | + } | ||
46 | + | ||
47 | + @Override | ||
48 | + public Optional<Long> findLastProcessedPartition(UUID nodeId, long clusteredHash) { | ||
49 | + String select = "SELECT partition FROM processed_msg_partitions WHERE " + | ||
50 | + "node_id = ? AND clustered_hash = ?"; | ||
51 | + PreparedStatement prepared = prepare(select); | ||
52 | + BoundStatement boundStatement = prepared.bind(nodeId, clusteredHash); | ||
53 | + Row row = executeRead(boundStatement).one(); | ||
54 | + if (row == null) { | ||
55 | + return Optional.empty(); | ||
56 | + } | ||
57 | + | ||
58 | + return Optional.of(row.getLong("partition")); | ||
59 | + } | ||
60 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * <p> | ||
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 | + * <p> | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * <p> | ||
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.rule.engine.queue.cassandra.repository.impl; | ||
17 | + | ||
18 | +import com.datastax.driver.core.*; | ||
19 | +import lombok.extern.slf4j.Slf4j; | ||
20 | +import org.springframework.stereotype.Component; | ||
21 | + | ||
22 | +import java.util.Map; | ||
23 | +import java.util.concurrent.ConcurrentHashMap; | ||
24 | + | ||
25 | +@Component | ||
26 | +@Slf4j | ||
27 | +public abstract class SimpleAbstractCassandraDao { | ||
28 | + | ||
29 | + private ConsistencyLevel defaultReadLevel = ConsistencyLevel.QUORUM; | ||
30 | + private ConsistencyLevel defaultWriteLevel = ConsistencyLevel.QUORUM; | ||
31 | + private Session session; | ||
32 | + private Map<String, PreparedStatement> preparedStatementMap = new ConcurrentHashMap<>(); | ||
33 | + | ||
34 | + public SimpleAbstractCassandraDao(Session session) { | ||
35 | + this.session = session; | ||
36 | + } | ||
37 | + | ||
38 | + protected Session getSession() { | ||
39 | + return session; | ||
40 | + } | ||
41 | + | ||
42 | + protected ResultSet executeRead(Statement statement) { | ||
43 | + return execute(statement, defaultReadLevel); | ||
44 | + } | ||
45 | + | ||
46 | + protected ResultSet executeWrite(Statement statement) { | ||
47 | + return execute(statement, defaultWriteLevel); | ||
48 | + } | ||
49 | + | ||
50 | + protected ResultSetFuture executeAsyncRead(Statement statement) { | ||
51 | + return executeAsync(statement, defaultReadLevel); | ||
52 | + } | ||
53 | + | ||
54 | + protected ResultSetFuture executeAsyncWrite(Statement statement) { | ||
55 | + return executeAsync(statement, defaultWriteLevel); | ||
56 | + } | ||
57 | + | ||
58 | + protected PreparedStatement prepare(String query) { | ||
59 | + return preparedStatementMap.computeIfAbsent(query, i -> getSession().prepare(i)); | ||
60 | + } | ||
61 | + | ||
62 | + private ResultSet execute(Statement statement, ConsistencyLevel level) { | ||
63 | + log.debug("Execute cassandra statement {}", statement); | ||
64 | + if (statement.getConsistencyLevel() == null) { | ||
65 | + statement.setConsistencyLevel(level); | ||
66 | + } | ||
67 | + return getSession().execute(statement); | ||
68 | + } | ||
69 | + | ||
70 | + private ResultSetFuture executeAsync(Statement statement, ConsistencyLevel level) { | ||
71 | + log.debug("Execute cassandra async statement {}", statement); | ||
72 | + if (statement.getConsistencyLevel() == null) { | ||
73 | + statement.setConsistencyLevel(level); | ||
74 | + } | ||
75 | + return getSession().executeAsync(statement); | ||
76 | + } | ||
77 | +} |
1 | +package org.thingsboard.rule.engine.tool; | ||
2 | + | ||
3 | +import com.datastax.driver.core.Cluster; | ||
4 | +import com.datastax.driver.core.HostDistance; | ||
5 | +import com.datastax.driver.core.PoolingOptions; | ||
6 | +import com.datastax.driver.core.Session; | ||
7 | +import com.datastax.driver.core.utils.UUIDs; | ||
8 | +import com.google.common.util.concurrent.FutureCallback; | ||
9 | +import com.google.common.util.concurrent.Futures; | ||
10 | +import com.google.common.util.concurrent.ListenableFuture; | ||
11 | +import lombok.extern.slf4j.Slf4j; | ||
12 | +import org.springframework.beans.factory.annotation.Autowired; | ||
13 | +import org.springframework.boot.CommandLineRunner; | ||
14 | +import org.springframework.boot.SpringApplication; | ||
15 | +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; | ||
16 | +import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
17 | +import org.springframework.context.annotation.Bean; | ||
18 | +import org.springframework.context.annotation.ComponentScan; | ||
19 | +import org.thingsboard.rule.engine.api.MsqQueue; | ||
20 | +import org.thingsboard.rule.engine.api.TbMsg; | ||
21 | +import org.thingsboard.rule.engine.api.TbMsgMetaData; | ||
22 | + | ||
23 | +import javax.annotation.Nullable; | ||
24 | +import java.net.InetSocketAddress; | ||
25 | +import java.util.UUID; | ||
26 | +import java.util.concurrent.CountDownLatch; | ||
27 | +import java.util.concurrent.ExecutorService; | ||
28 | +import java.util.concurrent.Executors; | ||
29 | +import java.util.concurrent.TimeUnit; | ||
30 | +import java.util.concurrent.atomic.AtomicLong; | ||
31 | + | ||
32 | +@SpringBootApplication | ||
33 | +@EnableAutoConfiguration | ||
34 | +@ComponentScan({"org.thingsboard.rule.engine"}) | ||
35 | +//@PropertySource("classpath:processing-pipeline.properties") | ||
36 | +@Slf4j | ||
37 | +public class QueueBenchmark implements CommandLineRunner { | ||
38 | + | ||
39 | + public static void main(String[] args) { | ||
40 | + try { | ||
41 | + SpringApplication.run(QueueBenchmark.class, args); | ||
42 | + } catch (Throwable th) { | ||
43 | + th.printStackTrace(); | ||
44 | + System.exit(0); | ||
45 | + } | ||
46 | + } | ||
47 | + | ||
48 | + @Autowired | ||
49 | + private MsqQueue msqQueue; | ||
50 | + | ||
51 | + @Override | ||
52 | + public void run(String... strings) throws Exception { | ||
53 | + System.out.println("It works + " + msqQueue); | ||
54 | + | ||
55 | + | ||
56 | + long start = System.currentTimeMillis(); | ||
57 | + int msgCount = 10000000; | ||
58 | + AtomicLong count = new AtomicLong(0); | ||
59 | + ExecutorService service = Executors.newFixedThreadPool(100); | ||
60 | + | ||
61 | + CountDownLatch latch = new CountDownLatch(msgCount); | ||
62 | + for (int i = 0; i < msgCount; i++) { | ||
63 | + service.submit(() -> { | ||
64 | + boolean isFinished = false; | ||
65 | + while (!isFinished) { | ||
66 | + try { | ||
67 | + TbMsg msg = randomMsg(); | ||
68 | + UUID nodeId = UUIDs.timeBased(); | ||
69 | + ListenableFuture<Void> put = msqQueue.put(msg, nodeId, 100L); | ||
70 | +// ListenableFuture<Void> put = msqQueue.ack(msg, nodeId, 100L); | ||
71 | + Futures.addCallback(put, new FutureCallback<Void>() { | ||
72 | + @Override | ||
73 | + public void onSuccess(@Nullable Void result) { | ||
74 | + latch.countDown(); | ||
75 | + } | ||
76 | + | ||
77 | + @Override | ||
78 | + public void onFailure(Throwable t) { | ||
79 | +// t.printStackTrace(); | ||
80 | + System.out.println("onFailure, because:" + t.getMessage()); | ||
81 | + latch.countDown(); | ||
82 | + } | ||
83 | + }); | ||
84 | + isFinished = true; | ||
85 | + } catch (Throwable th) { | ||
86 | +// th.printStackTrace(); | ||
87 | + System.out.println("Repeat query, because:" + th.getMessage()); | ||
88 | +// latch.countDown(); | ||
89 | + } | ||
90 | + } | ||
91 | + }); | ||
92 | + } | ||
93 | + | ||
94 | + long prev = 0L; | ||
95 | + while (latch.getCount() != 0) { | ||
96 | + TimeUnit.SECONDS.sleep(1); | ||
97 | + long curr = latch.getCount(); | ||
98 | + long rps = prev - curr; | ||
99 | + prev = curr; | ||
100 | + System.out.println("rps = " + rps); | ||
101 | + } | ||
102 | + | ||
103 | + long end = System.currentTimeMillis(); | ||
104 | + System.out.println("final rps = " + (msgCount / (end - start) * 1000)); | ||
105 | + | ||
106 | + System.out.println("Finished"); | ||
107 | + | ||
108 | + } | ||
109 | + | ||
110 | + private TbMsg randomMsg() { | ||
111 | + TbMsgMetaData metaData = new TbMsgMetaData(); | ||
112 | + metaData.putValue("key", "value"); | ||
113 | + String dataStr = "someContent"; | ||
114 | + return new TbMsg(UUIDs.timeBased(), "type", null, metaData, dataStr.getBytes()); | ||
115 | + } | ||
116 | + | ||
117 | + @Bean | ||
118 | + public Session session() { | ||
119 | + Cluster thingsboard = Cluster.builder() | ||
120 | + .addContactPointsWithPorts(new InetSocketAddress("127.0.0.1", 9042)) | ||
121 | + .withClusterName("thingsboard") | ||
122 | +// .withSocketOptions(socketOpts.getOpts()) | ||
123 | + .withPoolingOptions(new PoolingOptions() | ||
124 | + .setMaxRequestsPerConnection(HostDistance.LOCAL, 32768) | ||
125 | + .setMaxRequestsPerConnection(HostDistance.REMOTE, 32768)).build(); | ||
126 | + | ||
127 | + Session session = thingsboard.connect("thingsboard"); | ||
128 | + return session; | ||
129 | + } | ||
130 | + | ||
131 | + @Bean | ||
132 | + public int defaultTtl() { | ||
133 | + return 6000; | ||
134 | + } | ||
135 | + | ||
136 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 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 | +syntax = "proto3"; | ||
17 | +package msgqueue; | ||
18 | + | ||
19 | +option java_package = "org.thingsboard.rule.engine.queue.cassandra.repository.gen"; | ||
20 | +option java_outer_classname = "MsgQueueProtos"; | ||
21 | + | ||
22 | + | ||
23 | +message TbMsgProto { | ||
24 | + string id = 1; | ||
25 | + string type = 2; | ||
26 | + string entityType = 3; | ||
27 | + string entityId = 4; | ||
28 | + | ||
29 | + message TbMsgMetaDataProto { | ||
30 | + map<string, string> data = 1; | ||
31 | + } | ||
32 | + | ||
33 | + repeated TbMsgMetaDataProto metaData = 5; | ||
34 | + | ||
35 | + bytes data = 6; | ||
36 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 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.rule.engine.queue.cassandra; | ||
17 | + | ||
18 | +import org.junit.Before; | ||
19 | +import org.junit.Test; | ||
20 | +import org.mockito.Mock; | ||
21 | +import org.thingsboard.rule.engine.queue.cassandra.repository.AckRepository; | ||
22 | +import org.thingsboard.rule.engine.queue.cassandra.repository.MsgRepository; | ||
23 | + | ||
24 | +public class CassandraMsqQueueTest { | ||
25 | + | ||
26 | + private CassandraMsqQueue msqQueue; | ||
27 | + | ||
28 | + @Mock | ||
29 | + private MsgRepository msgRepository; | ||
30 | + @Mock | ||
31 | + private AckRepository ackRepository; | ||
32 | + @Mock | ||
33 | + private UnprocessedMsgFilter unprocessedMsgFilter; | ||
34 | + @Mock | ||
35 | + private QueuePartitioner queuePartitioner; | ||
36 | + | ||
37 | + @Before | ||
38 | + public void init() { | ||
39 | + msqQueue = new CassandraMsqQueue(msgRepository, ackRepository, unprocessedMsgFilter, queuePartitioner); | ||
40 | + } | ||
41 | + | ||
42 | + @Test | ||
43 | + public void msgCanBeSaved() { | ||
44 | +// todo-vp: implement | ||
45 | + } | ||
46 | + | ||
47 | + | ||
48 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * <p> | ||
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 | + * <p> | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * <p> | ||
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.rule.engine.queue.cassandra; | ||
17 | + | ||
18 | + | ||
19 | +import org.junit.Before; | ||
20 | +import org.junit.Test; | ||
21 | +import org.junit.runner.RunWith; | ||
22 | +import org.mockito.Mock; | ||
23 | +import org.mockito.runners.MockitoJUnitRunner; | ||
24 | +import org.thingsboard.rule.engine.queue.cassandra.repository.ProcessedPartitionRepository; | ||
25 | + | ||
26 | +import java.time.Clock; | ||
27 | +import java.time.Instant; | ||
28 | +import java.time.ZoneOffset; | ||
29 | +import java.time.temporal.ChronoUnit; | ||
30 | +import java.util.List; | ||
31 | +import java.util.Optional; | ||
32 | +import java.util.UUID; | ||
33 | + | ||
34 | +import static org.junit.Assert.assertEquals; | ||
35 | +import static org.mockito.Mockito.when; | ||
36 | + | ||
37 | +@RunWith(MockitoJUnitRunner.class) | ||
38 | +public class QueuePartitionerTest { | ||
39 | + | ||
40 | + private QueuePartitioner queuePartitioner; | ||
41 | + | ||
42 | + @Mock | ||
43 | + private ProcessedPartitionRepository partitionRepo; | ||
44 | + | ||
45 | + private Instant startInstant; | ||
46 | + private Instant endInstant; | ||
47 | + | ||
48 | + @Before | ||
49 | + public void init() { | ||
50 | + queuePartitioner = new QueuePartitioner("MINUTES", partitionRepo); | ||
51 | + startInstant = Instant.now(); | ||
52 | + endInstant = startInstant.plus(2, ChronoUnit.MINUTES); | ||
53 | + queuePartitioner.setClock(Clock.fixed(endInstant, ZoneOffset.UTC)); | ||
54 | + } | ||
55 | + | ||
56 | + @Test | ||
57 | + public void partitionCalculated() { | ||
58 | + long time = 1519390191425L; | ||
59 | + long partition = queuePartitioner.getPartition(time); | ||
60 | + assertEquals(1519390140000L, partition); | ||
61 | + } | ||
62 | + | ||
63 | + @Test | ||
64 | + public void unprocessedPartitionsReturned() { | ||
65 | + UUID nodeId = UUID.randomUUID(); | ||
66 | + long clusteredHash = 101L; | ||
67 | + when(partitionRepo.findLastProcessedPartition(nodeId, clusteredHash)).thenReturn(Optional.of(startInstant.toEpochMilli())); | ||
68 | + List<Long> actual = queuePartitioner.findUnprocessedPartitions(nodeId, clusteredHash); | ||
69 | + assertEquals(3, actual.size()); | ||
70 | + } | ||
71 | + | ||
72 | + @Test | ||
73 | + public void defaultShiftUsedIfNoPartitionWasProcessed() { | ||
74 | + UUID nodeId = UUID.randomUUID(); | ||
75 | + long clusteredHash = 101L; | ||
76 | + when(partitionRepo.findLastProcessedPartition(nodeId, clusteredHash)).thenReturn(Optional.empty()); | ||
77 | + List<Long> actual = queuePartitioner.findUnprocessedPartitions(nodeId, clusteredHash); | ||
78 | + assertEquals(1011, actual.size()); | ||
79 | + } | ||
80 | + | ||
81 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 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.rule.engine.queue.cassandra; | ||
17 | + | ||
18 | +import com.google.common.collect.Lists; | ||
19 | +import org.junit.Test; | ||
20 | +import org.thingsboard.rule.engine.api.TbMsg; | ||
21 | + | ||
22 | +import java.util.Collection; | ||
23 | +import java.util.List; | ||
24 | +import java.util.UUID; | ||
25 | + | ||
26 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
27 | + | ||
28 | +public class UnprocessedMsgFilterTest { | ||
29 | + | ||
30 | + private UnprocessedMsgFilter msgFilter = new UnprocessedMsgFilter(); | ||
31 | + | ||
32 | + @Test | ||
33 | + public void acknowledgedMsgsAreFilteredOut() { | ||
34 | + UUID id1 = UUID.randomUUID(); | ||
35 | + UUID id2 = UUID.randomUUID(); | ||
36 | + TbMsg msg1 = new TbMsg(id1, "T", null, null, null); | ||
37 | + TbMsg msg2 = new TbMsg(id2, "T", null, null, null); | ||
38 | + List<TbMsg> msgs = Lists.newArrayList(msg1, msg2); | ||
39 | + List<MsgAck> acks = Lists.newArrayList(new MsgAck(id2, UUID.randomUUID(), 1L, 1L)); | ||
40 | + Collection<TbMsg> actual = msgFilter.filter(msgs, acks); | ||
41 | + assertEquals(1, actual.size()); | ||
42 | + assertEquals(msg1, actual.iterator().next()); | ||
43 | + } | ||
44 | + | ||
45 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 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.rule.engine.queue.cassandra.repository.impl; | ||
17 | + | ||
18 | +import com.datastax.driver.core.utils.UUIDs; | ||
19 | +import com.google.common.collect.Lists; | ||
20 | +import com.google.common.util.concurrent.ListenableFuture; | ||
21 | +import org.junit.Before; | ||
22 | +import org.junit.Test; | ||
23 | +import org.thingsboard.rule.engine.queue.cassandra.MsgAck; | ||
24 | + | ||
25 | +import java.util.List; | ||
26 | +import java.util.UUID; | ||
27 | +import java.util.concurrent.ExecutionException; | ||
28 | +import java.util.concurrent.TimeUnit; | ||
29 | + | ||
30 | +import static org.junit.Assert.assertEquals; | ||
31 | +import static org.junit.Assert.assertTrue; | ||
32 | + | ||
33 | +public class CassandraAckRepositoryTest extends SimpleAbstractCassandraDaoTest { | ||
34 | + | ||
35 | + private CassandraAckRepository ackRepository; | ||
36 | + | ||
37 | + @Before | ||
38 | + public void init() { | ||
39 | + ackRepository = new CassandraAckRepository(cassandraUnit.session, 1); | ||
40 | + } | ||
41 | + | ||
42 | + @Test | ||
43 | + public void acksInPartitionCouldBeFound() { | ||
44 | + UUID nodeId = UUID.fromString("055eee50-1883-11e8-b380-65b5d5335ba9"); | ||
45 | + | ||
46 | + List<MsgAck> extectedAcks = Lists.newArrayList( | ||
47 | + new MsgAck(UUID.fromString("bebaeb60-1888-11e8-bf21-65b5d5335ba9"), nodeId, 101L, 300L), | ||
48 | + new MsgAck(UUID.fromString("12baeb60-1888-11e8-bf21-65b5d5335ba9"), nodeId, 101L, 300L) | ||
49 | + ); | ||
50 | + | ||
51 | + List<MsgAck> actualAcks = ackRepository.findAcks(nodeId, 101L, 300L); | ||
52 | + assertEquals(extectedAcks, actualAcks); | ||
53 | + } | ||
54 | + | ||
55 | + @Test | ||
56 | + public void ackCanBeSavedAndRead() throws ExecutionException, InterruptedException { | ||
57 | + UUID msgId = UUIDs.timeBased(); | ||
58 | + UUID nodeId = UUIDs.timeBased(); | ||
59 | + MsgAck ack = new MsgAck(msgId, nodeId, 10L, 20L); | ||
60 | + ListenableFuture<Void> future = ackRepository.ack(ack); | ||
61 | + future.get(); | ||
62 | + List<MsgAck> actualAcks = ackRepository.findAcks(nodeId, 10L, 20L); | ||
63 | + assertEquals(1, actualAcks.size()); | ||
64 | + assertEquals(ack, actualAcks.get(0)); | ||
65 | + } | ||
66 | + | ||
67 | + @Test | ||
68 | + public void expiredAcksAreNotReturned() throws ExecutionException, InterruptedException { | ||
69 | + UUID msgId = UUIDs.timeBased(); | ||
70 | + UUID nodeId = UUIDs.timeBased(); | ||
71 | + MsgAck ack = new MsgAck(msgId, nodeId, 30L, 40L); | ||
72 | + ListenableFuture<Void> future = ackRepository.ack(ack); | ||
73 | + future.get(); | ||
74 | + List<MsgAck> actualAcks = ackRepository.findAcks(nodeId, 30L, 40L); | ||
75 | + assertEquals(1, actualAcks.size()); | ||
76 | + TimeUnit.SECONDS.sleep(2); | ||
77 | + assertTrue(ackRepository.findAcks(nodeId, 30L, 40L).isEmpty()); | ||
78 | + } | ||
79 | + | ||
80 | + | ||
81 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 The Thingsboard Authors | ||
3 | + * <p> | ||
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 | + * <p> | ||
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | ||
9 | + * <p> | ||
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.rule.engine.queue.cassandra.repository.impl; | ||
17 | + | ||
18 | +//import static org.junit.jupiter.api.Assertions.*; | ||
19 | + | ||
20 | +import com.datastax.driver.core.utils.UUIDs; | ||
21 | +import com.google.common.util.concurrent.ListenableFuture; | ||
22 | +import org.junit.Before; | ||
23 | +import org.junit.Test; | ||
24 | +import org.thingsboard.rule.engine.api.TbMsg; | ||
25 | +import org.thingsboard.rule.engine.api.TbMsgMetaData; | ||
26 | +import org.thingsboard.server.common.data.id.DeviceId; | ||
27 | + | ||
28 | +import java.util.List; | ||
29 | +import java.util.UUID; | ||
30 | +import java.util.concurrent.ExecutionException; | ||
31 | +import java.util.concurrent.TimeUnit; | ||
32 | + | ||
33 | +import static org.junit.Assert.assertEquals; | ||
34 | +import static org.junit.Assert.assertTrue; | ||
35 | + | ||
36 | +public class CassandraMsgRepositoryTest extends SimpleAbstractCassandraDaoTest { | ||
37 | + | ||
38 | + private CassandraMsgRepository msgRepository; | ||
39 | + | ||
40 | + @Before | ||
41 | + public void init() { | ||
42 | + msgRepository = new CassandraMsgRepository(cassandraUnit.session, 1); | ||
43 | + } | ||
44 | + | ||
45 | + @Test | ||
46 | + public void msgCanBeSavedAndRead() throws ExecutionException, InterruptedException { | ||
47 | + TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, new byte[4]); | ||
48 | + UUID nodeId = UUIDs.timeBased(); | ||
49 | + ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 1L, 1L, 1L); | ||
50 | + future.get(); | ||
51 | + List<TbMsg> msgs = msgRepository.findMsgs(nodeId, 1L, 1L); | ||
52 | + assertEquals(1, msgs.size()); | ||
53 | + } | ||
54 | + | ||
55 | + @Test | ||
56 | + public void expiredMsgsAreNotReturned() throws ExecutionException, InterruptedException { | ||
57 | + TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), null, new byte[4]); | ||
58 | + UUID nodeId = UUIDs.timeBased(); | ||
59 | + ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 2L, 2L, 2L); | ||
60 | + future.get(); | ||
61 | + List<TbMsg> msgs = msgRepository.findMsgs(nodeId, 2L, 2L); | ||
62 | + assertEquals(1, msgs.size()); | ||
63 | + TimeUnit.SECONDS.sleep(2); | ||
64 | + assertTrue(msgRepository.findMsgs(nodeId, 2L, 2L).isEmpty()); | ||
65 | + } | ||
66 | + | ||
67 | + @Test | ||
68 | + public void protoBufConverterWorkAsExpected() throws ExecutionException, InterruptedException { | ||
69 | + TbMsgMetaData metaData = new TbMsgMetaData(); | ||
70 | + metaData.putValue("key", "value"); | ||
71 | + String dataStr = "someContent"; | ||
72 | + TbMsg msg = new TbMsg(UUIDs.timeBased(), "type", new DeviceId(UUIDs.timeBased()), metaData, dataStr.getBytes()); | ||
73 | + UUID nodeId = UUIDs.timeBased(); | ||
74 | + ListenableFuture<Void> future = msgRepository.save(msg, nodeId, 1L, 1L, 1L); | ||
75 | + future.get(); | ||
76 | + List<TbMsg> msgs = msgRepository.findMsgs(nodeId, 1L, 1L); | ||
77 | + assertEquals(1, msgs.size()); | ||
78 | + assertEquals(msg, msgs.get(0)); | ||
79 | + } | ||
80 | + | ||
81 | + | ||
82 | +} |
1 | +/** | ||
2 | + * Copyright © 2016-2017 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.rule.engine.queue.cassandra.repository.impl; | ||
17 | + | ||
18 | +import com.datastax.driver.core.utils.UUIDs; | ||
19 | +import com.google.common.util.concurrent.Futures; | ||
20 | +import com.google.common.util.concurrent.ListenableFuture; | ||
21 | +import org.junit.Before; | ||
22 | +import org.junit.Test; | ||
23 | + | ||
24 | +import java.util.List; | ||
25 | +import java.util.Optional; | ||
26 | +import java.util.UUID; | ||
27 | +import java.util.concurrent.ExecutionException; | ||
28 | +import java.util.concurrent.TimeUnit; | ||
29 | + | ||
30 | +import static org.junit.Assert.*; | ||
31 | + | ||
32 | +public class CassandraProcessedPartitionRepositoryTest extends SimpleAbstractCassandraDaoTest { | ||
33 | + | ||
34 | + private CassandraProcessedPartitionRepository partitionRepository; | ||
35 | + | ||
36 | + @Before | ||
37 | + public void init() { | ||
38 | + partitionRepository = new CassandraProcessedPartitionRepository(cassandraUnit.session, 1); | ||
39 | + } | ||
40 | + | ||
41 | + @Test | ||
42 | + public void lastProcessedPartitionCouldBeFound() { | ||
43 | + UUID nodeId = UUID.fromString("055eee50-1883-11e8-b380-65b5d5335ba9"); | ||
44 | + Optional<Long> lastProcessedPartition = partitionRepository.findLastProcessedPartition(nodeId, 101L); | ||
45 | + assertTrue(lastProcessedPartition.isPresent()); | ||
46 | + assertEquals((Long) 777L, lastProcessedPartition.get()); | ||
47 | + } | ||
48 | + | ||
49 | + @Test | ||
50 | + public void highestProcessedPartitionReturned() throws ExecutionException, InterruptedException { | ||
51 | + UUID nodeId = UUIDs.timeBased(); | ||
52 | + ListenableFuture<Void> future1 = partitionRepository.partitionProcessed(nodeId, 303L, 100L); | ||
53 | + ListenableFuture<Void> future2 = partitionRepository.partitionProcessed(nodeId, 303L, 200L); | ||
54 | + ListenableFuture<Void> future3 = partitionRepository.partitionProcessed(nodeId, 303L, 10L); | ||
55 | + ListenableFuture<List<Void>> allFutures = Futures.allAsList(future1, future2, future3); | ||
56 | + allFutures.get(); | ||
57 | + Optional<Long> actual = partitionRepository.findLastProcessedPartition(nodeId, 303L); | ||
58 | + assertTrue(actual.isPresent()); | ||
59 | + assertEquals((Long) 200L, actual.get()); | ||
60 | + } | ||
61 | + | ||
62 | + @Test | ||
63 | + public void expiredPartitionsAreNotReturned() throws ExecutionException, InterruptedException { | ||
64 | + UUID nodeId = UUIDs.timeBased(); | ||
65 | + ListenableFuture<Void> future = partitionRepository.partitionProcessed(nodeId, 404L, 10L); | ||
66 | + future.get(); | ||
67 | + Optional<Long> actual = partitionRepository.findLastProcessedPartition(nodeId, 404L); | ||
68 | + assertEquals((Long) 10L, actual.get()); | ||
69 | + TimeUnit.SECONDS.sleep(2); | ||
70 | + assertFalse(partitionRepository.findLastProcessedPartition(nodeId, 404L).isPresent()); | ||
71 | + } | ||
72 | + | ||
73 | + @Test | ||
74 | + public void ifNoPartitionsWereProcessedEmptyResultReturned() { | ||
75 | + UUID nodeId = UUIDs.timeBased(); | ||
76 | + Optional<Long> actual = partitionRepository.findLastProcessedPartition(nodeId, 505L); | ||
77 | + assertFalse(actual.isPresent()); | ||
78 | + } | ||
79 | + | ||
80 | +} |
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/queue/cassandra/repository/impl/SimpleAbstractCassandraDaoTest.java
renamed from
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/queue/cassandra/AckBuilder.java
@@ -13,17 +13,18 @@ | @@ -13,17 +13,18 @@ | ||
13 | * See the License for the specific language governing permissions and | 13 | * See the License for the specific language governing permissions and |
14 | * limitations under the License. | 14 | * limitations under the License. |
15 | */ | 15 | */ |
16 | -package org.thingsboard.rule.engine.queue.cassandra; | 16 | +package org.thingsboard.rule.engine.queue.cassandra.repository.impl; |
17 | 17 | ||
18 | -import org.springframework.stereotype.Component; | ||
19 | -import org.thingsboard.rule.engine.api.TbMsg; | 18 | +import org.cassandraunit.CassandraCQLUnit; |
19 | +import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; | ||
20 | +import org.junit.ClassRule; | ||
20 | 21 | ||
21 | -import java.util.UUID; | ||
22 | 22 | ||
23 | -@Component | ||
24 | -public class AckBuilder { | 23 | +public abstract class SimpleAbstractCassandraDaoTest { |
25 | 24 | ||
26 | - public MsgAck build(TbMsg msg, UUID nodeId, long clusteredHash) { | ||
27 | - return null; | ||
28 | - } | ||
29 | -} | 25 | + @ClassRule |
26 | + public static CassandraCQLUnit cassandraUnit = new CassandraCQLUnit( | ||
27 | + new ClassPathCQLDataSet("cassandra/system-test.cql", "thingsboard")); | ||
28 | + | ||
29 | + | ||
30 | +} |
1 | +CREATE TABLE IF NOT EXISTS thingsboard.msg_queue ( | ||
2 | + node_id timeuuid, | ||
3 | + clustered_hash bigint, | ||
4 | + partition bigint, | ||
5 | + ts bigint, | ||
6 | + msg blob, | ||
7 | + PRIMARY KEY ((node_id, clustered_hash, partition), ts)) | ||
8 | +WITH CLUSTERING ORDER BY (ts DESC) | ||
9 | +AND compaction = { | ||
10 | + 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
11 | + 'min_threshold': '5', | ||
12 | + 'base_time_seconds': '43200', | ||
13 | + 'max_window_size_seconds': '43200', | ||
14 | + 'tombstone_threshold': '0.9', | ||
15 | + 'unchecked_tombstone_compaction': 'true' | ||
16 | +}; | ||
17 | + | ||
18 | + | ||
19 | +CREATE TABLE IF NOT EXISTS thingsboard.msg_ack_queue ( | ||
20 | + node_id timeuuid, | ||
21 | + clustered_hash bigint, | ||
22 | + partition bigint, | ||
23 | + msg_id timeuuid, | ||
24 | + PRIMARY KEY ((node_id, clustered_hash, partition), msg_id)) | ||
25 | +WITH CLUSTERING ORDER BY (msg_id DESC) | ||
26 | +AND compaction = { | ||
27 | + 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
28 | + 'min_threshold': '5', | ||
29 | + 'base_time_seconds': '43200', | ||
30 | + 'max_window_size_seconds': '43200', | ||
31 | + 'tombstone_threshold': '0.9', | ||
32 | + 'unchecked_tombstone_compaction': 'true' | ||
33 | +}; | ||
34 | + | ||
35 | +CREATE TABLE IF NOT EXISTS thingsboard.processed_msg_partitions ( | ||
36 | + node_id timeuuid, | ||
37 | + clustered_hash bigint, | ||
38 | + partition bigint, | ||
39 | + PRIMARY KEY ((node_id, clustered_hash), partition)) | ||
40 | +WITH CLUSTERING ORDER BY (partition DESC) | ||
41 | +AND compaction = { | ||
42 | + 'class': 'org.apache.cassandra.db.compaction.DateTieredCompactionStrategy', | ||
43 | + 'min_threshold': '5', | ||
44 | + 'base_time_seconds': '43200', | ||
45 | + 'max_window_size_seconds': '43200', | ||
46 | + 'tombstone_threshold': '0.9', | ||
47 | + 'unchecked_tombstone_compaction': 'true' | ||
48 | +}; | ||
49 | + | ||
50 | + | ||
51 | + | ||
52 | +-- msg_queue dataset | ||
53 | + | ||
54 | +INSERT INTO thingsboard.msg_queue (node_id, clustered_hash, partition, ts, msg) | ||
55 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 201, null); | ||
56 | +INSERT INTO thingsboard.msg_queue (node_id, clustered_hash, partition, ts, msg) | ||
57 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 202, null); | ||
58 | +INSERT INTO thingsboard.msg_queue (node_id, clustered_hash, partition, ts, msg) | ||
59 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, 301, null); | ||
60 | + | ||
61 | +-- ack_queue dataset | ||
62 | +INSERT INTO msg_ack_queue (node_id, clustered_hash, partition, msg_id) | ||
63 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, bebaeb60-1888-11e8-bf21-65b5d5335ba9); | ||
64 | +INSERT INTO msg_ack_queue (node_id, clustered_hash, partition, msg_id) | ||
65 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 300, 12baeb60-1888-11e8-bf21-65b5d5335ba9); | ||
66 | + INSERT INTO msg_ack_queue (node_id, clustered_hash, partition, msg_id) | ||
67 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 200, 32baeb60-1888-11e8-bf21-65b5d5335ba9); | ||
68 | + | ||
69 | +-- processed partition dataset | ||
70 | +INSERT INTO processed_msg_partitions (node_id, clustered_hash, partition) | ||
71 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 100); | ||
72 | +INSERT INTO processed_msg_partitions (node_id, clustered_hash, partition) | ||
73 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 101, 777); | ||
74 | +INSERT INTO processed_msg_partitions (node_id, clustered_hash, partition) | ||
75 | + VALUES (055eee50-1883-11e8-b380-65b5d5335ba9, 202, 200); |