Commit 3b5a4941ff6161acfc2fba95fc6a91af059231cc

Authored by vparomskiy
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);