Commit 1e9016cb478c6494344903e6de45f33bb646d32a

Authored by Andrii Shvaika
1 parent 3176c208

Implementation

@@ -54,6 +54,7 @@ import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; @@ -54,6 +54,7 @@ import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
54 import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; 54 import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
55 import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; 55 import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
56 import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate; 56 import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate;
  57 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
57 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; 58 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
58 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; 59 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
59 import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; 60 import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
@@ -92,7 +93,7 @@ import java.util.stream.Collectors; @@ -92,7 +93,7 @@ import java.util.stream.Collectors;
92 public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService { 93 public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubscriptionService {
93 94
94 private static final int DEFAULT_LIMIT = 100; 95 private static final int DEFAULT_LIMIT = 100;
95 - private final Map<String, Map<Integer, TbAbstractDataSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>(); 96 + private final Map<String, Map<Integer, TbAbstractSubCtx>> subscriptionsBySessionId = new ConcurrentHashMap<>();
96 97
97 @Autowired 98 @Autowired
98 private TelemetryWebSocketService wsService; 99 private TelemetryWebSocketService wsService;
@@ -202,7 +203,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc @@ -202,7 +203,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
202 //TODO: validate number of dynamic page links against rate limits. Ignore dynamic flag if limit is reached. 203 //TODO: validate number of dynamic page links against rate limits. Ignore dynamic flag if limit is reached.
203 TbEntityDataSubCtx finalCtx = ctx; 204 TbEntityDataSubCtx finalCtx = ctx;
204 ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay( 205 ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(
205 - () -> refreshDynamicQuery(tenantId, customerId, finalCtx), 206 + () -> refreshDynamicQuery(finalCtx),
206 dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS); 207 dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS);
207 finalCtx.setRefreshTask(task); 208 finalCtx.setRefreshTask(task);
208 } 209 }
@@ -236,6 +237,26 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc @@ -236,6 +237,26 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
236 } 237 }
237 238
238 @Override 239 @Override
  240 + public void handleCmd(TelemetryWebSocketSessionRef session, EntityCountCmd cmd) {
  241 + TbEntityCountSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
  242 + if (ctx == null) {
  243 + ctx = createSubCtx(session, cmd);
  244 + long start = System.currentTimeMillis();
  245 + ctx.fetchData();
  246 + long end = System.currentTimeMillis();
  247 + stats.getRegularQueryInvocationCnt().incrementAndGet();
  248 + stats.getRegularQueryTimeSpent().addAndGet(end - start);
  249 + TbEntityCountSubCtx finalCtx = ctx;
  250 + ScheduledFuture<?> task = scheduler.scheduleWithFixedDelay(
  251 + () -> refreshDynamicQuery(finalCtx),
  252 + dynamicPageLinkRefreshInterval, dynamicPageLinkRefreshInterval, TimeUnit.SECONDS);
  253 + finalCtx.setRefreshTask(task);
  254 + } else {
  255 + log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd);
  256 + }
  257 + }
  258 +
  259 + @Override
239 public void handleCmd(TelemetryWebSocketSessionRef session, AlarmDataCmd cmd) { 260 public void handleCmd(TelemetryWebSocketSessionRef session, AlarmDataCmd cmd) {
240 TbAlarmDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); 261 TbAlarmDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
241 if (ctx == null) { 262 if (ctx == null) {
@@ -267,7 +288,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc @@ -267,7 +288,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
267 } 288 }
268 } 289 }
269 290
270 - private void refreshDynamicQuery(TenantId tenantId, CustomerId customerId, TbEntityDataSubCtx finalCtx) { 291 + private void refreshDynamicQuery(TbAbstractSubCtx finalCtx) {
271 try { 292 try {
272 long start = System.currentTimeMillis(); 293 long start = System.currentTimeMillis();
273 finalCtx.update(); 294 finalCtx.update();
@@ -299,7 +320,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc @@ -299,7 +320,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
299 } 320 }
300 321
301 private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { 322 private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) {
302 - Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); 323 + Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
303 TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, 324 TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
304 attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription); 325 attributesService, stats, sessionRef, cmd.getCmdId(), maxEntitiesPerDataSubscription);
305 if (cmd.getQuery() != null) { 326 if (cmd.getQuery() != null) {
@@ -309,8 +330,20 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc @@ -309,8 +330,20 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
309 return ctx; 330 return ctx;
310 } 331 }
311 332
  333 + private TbEntityCountSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
  334 + Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
  335 + TbEntityCountSubCtx ctx = new TbEntityCountSubCtx(serviceId, wsService, entityService, localSubscriptionService,
  336 + attributesService, stats, sessionRef, cmd.getCmdId());
  337 + if (cmd.getQuery() != null) {
  338 + ctx.setAndResolveQuery(cmd.getQuery());
  339 + }
  340 + sessionSubs.put(cmd.getCmdId(), ctx);
  341 + return ctx;
  342 + }
  343 +
  344 +
312 private TbAlarmDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { 345 private TbAlarmDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
313 - Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); 346 + Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>());
314 TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService, 347 TbAlarmDataSubCtx ctx = new TbAlarmDataSubCtx(serviceId, wsService, entityService, localSubscriptionService,
315 attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription); 348 attributesService, stats, alarmService, sessionRef, cmd.getCmdId(), maxEntitiesPerAlarmSubscription);
316 ctx.setAndResolveQuery(cmd.getQuery()); 349 ctx.setAndResolveQuery(cmd.getQuery());
@@ -319,8 +352,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc @@ -319,8 +352,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
319 } 352 }
320 353
321 @SuppressWarnings("unchecked") 354 @SuppressWarnings("unchecked")
322 - private <T extends TbAbstractDataSubCtx> T getSubCtx(String sessionId, int cmdId) {  
323 - Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.get(sessionId); 355 + private <T extends TbAbstractSubCtx> T getSubCtx(String sessionId, int cmdId) {
  356 + Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.get(sessionId);
324 if (sessionSubs != null) { 357 if (sessionSubs != null) {
325 return (T) sessionSubs.get(cmdId); 358 return (T) sessionSubs.get(cmdId);
326 } else { 359 } else {
@@ -464,17 +497,16 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc @@ -464,17 +497,16 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
464 cleanupAndCancel(getSubCtx(sessionId, cmd.getCmdId())); 497 cleanupAndCancel(getSubCtx(sessionId, cmd.getCmdId()));
465 } 498 }
466 499
467 - private void cleanupAndCancel(TbAbstractDataSubCtx ctx) { 500 + private void cleanupAndCancel(TbAbstractSubCtx ctx) {
468 if (ctx != null) { 501 if (ctx != null) {
469 ctx.cancelTasks(); 502 ctx.cancelTasks();
470 - ctx.clearEntitySubscriptions();  
471 - ctx.clearDynamicValueSubscriptions(); 503 + ctx.clearSubscriptions();
472 } 504 }
473 } 505 }
474 506
475 @Override 507 @Override
476 public void cancelAllSessionSubscriptions(String sessionId) { 508 public void cancelAllSessionSubscriptions(String sessionId) {
477 - Map<Integer, TbAbstractDataSubCtx> sessionSubs = subscriptionsBySessionId.remove(sessionId); 509 + Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.remove(sessionId);
478 if (sessionSubs != null) { 510 if (sessionSubs != null) {
479 sessionSubs.values().forEach(this::cleanupAndCancel); 511 sessionSubs.values().forEach(this::cleanupAndCancel);
480 } 512 }
@@ -15,32 +15,16 @@ @@ -15,32 +15,16 @@
15 */ 15 */
16 package org.thingsboard.server.service.subscription; 16 package org.thingsboard.server.service.subscription;
17 17
18 -import com.google.common.util.concurrent.Futures;  
19 -import com.google.common.util.concurrent.ListenableFuture;  
20 -import com.google.common.util.concurrent.MoreExecutors;  
21 -import lombok.Data;  
22 import lombok.Getter; 18 import lombok.Getter;
23 -import lombok.Setter;  
24 import lombok.extern.slf4j.Slf4j; 19 import lombok.extern.slf4j.Slf4j;
25 -import org.thingsboard.server.common.data.id.CustomerId;  
26 import org.thingsboard.server.common.data.id.EntityId; 20 import org.thingsboard.server.common.data.id.EntityId;
27 -import org.thingsboard.server.common.data.id.TenantId;  
28 -import org.thingsboard.server.common.data.id.UserId;  
29 -import org.thingsboard.server.common.data.kv.AttributeKvEntry;  
30 import org.thingsboard.server.common.data.page.PageData; 21 import org.thingsboard.server.common.data.page.PageData;
31 import org.thingsboard.server.common.data.query.AbstractDataQuery; 22 import org.thingsboard.server.common.data.query.AbstractDataQuery;
32 -import org.thingsboard.server.common.data.query.ComplexFilterPredicate;  
33 -import org.thingsboard.server.common.data.query.DynamicValue;  
34 -import org.thingsboard.server.common.data.query.DynamicValueSourceType;  
35 import org.thingsboard.server.common.data.query.EntityData; 23 import org.thingsboard.server.common.data.query.EntityData;
36 import org.thingsboard.server.common.data.query.EntityDataPageLink; 24 import org.thingsboard.server.common.data.query.EntityDataPageLink;
37 import org.thingsboard.server.common.data.query.EntityDataQuery; 25 import org.thingsboard.server.common.data.query.EntityDataQuery;
38 import org.thingsboard.server.common.data.query.EntityKey; 26 import org.thingsboard.server.common.data.query.EntityKey;
39 import org.thingsboard.server.common.data.query.EntityKeyType; 27 import org.thingsboard.server.common.data.query.EntityKeyType;
40 -import org.thingsboard.server.common.data.query.FilterPredicateType;  
41 -import org.thingsboard.server.common.data.query.KeyFilter;  
42 -import org.thingsboard.server.common.data.query.KeyFilterPredicate;  
43 -import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;  
44 import org.thingsboard.server.common.data.query.TsValue; 28 import org.thingsboard.server.common.data.query.TsValue;
45 import org.thingsboard.server.dao.attributes.AttributesService; 29 import org.thingsboard.server.dao.attributes.AttributesService;
46 import org.thingsboard.server.dao.entity.EntityService; 30 import org.thingsboard.server.dao.entity.EntityService;
@@ -52,140 +36,25 @@ import java.util.ArrayList; @@ -52,140 +36,25 @@ import java.util.ArrayList;
52 import java.util.Arrays; 36 import java.util.Arrays;
53 import java.util.Collections; 37 import java.util.Collections;
54 import java.util.HashMap; 38 import java.util.HashMap;
55 -import java.util.HashSet;  
56 import java.util.List; 39 import java.util.List;
57 import java.util.Map; 40 import java.util.Map;
58 -import java.util.Optional;  
59 -import java.util.Set;  
60 import java.util.concurrent.ConcurrentHashMap; 41 import java.util.concurrent.ConcurrentHashMap;
61 -import java.util.concurrent.ExecutionException;  
62 -import java.util.concurrent.ScheduledFuture;  
63 import java.util.function.Function; 42 import java.util.function.Function;
64 import java.util.stream.Collectors; 43 import java.util.stream.Collectors;
65 44
66 @Slf4j 45 @Slf4j
67 -@Data  
68 -public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends EntityDataPageLink>> { 46 +public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends EntityDataPageLink>> extends TbAbstractSubCtx<T> {
69 47
70 - protected final String serviceId;  
71 - protected final SubscriptionServiceStatistics stats;  
72 - protected final TelemetryWebSocketService wsService;  
73 - protected final EntityService entityService;  
74 - protected final TbLocalSubscriptionService localSubscriptionService;  
75 - protected final AttributesService attributesService;  
76 - protected final TelemetryWebSocketSessionRef sessionRef;  
77 - protected final int cmdId;  
78 protected final Map<Integer, EntityId> subToEntityIdMap; 48 protected final Map<Integer, EntityId> subToEntityIdMap;
79 - protected final Set<Integer> subToDynamicValueKeySet;  
80 - @Getter  
81 - protected final Map<DynamicValueKey, List<DynamicValue>> dynamicValues;  
82 @Getter 49 @Getter
83 protected PageData<EntityData> data; 50 protected PageData<EntityData> data;
84 - @Getter  
85 - @Setter  
86 - protected T query;  
87 - @Setter  
88 - protected volatile ScheduledFuture<?> refreshTask;  
89 51
90 public TbAbstractDataSubCtx(String serviceId, TelemetryWebSocketService wsService, 52 public TbAbstractDataSubCtx(String serviceId, TelemetryWebSocketService wsService,
91 EntityService entityService, TbLocalSubscriptionService localSubscriptionService, 53 EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
92 AttributesService attributesService, SubscriptionServiceStatistics stats, 54 AttributesService attributesService, SubscriptionServiceStatistics stats,
93 TelemetryWebSocketSessionRef sessionRef, int cmdId) { 55 TelemetryWebSocketSessionRef sessionRef, int cmdId) {
94 - this.serviceId = serviceId;  
95 - this.wsService = wsService;  
96 - this.entityService = entityService;  
97 - this.localSubscriptionService = localSubscriptionService;  
98 - this.attributesService = attributesService;  
99 - this.stats = stats;  
100 - this.sessionRef = sessionRef;  
101 - this.cmdId = cmdId; 56 + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
102 this.subToEntityIdMap = new ConcurrentHashMap<>(); 57 this.subToEntityIdMap = new ConcurrentHashMap<>();
103 - this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet();  
104 - this.dynamicValues = new ConcurrentHashMap<>();  
105 - }  
106 -  
107 - public void setAndResolveQuery(T query) {  
108 - dynamicValues.clear();  
109 - this.query = query;  
110 - if (query != null && query.getKeyFilters() != null) {  
111 - for (KeyFilter filter : query.getKeyFilters()) {  
112 - registerDynamicValues(filter.getPredicate());  
113 - }  
114 - }  
115 - resolve(getTenantId(), getCustomerId(), getUserId());  
116 - }  
117 -  
118 - public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) {  
119 - List<ListenableFuture<DynamicValueKeySub>> futures = new ArrayList<>();  
120 - for (DynamicValueKey key : dynamicValues.keySet()) {  
121 - switch (key.getSourceType()) {  
122 - case CURRENT_TENANT:  
123 - futures.add(resolveEntityValue(tenantId, tenantId, key));  
124 - break;  
125 - case CURRENT_CUSTOMER:  
126 - if (customerId != null && !customerId.isNullUid()) {  
127 - futures.add(resolveEntityValue(tenantId, customerId, key));  
128 - }  
129 - break;  
130 - case CURRENT_USER:  
131 - if (userId != null && !userId.isNullUid()) {  
132 - futures.add(resolveEntityValue(tenantId, userId, key));  
133 - }  
134 - break;  
135 - }  
136 - }  
137 - try {  
138 - Map<EntityId, Map<String, DynamicValueKeySub>> tmpSubMap = new HashMap<>();  
139 - for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) {  
140 - tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub);  
141 - }  
142 - for (EntityId entityId : tmpSubMap.keySet()) {  
143 - Map<String, Long> keyStates = new HashMap<>();  
144 - Map<String, DynamicValueKeySub> dynamicValueKeySubMap = tmpSubMap.get(entityId);  
145 - dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs()));  
146 - int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();  
147 - TbAttributeSubscription sub = TbAttributeSubscription.builder()  
148 - .serviceId(serviceId)  
149 - .sessionId(sessionRef.getSessionId())  
150 - .subscriptionId(subIdx)  
151 - .tenantId(sessionRef.getSecurityCtx().getTenantId())  
152 - .entityId(entityId)  
153 - .updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap))  
154 - .allKeys(false)  
155 - .keyStates(keyStates)  
156 - .scope(TbAttributeSubscriptionScope.SERVER_SCOPE)  
157 - .build();  
158 - subToDynamicValueKeySet.add(subIdx);  
159 - localSubscriptionService.addSubscription(sub);  
160 - }  
161 - } catch (InterruptedException | ExecutionException e) {  
162 - log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet());  
163 - }  
164 -  
165 - }  
166 -  
167 - private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate,  
168 - Map<String, DynamicValueKeySub> dynamicValueKeySubMap) {  
169 - Map<String, TsValue> latestUpdate = new HashMap<>();  
170 - subscriptionUpdate.getData().forEach((k, v) -> {  
171 - Object[] data = (Object[]) v.get(0);  
172 - latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));  
173 - });  
174 -  
175 - boolean invalidateFilter = false;  
176 - for (Map.Entry<String, TsValue> entry : latestUpdate.entrySet()) {  
177 - String k = entry.getKey();  
178 - TsValue tsValue = entry.getValue();  
179 - DynamicValueKeySub sub = dynamicValueKeySubMap.get(k);  
180 - if (sub.updateValue(tsValue)) {  
181 - invalidateFilter = true;  
182 - updateDynamicValuesByKey(sub, tsValue);  
183 - }  
184 - }  
185 -  
186 - if (invalidateFilter) {  
187 - update();  
188 - }  
189 } 58 }
190 59
191 public void fetchData() { 60 public void fetchData() {
@@ -231,104 +100,10 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends @@ -231,104 +100,10 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
231 return data.getData(); 100 return data.getData();
232 } 101 }
233 102
234 - @Data  
235 - private static class DynamicValueKeySub {  
236 - private final DynamicValueKey key;  
237 - private final EntityId entityId;  
238 - private long lastUpdateTs;  
239 - private String lastUpdateValue;  
240 -  
241 - boolean updateValue(TsValue value) {  
242 - if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) {  
243 - this.lastUpdateTs = value.getTs();  
244 - this.lastUpdateValue = value.getValue();  
245 - return true;  
246 - } else {  
247 - return false;  
248 - }  
249 - }  
250 - }  
251 -  
252 - private ListenableFuture<DynamicValueKeySub> resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) {  
253 - ListenableFuture<Optional<AttributeKvEntry>> entry = attributesService.find(tenantId, entityId,  
254 - TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute());  
255 - return Futures.transform(entry, attributeOpt -> {  
256 - DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId);  
257 - if (attributeOpt.isPresent()) {  
258 - AttributeKvEntry attribute = attributeOpt.get();  
259 - sub.setLastUpdateTs(attribute.getLastUpdateTs());  
260 - sub.setLastUpdateValue(attribute.getValueAsString());  
261 - updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString()));  
262 - }  
263 - return sub;  
264 - }, MoreExecutors.directExecutor());  
265 - }  
266 -  
267 - @SuppressWarnings("unchecked")  
268 - private void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) {  
269 - DynamicValueKey dvk = sub.getKey();  
270 - switch (dvk.getPredicateType()) {  
271 - case STRING:  
272 - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue()));  
273 - break;  
274 - case NUMERIC:  
275 - try {  
276 - Double dValue = Double.parseDouble(tsValue.getValue());  
277 - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue));  
278 - } catch (NumberFormatException e) {  
279 - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null));  
280 - }  
281 - break;  
282 - case BOOLEAN:  
283 - Boolean bValue = Boolean.parseBoolean(tsValue.getValue());  
284 - dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue));  
285 - break;  
286 - }  
287 - }  
288 -  
289 - @SuppressWarnings("unchecked")  
290 - private void registerDynamicValues(KeyFilterPredicate predicate) {  
291 - switch (predicate.getType()) {  
292 - case STRING:  
293 - case NUMERIC:  
294 - case BOOLEAN:  
295 - Optional<DynamicValue> value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate);  
296 - if (value.isPresent()) {  
297 - DynamicValue dynamicValue = value.get();  
298 - DynamicValueKey key = new DynamicValueKey(  
299 - predicate.getType(),  
300 - dynamicValue.getSourceType(),  
301 - dynamicValue.getSourceAttribute());  
302 - dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue);  
303 - }  
304 - break;  
305 - case COMPLEX:  
306 - ((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues);  
307 - }  
308 - }  
309 -  
310 - private Optional<DynamicValue<T>> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate<T> predicate) {  
311 - if (predicate.getValue().getUserValue() == null) {  
312 - return Optional.ofNullable(predicate.getValue().getDynamicValue());  
313 - } else {  
314 - return Optional.empty();  
315 - }  
316 - }  
317 -  
318 - public String getSessionId() {  
319 - return sessionRef.getSessionId();  
320 - }  
321 -  
322 - public TenantId getTenantId() {  
323 - return sessionRef.getSecurityCtx().getTenantId();  
324 - }  
325 -  
326 - public CustomerId getCustomerId() {  
327 - return sessionRef.getSecurityCtx().getCustomerId();  
328 - }  
329 -  
330 - public UserId getUserId() {  
331 - return sessionRef.getSecurityCtx().getId(); 103 + @Override
  104 + public void clearSubscriptions() {
  105 + clearEntitySubscriptions();
  106 + super.clearSubscriptions();
332 } 107 }
333 108
334 public void clearEntitySubscriptions() { 109 public void clearEntitySubscriptions() {
@@ -340,26 +115,6 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends @@ -340,26 +115,6 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
340 } 115 }
341 } 116 }
342 117
343 - public void clearDynamicValueSubscriptions() {  
344 - if (subToDynamicValueKeySet != null) {  
345 - for (Integer subId : subToDynamicValueKeySet) {  
346 - localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId);  
347 - }  
348 - subToDynamicValueKeySet.clear();  
349 - }  
350 - }  
351 -  
352 - public void setRefreshTask(ScheduledFuture<?> task) {  
353 - this.refreshTask = task;  
354 - }  
355 -  
356 - public void cancelTasks() {  
357 - if (this.refreshTask != null) {  
358 - log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId);  
359 - this.refreshTask.cancel(true);  
360 - }  
361 - }  
362 -  
363 public void createSubscriptions(List<EntityKey> keys, boolean resultToLatestValues) { 118 public void createSubscriptions(List<EntityKey> keys, boolean resultToLatestValues) {
364 Map<EntityKeyType, List<EntityKey>> keysByType = getEntityKeyByTypeMap(keys); 119 Map<EntityKeyType, List<EntityKey>> keysByType = getEntityKeyByTypeMap(keys);
365 for (EntityData entityData : data.getData()) { 120 for (EntityData entityData : data.getData()) {
@@ -459,14 +214,4 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends @@ -459,14 +214,4 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
459 214
460 abstract void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues); 215 abstract void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate, EntityKeyType keyType, boolean resultToLatestValues);
461 216
462 - @Data  
463 - private static class DynamicValueKey {  
464 - @Getter  
465 - private final FilterPredicateType predicateType;  
466 - @Getter  
467 - private final DynamicValueSourceType sourceType;  
468 - @Getter  
469 - private final String sourceAttribute;  
470 - }  
471 -  
472 } 217 }
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.subscription;
  17 +
  18 +import com.google.common.util.concurrent.Futures;
  19 +import com.google.common.util.concurrent.ListenableFuture;
  20 +import com.google.common.util.concurrent.MoreExecutors;
  21 +import lombok.Data;
  22 +import lombok.Getter;
  23 +import lombok.Setter;
  24 +import lombok.extern.slf4j.Slf4j;
  25 +import org.thingsboard.server.common.data.id.CustomerId;
  26 +import org.thingsboard.server.common.data.id.EntityId;
  27 +import org.thingsboard.server.common.data.id.TenantId;
  28 +import org.thingsboard.server.common.data.id.UserId;
  29 +import org.thingsboard.server.common.data.kv.AttributeKvEntry;
  30 +import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
  31 +import org.thingsboard.server.common.data.query.DynamicValue;
  32 +import org.thingsboard.server.common.data.query.DynamicValueSourceType;
  33 +import org.thingsboard.server.common.data.query.EntityCountQuery;
  34 +import org.thingsboard.server.common.data.query.EntityKeyType;
  35 +import org.thingsboard.server.common.data.query.FilterPredicateType;
  36 +import org.thingsboard.server.common.data.query.KeyFilter;
  37 +import org.thingsboard.server.common.data.query.KeyFilterPredicate;
  38 +import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
  39 +import org.thingsboard.server.common.data.query.TsValue;
  40 +import org.thingsboard.server.dao.attributes.AttributesService;
  41 +import org.thingsboard.server.dao.entity.EntityService;
  42 +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
  43 +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
  44 +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
  45 +
  46 +import java.util.ArrayList;
  47 +import java.util.HashMap;
  48 +import java.util.List;
  49 +import java.util.Map;
  50 +import java.util.Optional;
  51 +import java.util.Set;
  52 +import java.util.concurrent.ConcurrentHashMap;
  53 +import java.util.concurrent.ExecutionException;
  54 +import java.util.concurrent.ScheduledFuture;
  55 +
  56 +@Slf4j
  57 +@Data
  58 +public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
  59 +
  60 + protected final String serviceId;
  61 + protected final SubscriptionServiceStatistics stats;
  62 + protected final TelemetryWebSocketService wsService;
  63 + protected final EntityService entityService;
  64 + protected final TbLocalSubscriptionService localSubscriptionService;
  65 + protected final AttributesService attributesService;
  66 + protected final TelemetryWebSocketSessionRef sessionRef;
  67 + protected final int cmdId;
  68 + protected final Set<Integer> subToDynamicValueKeySet;
  69 + @Getter
  70 + protected final Map<DynamicValueKey, List<DynamicValue>> dynamicValues;
  71 + @Getter
  72 + @Setter
  73 + protected T query;
  74 + @Setter
  75 + protected volatile ScheduledFuture<?> refreshTask;
  76 +
  77 + public TbAbstractSubCtx(String serviceId, TelemetryWebSocketService wsService,
  78 + EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
  79 + AttributesService attributesService, SubscriptionServiceStatistics stats,
  80 + TelemetryWebSocketSessionRef sessionRef, int cmdId) {
  81 + this.serviceId = serviceId;
  82 + this.wsService = wsService;
  83 + this.entityService = entityService;
  84 + this.localSubscriptionService = localSubscriptionService;
  85 + this.attributesService = attributesService;
  86 + this.stats = stats;
  87 + this.sessionRef = sessionRef;
  88 + this.cmdId = cmdId;
  89 + this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet();
  90 + this.dynamicValues = new ConcurrentHashMap<>();
  91 + }
  92 +
  93 + public void setAndResolveQuery(T query) {
  94 + dynamicValues.clear();
  95 + this.query = query;
  96 + if (query != null && query.getKeyFilters() != null) {
  97 + for (KeyFilter filter : query.getKeyFilters()) {
  98 + registerDynamicValues(filter.getPredicate());
  99 + }
  100 + }
  101 + resolve(getTenantId(), getCustomerId(), getUserId());
  102 + }
  103 +
  104 + public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) {
  105 + List<ListenableFuture<DynamicValueKeySub>> futures = new ArrayList<>();
  106 + for (DynamicValueKey key : dynamicValues.keySet()) {
  107 + switch (key.getSourceType()) {
  108 + case CURRENT_TENANT:
  109 + futures.add(resolveEntityValue(tenantId, tenantId, key));
  110 + break;
  111 + case CURRENT_CUSTOMER:
  112 + if (customerId != null && !customerId.isNullUid()) {
  113 + futures.add(resolveEntityValue(tenantId, customerId, key));
  114 + }
  115 + break;
  116 + case CURRENT_USER:
  117 + if (userId != null && !userId.isNullUid()) {
  118 + futures.add(resolveEntityValue(tenantId, userId, key));
  119 + }
  120 + break;
  121 + }
  122 + }
  123 + try {
  124 + Map<EntityId, Map<String, DynamicValueKeySub>> tmpSubMap = new HashMap<>();
  125 + for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) {
  126 + tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub);
  127 + }
  128 + for (EntityId entityId : tmpSubMap.keySet()) {
  129 + Map<String, Long> keyStates = new HashMap<>();
  130 + Map<String, DynamicValueKeySub> dynamicValueKeySubMap = tmpSubMap.get(entityId);
  131 + dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs()));
  132 + int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
  133 + TbAttributeSubscription sub = TbAttributeSubscription.builder()
  134 + .serviceId(serviceId)
  135 + .sessionId(sessionRef.getSessionId())
  136 + .subscriptionId(subIdx)
  137 + .tenantId(sessionRef.getSecurityCtx().getTenantId())
  138 + .entityId(entityId)
  139 + .updateConsumer((s, subscriptionUpdate) -> dynamicValueSubUpdate(s, subscriptionUpdate, dynamicValueKeySubMap))
  140 + .allKeys(false)
  141 + .keyStates(keyStates)
  142 + .scope(TbAttributeSubscriptionScope.SERVER_SCOPE)
  143 + .build();
  144 + subToDynamicValueKeySet.add(subIdx);
  145 + localSubscriptionService.addSubscription(sub);
  146 + }
  147 + } catch (InterruptedException | ExecutionException e) {
  148 + log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet());
  149 + }
  150 +
  151 + }
  152 +
  153 + private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate,
  154 + Map<String, DynamicValueKeySub> dynamicValueKeySubMap) {
  155 + Map<String, TsValue> latestUpdate = new HashMap<>();
  156 + subscriptionUpdate.getData().forEach((k, v) -> {
  157 + Object[] data = (Object[]) v.get(0);
  158 + latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));
  159 + });
  160 +
  161 + boolean invalidateFilter = false;
  162 + for (Map.Entry<String, TsValue> entry : latestUpdate.entrySet()) {
  163 + String k = entry.getKey();
  164 + TsValue tsValue = entry.getValue();
  165 + DynamicValueKeySub sub = dynamicValueKeySubMap.get(k);
  166 + if (sub.updateValue(tsValue)) {
  167 + invalidateFilter = true;
  168 + updateDynamicValuesByKey(sub, tsValue);
  169 + }
  170 + }
  171 +
  172 + if (invalidateFilter) {
  173 + update();
  174 + }
  175 + }
  176 +
  177 + public abstract void fetchData();
  178 +
  179 + protected abstract void update();
  180 +
  181 + public void clearSubscriptions() {
  182 + clearDynamicValueSubscriptions();
  183 + }
  184 +
  185 + @Data
  186 + private static class DynamicValueKeySub {
  187 + private final DynamicValueKey key;
  188 + private final EntityId entityId;
  189 + private long lastUpdateTs;
  190 + private String lastUpdateValue;
  191 +
  192 + boolean updateValue(TsValue value) {
  193 + if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) {
  194 + this.lastUpdateTs = value.getTs();
  195 + this.lastUpdateValue = value.getValue();
  196 + return true;
  197 + } else {
  198 + return false;
  199 + }
  200 + }
  201 + }
  202 +
  203 + private ListenableFuture<DynamicValueKeySub> resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) {
  204 + ListenableFuture<Optional<AttributeKvEntry>> entry = attributesService.find(tenantId, entityId,
  205 + TbAttributeSubscriptionScope.SERVER_SCOPE.name(), key.getSourceAttribute());
  206 + return Futures.transform(entry, attributeOpt -> {
  207 + DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId);
  208 + if (attributeOpt.isPresent()) {
  209 + AttributeKvEntry attribute = attributeOpt.get();
  210 + sub.setLastUpdateTs(attribute.getLastUpdateTs());
  211 + sub.setLastUpdateValue(attribute.getValueAsString());
  212 + updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString()));
  213 + }
  214 + return sub;
  215 + }, MoreExecutors.directExecutor());
  216 + }
  217 +
  218 + @SuppressWarnings("unchecked")
  219 + protected void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) {
  220 + DynamicValueKey dvk = sub.getKey();
  221 + switch (dvk.getPredicateType()) {
  222 + case STRING:
  223 + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue()));
  224 + break;
  225 + case NUMERIC:
  226 + try {
  227 + Double dValue = Double.parseDouble(tsValue.getValue());
  228 + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue));
  229 + } catch (NumberFormatException e) {
  230 + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null));
  231 + }
  232 + break;
  233 + case BOOLEAN:
  234 + Boolean bValue = Boolean.parseBoolean(tsValue.getValue());
  235 + dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue));
  236 + break;
  237 + }
  238 + }
  239 +
  240 + @SuppressWarnings("unchecked")
  241 + private void registerDynamicValues(KeyFilterPredicate predicate) {
  242 + switch (predicate.getType()) {
  243 + case STRING:
  244 + case NUMERIC:
  245 + case BOOLEAN:
  246 + Optional<DynamicValue> value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate);
  247 + if (value.isPresent()) {
  248 + DynamicValue dynamicValue = value.get();
  249 + DynamicValueKey key = new DynamicValueKey(
  250 + predicate.getType(),
  251 + dynamicValue.getSourceType(),
  252 + dynamicValue.getSourceAttribute());
  253 + dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue);
  254 + }
  255 + break;
  256 + case COMPLEX:
  257 + ((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues);
  258 + }
  259 + }
  260 +
  261 + private Optional<DynamicValue<T>> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate<T> predicate) {
  262 + if (predicate.getValue().getUserValue() == null) {
  263 + return Optional.ofNullable(predicate.getValue().getDynamicValue());
  264 + } else {
  265 + return Optional.empty();
  266 + }
  267 + }
  268 +
  269 + public String getSessionId() {
  270 + return sessionRef.getSessionId();
  271 + }
  272 +
  273 + public TenantId getTenantId() {
  274 + return sessionRef.getSecurityCtx().getTenantId();
  275 + }
  276 +
  277 + public CustomerId getCustomerId() {
  278 + return sessionRef.getSecurityCtx().getCustomerId();
  279 + }
  280 +
  281 + public UserId getUserId() {
  282 + return sessionRef.getSecurityCtx().getId();
  283 + }
  284 +
  285 + protected void clearDynamicValueSubscriptions() {
  286 + if (subToDynamicValueKeySet != null) {
  287 + for (Integer subId : subToDynamicValueKeySet) {
  288 + localSubscriptionService.cancelSubscription(sessionRef.getSessionId(), subId);
  289 + }
  290 + subToDynamicValueKeySet.clear();
  291 + }
  292 + }
  293 +
  294 + public void setRefreshTask(ScheduledFuture<?> task) {
  295 + this.refreshTask = task;
  296 + }
  297 +
  298 + public void cancelTasks() {
  299 + if (this.refreshTask != null) {
  300 + log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId);
  301 + this.refreshTask.cancel(true);
  302 + }
  303 + }
  304 +
  305 + @Data
  306 + public static class DynamicValueKey {
  307 + @Getter
  308 + private final FilterPredicateType predicateType;
  309 + @Getter
  310 + private final DynamicValueSourceType sourceType;
  311 + @Getter
  312 + private final String sourceAttribute;
  313 + }
  314 +
  315 +}
@@ -90,8 +90,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> { @@ -90,8 +90,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
90 AlarmDataUpdate update; 90 AlarmDataUpdate update;
91 if (!entitiesMap.isEmpty()) { 91 if (!entitiesMap.isEmpty()) {
92 long start = System.currentTimeMillis(); 92 long start = System.currentTimeMillis();
93 - PageData<AlarmData> alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(),  
94 - query, getOrderedEntityIds()); 93 + PageData<AlarmData> alarms = alarmService.findAlarmDataByQueryForEntities(getTenantId(), getCustomerId(), query, getOrderedEntityIds());
95 long end = System.currentTimeMillis(); 94 long end = System.currentTimeMillis();
96 stats.getAlarmQueryInvocationCnt().incrementAndGet(); 95 stats.getAlarmQueryInvocationCnt().incrementAndGet();
97 stats.getAlarmQueryTimeSpent().addAndGet(end - start); 96 stats.getAlarmQueryTimeSpent().addAndGet(end - start);
  1 +/**
  2 + * Copyright © 2016-2021 The Thingsboard Authors
  3 + *
  4 + * Licensed under the Apache License, Version 2.0 (the "License");
  5 + * you may not use this file except in compliance with the License.
  6 + * You may obtain a copy of the License at
  7 + *
  8 + * http://www.apache.org/licenses/LICENSE-2.0
  9 + *
  10 + * Unless required by applicable law or agreed to in writing, software
  11 + * distributed under the License is distributed on an "AS IS" BASIS,
  12 + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13 + * See the License for the specific language governing permissions and
  14 + * limitations under the License.
  15 + */
  16 +package org.thingsboard.server.service.subscription;
  17 +
  18 +import lombok.extern.slf4j.Slf4j;
  19 +import org.thingsboard.server.common.data.query.EntityCountQuery;
  20 +import org.thingsboard.server.common.data.query.EntityKeyType;
  21 +import org.thingsboard.server.dao.attributes.AttributesService;
  22 +import org.thingsboard.server.dao.entity.EntityService;
  23 +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
  24 +import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
  25 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate;
  26 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
  27 +import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
  28 +
  29 +@Slf4j
  30 +public class TbEntityCountSubCtx extends TbAbstractSubCtx<EntityCountQuery> {
  31 +
  32 + private volatile int result;
  33 +
  34 + public TbEntityCountSubCtx(String serviceId, TelemetryWebSocketService wsService, EntityService entityService,
  35 + TbLocalSubscriptionService localSubscriptionService, AttributesService attributesService,
  36 + SubscriptionServiceStatistics stats, TelemetryWebSocketSessionRef sessionRef, int cmdId) {
  37 + super(serviceId, wsService, entityService, localSubscriptionService, attributesService, stats, sessionRef, cmdId);
  38 + }
  39 +
  40 + @Override
  41 + public void fetchData() {
  42 + result = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query);
  43 + wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result));
  44 + }
  45 +
  46 + @Override
  47 + protected void update() {
  48 + int newCount = (int) entityService.countEntitiesByQuery(getTenantId(), getCustomerId(), query);
  49 + if (newCount != result) {
  50 + result = newCount;
  51 + wsService.sendWsMsg(sessionRef.getSessionId(), new EntityCountUpdate(cmdId, result));
  52 + }
  53 + }
  54 +
  55 +}
@@ -17,6 +17,7 @@ package org.thingsboard.server.service.subscription; @@ -17,6 +17,7 @@ package org.thingsboard.server.service.subscription;
17 17
18 import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; 18 import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
19 import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; 19 import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
  20 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
20 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; 21 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
21 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd; 22 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;
22 import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; 23 import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd;
@@ -25,6 +26,8 @@ public interface TbEntityDataSubscriptionService { @@ -25,6 +26,8 @@ public interface TbEntityDataSubscriptionService {
25 26
26 void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd); 27 void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityDataCmd cmd);
27 28
  29 + void handleCmd(TelemetryWebSocketSessionRef sessionId, EntityCountCmd cmd);
  30 +
28 void handleCmd(TelemetryWebSocketSessionRef sessionId, AlarmDataCmd cmd); 31 void handleCmd(TelemetryWebSocketSessionRef sessionId, AlarmDataCmd cmd);
29 32
30 void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId); 33 void cancelSubscription(String sessionId, UnsubscribeCmd subscriptionId);
@@ -51,22 +51,22 @@ import org.thingsboard.server.service.security.ValidationResult; @@ -51,22 +51,22 @@ import org.thingsboard.server.service.security.ValidationResult;
51 import org.thingsboard.server.service.security.ValidationResultCode; 51 import org.thingsboard.server.service.security.ValidationResultCode;
52 import org.thingsboard.server.service.security.model.UserPrincipal; 52 import org.thingsboard.server.service.security.model.UserPrincipal;
53 import org.thingsboard.server.service.security.permission.Operation; 53 import org.thingsboard.server.service.security.permission.Operation;
  54 +import org.thingsboard.server.service.subscription.TbAttributeSubscription;
  55 +import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
54 import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService; 56 import org.thingsboard.server.service.subscription.TbEntityDataSubscriptionService;
55 import org.thingsboard.server.service.subscription.TbLocalSubscriptionService; 57 import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
56 -import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;  
57 -import org.thingsboard.server.service.subscription.TbAttributeSubscription;  
58 import org.thingsboard.server.service.subscription.TbTimeseriesSubscription; 58 import org.thingsboard.server.service.subscription.TbTimeseriesSubscription;
  59 +import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
59 import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd; 60 import org.thingsboard.server.service.telemetry.cmd.v1.AttributesSubscriptionCmd;
60 import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd; 61 import org.thingsboard.server.service.telemetry.cmd.v1.GetHistoryCmd;
61 import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd; 62 import org.thingsboard.server.service.telemetry.cmd.v1.SubscriptionCmd;
62 import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd; 63 import org.thingsboard.server.service.telemetry.cmd.v1.TelemetryPluginCmd;
63 -import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;  
64 import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd; 64 import org.thingsboard.server.service.telemetry.cmd.v1.TimeseriesSubscriptionCmd;
65 import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd; 65 import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
66 -import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUnsubscribeCmd; 66 +import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate;
67 import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; 67 import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate;
  68 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
68 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; 69 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
69 -import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUnsubscribeCmd;  
70 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; 70 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
71 import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd; 71 import org.thingsboard.server.service.telemetry.cmd.v2.UnsubscribeCmd;
72 import org.thingsboard.server.service.telemetry.exception.UnauthorizedException; 72 import org.thingsboard.server.service.telemetry.exception.UnauthorizedException;
@@ -216,12 +216,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -216,12 +216,18 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
216 if (cmdsWrapper.getAlarmDataCmds() != null) { 216 if (cmdsWrapper.getAlarmDataCmds() != null) {
217 cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd)); 217 cmdsWrapper.getAlarmDataCmds().forEach(cmd -> handleWsAlarmDataCmd(sessionRef, cmd));
218 } 218 }
  219 + if (cmdsWrapper.getEntityCountCmds() != null) {
  220 + cmdsWrapper.getEntityCountCmds().forEach(cmd -> handleWsEntityCountCmd(sessionRef, cmd));
  221 + }
219 if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) { 222 if (cmdsWrapper.getEntityDataUnsubscribeCmds() != null) {
220 cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); 223 cmdsWrapper.getEntityDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
221 } 224 }
222 if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) { 225 if (cmdsWrapper.getAlarmDataUnsubscribeCmds() != null) {
223 cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd)); 226 cmdsWrapper.getAlarmDataUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
224 } 227 }
  228 + if (cmdsWrapper.getEntityCountUnsubscribeCmds() != null) {
  229 + cmdsWrapper.getEntityCountUnsubscribeCmds().forEach(cmd -> handleWsDataUnsubscribeCmd(sessionRef, cmd));
  230 + }
225 } 231 }
226 } catch (IOException e) { 232 } catch (IOException e) {
227 log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e); 233 log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
@@ -239,6 +245,16 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -239,6 +245,16 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
239 } 245 }
240 } 246 }
241 247
  248 + private void handleWsEntityCountCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
  249 + String sessionId = sessionRef.getSessionId();
  250 + log.debug("[{}] Processing: {}", sessionId, cmd);
  251 +
  252 + if (validateSessionMetadata(sessionRef, cmd.getCmdId(), sessionId)
  253 + && validateSubscriptionCmd(sessionRef, cmd)) {
  254 + entityDataSubService.handleCmd(sessionRef, cmd);
  255 + }
  256 + }
  257 +
242 private void handleWsAlarmDataCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { 258 private void handleWsAlarmDataCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
243 String sessionId = sessionRef.getSessionId(); 259 String sessionId = sessionRef.getSessionId();
244 log.debug("[{}] Processing: {}", sessionId, cmd); 260 log.debug("[{}] Processing: {}", sessionId, cmd);
@@ -264,7 +280,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -264,7 +280,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
264 } 280 }
265 281
266 @Override 282 @Override
267 - public void sendWsMsg(String sessionId, DataUpdate update) { 283 + public void sendWsMsg(String sessionId, CmdUpdate update) {
268 sendWsMsg(sessionId, update.getCmdId(), update); 284 sendWsMsg(sessionId, update.getCmdId(), update);
269 } 285 }
270 286
@@ -679,6 +695,20 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @@ -679,6 +695,20 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
679 return true; 695 return true;
680 } 696 }
681 697
  698 + private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, EntityCountCmd cmd) {
  699 + if (cmd.getCmdId() < 0) {
  700 + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
  701 + "Cmd id is negative value!");
  702 + sendWsMsg(sessionRef, update);
  703 + return false;
  704 + } else if (cmd.getQuery() == null) {
  705 + TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, "Query is empty!");
  706 + sendWsMsg(sessionRef, update);
  707 + return false;
  708 + }
  709 + return true;
  710 + }
  711 +
682 private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) { 712 private boolean validateSubscriptionCmd(TelemetryWebSocketSessionRef sessionRef, AlarmDataCmd cmd) {
683 if (cmd.getCmdId() < 0) { 713 if (cmd.getCmdId() < 0) {
684 TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST, 714 TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
@@ -15,6 +15,7 @@ @@ -15,6 +15,7 @@
15 */ 15 */
16 package org.thingsboard.server.service.telemetry; 16 package org.thingsboard.server.service.telemetry;
17 17
  18 +import org.thingsboard.server.service.telemetry.cmd.v2.CmdUpdate;
18 import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate; 19 import org.thingsboard.server.service.telemetry.cmd.v2.DataUpdate;
19 import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate; 20 import org.thingsboard.server.service.telemetry.sub.TelemetrySubscriptionUpdate;
20 21
@@ -29,6 +30,6 @@ public interface TelemetryWebSocketService { @@ -29,6 +30,6 @@ public interface TelemetryWebSocketService {
29 30
30 void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update); 31 void sendWsMsg(String sessionId, TelemetrySubscriptionUpdate update);
31 32
32 - void sendWsMsg(String sessionId, DataUpdate update); 33 + void sendWsMsg(String sessionId, CmdUpdate update);
33 34
34 } 35 }
@@ -15,11 +15,13 @@ @@ -15,11 +15,13 @@
15 */ 15 */
16 package org.thingsboard.server.service.telemetry.cmd.v2; 16 package org.thingsboard.server.service.telemetry.cmd.v2;
17 17
  18 +import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
18 import lombok.AllArgsConstructor; 19 import lombok.AllArgsConstructor;
19 import lombok.Data; 20 import lombok.Data;
20 21
21 @Data 22 @Data
22 @AllArgsConstructor 23 @AllArgsConstructor
  24 +@JsonIgnoreProperties(ignoreUnknown = true)
23 public abstract class CmdUpdate { 25 public abstract class CmdUpdate {
24 26
25 private final int cmdId; 27 private final int cmdId;
@@ -15,18 +15,12 @@ @@ -15,18 +15,12 @@
15 */ 15 */
16 package org.thingsboard.server.service.telemetry.cmd.v2; 16 package org.thingsboard.server.service.telemetry.cmd.v2;
17 17
18 -import com.fasterxml.jackson.annotation.JsonIgnoreProperties;  
19 -import lombok.AllArgsConstructor;  
20 -import lombok.Data;  
21 -import lombok.EqualsAndHashCode;  
22 import lombok.Getter; 18 import lombok.Getter;
23 -import lombok.ToString;  
24 import org.thingsboard.server.common.data.page.PageData; 19 import org.thingsboard.server.common.data.page.PageData;
25 import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; 20 import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode;
26 21
27 import java.util.List; 22 import java.util.List;
28 23
29 -@JsonIgnoreProperties(ignoreUnknown = true)  
30 public abstract class DataUpdate<T> extends CmdUpdate { 24 public abstract class DataUpdate<T> extends CmdUpdate {
31 25
32 @Getter 26 @Getter
@@ -35,16 +35,23 @@ import org.thingsboard.server.common.data.kv.LongDataEntry; @@ -35,16 +35,23 @@ import org.thingsboard.server.common.data.kv.LongDataEntry;
35 import org.thingsboard.server.common.data.kv.TsKvEntry; 35 import org.thingsboard.server.common.data.kv.TsKvEntry;
36 import org.thingsboard.server.common.data.page.PageData; 36 import org.thingsboard.server.common.data.page.PageData;
37 import org.thingsboard.server.common.data.query.DeviceTypeFilter; 37 import org.thingsboard.server.common.data.query.DeviceTypeFilter;
  38 +import org.thingsboard.server.common.data.query.EntityCountQuery;
38 import org.thingsboard.server.common.data.query.EntityData; 39 import org.thingsboard.server.common.data.query.EntityData;
39 import org.thingsboard.server.common.data.query.EntityDataPageLink; 40 import org.thingsboard.server.common.data.query.EntityDataPageLink;
40 import org.thingsboard.server.common.data.query.EntityDataQuery; 41 import org.thingsboard.server.common.data.query.EntityDataQuery;
41 import org.thingsboard.server.common.data.query.EntityKey; 42 import org.thingsboard.server.common.data.query.EntityKey;
42 import org.thingsboard.server.common.data.query.EntityKeyType; 43 import org.thingsboard.server.common.data.query.EntityKeyType;
  44 +import org.thingsboard.server.common.data.query.EntityKeyValueType;
  45 +import org.thingsboard.server.common.data.query.FilterPredicateValue;
  46 +import org.thingsboard.server.common.data.query.KeyFilter;
  47 +import org.thingsboard.server.common.data.query.NumericFilterPredicate;
43 import org.thingsboard.server.common.data.query.TsValue; 48 import org.thingsboard.server.common.data.query.TsValue;
44 import org.thingsboard.server.common.data.security.Authority; 49 import org.thingsboard.server.common.data.security.Authority;
45 import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope; 50 import org.thingsboard.server.service.subscription.TbAttributeSubscriptionScope;
46 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; 51 import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
47 import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; 52 import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper;
  53 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
  54 +import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountUpdate;
48 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; 55 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd;
49 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; 56 import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate;
50 import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd; 57 import org.thingsboard.server.service.telemetry.cmd.v2.EntityHistoryCmd;
@@ -244,6 +251,98 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { @@ -244,6 +251,98 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest {
244 } 251 }
245 252
246 @Test 253 @Test
  254 + public void testEntityCountWsCmd() throws Exception {
  255 + Device device = new Device();
  256 + device.setName("Device");
  257 + device.setType("default");
  258 + device.setLabel("testLabel" + (int) (Math.random() * 1000));
  259 + device = doPost("/api/device", device, Device.class);
  260 +
  261 + AttributeKvEntry dataPoint1 = new BaseAttributeKvEntry(System.currentTimeMillis(), new LongDataEntry("temperature", 42L));
  262 + sendAttributes(device, TbAttributeSubscriptionScope.SERVER_SCOPE, Collections.singletonList(dataPoint1));
  263 +
  264 + DeviceTypeFilter dtf1 = new DeviceTypeFilter();
  265 + dtf1.setDeviceNameFilter("D");
  266 + dtf1.setDeviceType("default");
  267 + EntityCountQuery edq1 = new EntityCountQuery(dtf1, Collections.emptyList());
  268 +
  269 + EntityCountCmd cmd1 = new EntityCountCmd(1, edq1);
  270 +
  271 + TelemetryPluginCmdsWrapper wrapper1 = new TelemetryPluginCmdsWrapper();
  272 + wrapper1.setEntityCountCmds(Collections.singletonList(cmd1));
  273 +
  274 + wsClient.send(mapper.writeValueAsString(wrapper1));
  275 + String msg1 = wsClient.waitForReply();
  276 + EntityCountUpdate update1 = mapper.readValue(msg1, EntityCountUpdate.class);
  277 + Assert.assertEquals(1, update1.getCmdId());
  278 + Assert.assertEquals(1, update1.getCount());
  279 +
  280 + DeviceTypeFilter dtf2 = new DeviceTypeFilter();
  281 + dtf2.setDeviceNameFilter("D");
  282 + dtf2.setDeviceType("non-existing-device-type");
  283 + EntityCountQuery edq2 = new EntityCountQuery(dtf2, Collections.emptyList());
  284 +
  285 + EntityCountCmd cmd2 = new EntityCountCmd(2, edq2);
  286 +
  287 + TelemetryPluginCmdsWrapper wrapper2 = new TelemetryPluginCmdsWrapper();
  288 + wrapper2.setEntityCountCmds(Collections.singletonList(cmd2));
  289 + wsClient.send(mapper.writeValueAsString(wrapper2));
  290 +
  291 + String msg2 = wsClient.waitForReply();
  292 + EntityCountUpdate update2 = mapper.readValue(msg2, EntityCountUpdate.class);
  293 + Assert.assertEquals(2, update2.getCmdId());
  294 + Assert.assertEquals(0, update2.getCount());
  295 +
  296 + KeyFilter highTemperatureFilter = new KeyFilter();
  297 + highTemperatureFilter.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
  298 + NumericFilterPredicate predicate = new NumericFilterPredicate();
  299 + predicate.setValue(FilterPredicateValue.fromDouble(40));
  300 + predicate.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  301 + highTemperatureFilter.setPredicate(predicate);
  302 + highTemperatureFilter.setValueType(EntityKeyValueType.NUMERIC);
  303 +
  304 + DeviceTypeFilter dtf3 = new DeviceTypeFilter();
  305 + dtf3.setDeviceNameFilter("D");
  306 + dtf3.setDeviceType("default");
  307 + EntityCountQuery edq3 = new EntityCountQuery(dtf3, Collections.singletonList(highTemperatureFilter));
  308 +
  309 + EntityCountCmd cmd3 = new EntityCountCmd(3, edq3);
  310 +
  311 + TelemetryPluginCmdsWrapper wrapper3 = new TelemetryPluginCmdsWrapper();
  312 + wrapper3.setEntityCountCmds(Collections.singletonList(cmd3));
  313 + wsClient.send(mapper.writeValueAsString(wrapper3));
  314 +
  315 + String msg3 = wsClient.waitForReply();
  316 + EntityCountUpdate update3 = mapper.readValue(msg3, EntityCountUpdate.class);
  317 + Assert.assertEquals(3, update3.getCmdId());
  318 + Assert.assertEquals(1, update3.getCount());
  319 +
  320 + KeyFilter highTemperatureFilter2 = new KeyFilter();
  321 + highTemperatureFilter2.setKey(new EntityKey(EntityKeyType.ATTRIBUTE, "temperature"));
  322 + NumericFilterPredicate predicate2 = new NumericFilterPredicate();
  323 + predicate2.setValue(FilterPredicateValue.fromDouble(50));
  324 + predicate2.setOperation(NumericFilterPredicate.NumericOperation.GREATER);
  325 + highTemperatureFilter2.setPredicate(predicate2);
  326 + highTemperatureFilter2.setValueType(EntityKeyValueType.NUMERIC);
  327 +
  328 + DeviceTypeFilter dtf4 = new DeviceTypeFilter();
  329 + dtf4.setDeviceNameFilter("D");
  330 + dtf4.setDeviceType("default");
  331 + EntityCountQuery edq4 = new EntityCountQuery(dtf4, Collections.singletonList(highTemperatureFilter2));
  332 +
  333 + EntityCountCmd cmd4 = new EntityCountCmd(4, edq4);
  334 +
  335 + TelemetryPluginCmdsWrapper wrapper4 = new TelemetryPluginCmdsWrapper();
  336 + wrapper4.setEntityCountCmds(Collections.singletonList(cmd4));
  337 + wsClient.send(mapper.writeValueAsString(wrapper4));
  338 +
  339 + String msg4 = wsClient.waitForReply();
  340 + EntityCountUpdate update4 = mapper.readValue(msg4, EntityCountUpdate.class);
  341 + Assert.assertEquals(4, update4.getCmdId());
  342 + Assert.assertEquals(0, update4.getCount());
  343 + }
  344 +
  345 + @Test
247 public void testEntityDataLatestWidgetFlow() throws Exception { 346 public void testEntityDataLatestWidgetFlow() throws Exception {
248 Device device = new Device(); 347 Device device = new Device();
249 device.setName("Device"); 348 device.setName("Device");
@@ -26,9 +26,9 @@ import java.util.Arrays; @@ -26,9 +26,9 @@ import java.util.Arrays;
26 26
27 @RunWith(ClasspathSuite.class) 27 @RunWith(ClasspathSuite.class)
28 @ClasspathSuite.ClassnameFilters({ 28 @ClasspathSuite.ClassnameFilters({
29 -// "org.thingsboard.server.controller.sql.WebsocketApiSqlTest", 29 + "org.thingsboard.server.controller.sql.WebsocketApiSqlTest",
30 // "org.thingsboard.server.controller.sql.TenantProfileControllerSqlTest", 30 // "org.thingsboard.server.controller.sql.TenantProfileControllerSqlTest",
31 - "org.thingsboard.server.controller.sql.*Test", 31 +// "org.thingsboard.server.controller.sql.*Test",
32 }) 32 })
33 public class ControllerSqlTestSuite { 33 public class ControllerSqlTestSuite {
34 34
@@ -249,18 +249,70 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository { @@ -249,18 +249,70 @@ public class DefaultEntityQueryRepository implements EntityQueryRepository {
249 public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) { 249 public long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query) {
250 EntityType entityType = resolveEntityType(query.getEntityFilter()); 250 EntityType entityType = resolveEntityType(query.getEntityFilter());
251 QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType)); 251 QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, customerId, entityType));
252 - ctx.append("select count(e.id) from ");  
253 - ctx.append(addEntityTableQuery(ctx, query.getEntityFilter()));  
254 - ctx.append(" e where ");  
255 - ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList()));  
256 - return transactionTemplate.execute(status -> {  
257 - long startTs = System.currentTimeMillis();  
258 - try {  
259 - return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class);  
260 - } finally {  
261 - queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs); 252 + if (query.getKeyFilters() == null || query.getKeyFilters().isEmpty()) {
  253 + ctx.append("select count(e.id) from ");
  254 + ctx.append(addEntityTableQuery(ctx, query.getEntityFilter()));
  255 + ctx.append(" e where ");
  256 + ctx.append(buildEntityWhere(ctx, query.getEntityFilter(), Collections.emptyList()));
  257 + return transactionTemplate.execute(status -> {
  258 + long startTs = System.currentTimeMillis();
  259 + try {
  260 + return jdbcTemplate.queryForObject(ctx.getQuery(), ctx, Long.class);
  261 + } finally {
  262 + queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs);
  263 + }
  264 + });
  265 + } else {
  266 + List<EntityKeyMapping> mappings = EntityKeyMapping.prepareEntityCountKeyMapping(query);
  267 +
  268 + List<EntityKeyMapping> selectionMapping = mappings.stream().filter(EntityKeyMapping::isSelection)
  269 + .collect(Collectors.toList());
  270 + List<EntityKeyMapping> entityFieldsSelectionMapping = selectionMapping.stream().filter(mapping -> !mapping.isLatest())
  271 + .collect(Collectors.toList());
  272 +
  273 + List<EntityKeyMapping> filterMapping = mappings.stream().filter(EntityKeyMapping::hasFilter)
  274 + .collect(Collectors.toList());
  275 + List<EntityKeyMapping> entityFieldsFiltersMapping = filterMapping.stream().filter(mapping -> !mapping.isLatest())
  276 + .collect(Collectors.toList());
  277 +
  278 + List<EntityKeyMapping> allLatestMappings = mappings.stream().filter(EntityKeyMapping::isLatest)
  279 + .collect(Collectors.toList());
  280 +
  281 +
  282 + String entityWhereClause = DefaultEntityQueryRepository.this.buildEntityWhere(ctx, query.getEntityFilter(), entityFieldsFiltersMapping);
  283 + String latestJoinsCnt = EntityKeyMapping.buildLatestJoins(ctx, query.getEntityFilter(), entityType, allLatestMappings, true);
  284 + String entityFieldsSelection = EntityKeyMapping.buildSelections(entityFieldsSelectionMapping, query.getEntityFilter().getType(), entityType);
  285 + String entityTypeStr;
  286 + if (query.getEntityFilter().getType().equals(EntityFilterType.RELATIONS_QUERY)) {
  287 + entityTypeStr = "e.entity_type";
  288 + } else {
  289 + entityTypeStr = "'" + entityType.name() + "'";
262 } 290 }
263 - }); 291 + if (!StringUtils.isEmpty(entityFieldsSelection)) {
  292 + entityFieldsSelection = String.format("e.id id, %s entity_type, %s", entityTypeStr, entityFieldsSelection);
  293 + } else {
  294 + entityFieldsSelection = String.format("e.id id, %s entity_type", entityTypeStr);
  295 + }
  296 +
  297 + String fromClauseCount = String.format("from (select %s from (select %s from %s e where %s) entities %s ) result %s",
  298 + "entities.*",
  299 + entityFieldsSelection,
  300 + addEntityTableQuery(ctx, query.getEntityFilter()),
  301 + entityWhereClause,
  302 + latestJoinsCnt,
  303 + "");
  304 +
  305 + String countQuery = String.format("select count(id) %s", fromClauseCount);
  306 +
  307 + return transactionTemplate.execute(status -> {
  308 + long startTs = System.currentTimeMillis();
  309 + try {
  310 + return jdbcTemplate.queryForObject(countQuery, ctx, Long.class);
  311 + } finally {
  312 + queryLog.logQuery(ctx, ctx.getQuery(), System.currentTimeMillis() - startTs);
  313 + }
  314 + });
  315 + }
264 } 316 }
265 317
266 @Override 318 @Override
@@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.DataConstants; @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.DataConstants;
21 import org.thingsboard.server.common.data.EntityType; 21 import org.thingsboard.server.common.data.EntityType;
22 import org.thingsboard.server.common.data.query.BooleanFilterPredicate; 22 import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
23 import org.thingsboard.server.common.data.query.ComplexFilterPredicate; 23 import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
  24 +import org.thingsboard.server.common.data.query.EntityCountQuery;
24 import org.thingsboard.server.common.data.query.EntityDataQuery; 25 import org.thingsboard.server.common.data.query.EntityDataQuery;
25 import org.thingsboard.server.common.data.query.EntityDataSortOrder; 26 import org.thingsboard.server.common.data.query.EntityDataSortOrder;
26 import org.thingsboard.server.common.data.query.EntityFilter; 27 import org.thingsboard.server.common.data.query.EntityFilter;
@@ -380,6 +381,30 @@ public class EntityKeyMapping { @@ -380,6 +381,30 @@ public class EntityKeyMapping {
380 return mappings; 381 return mappings;
381 } 382 }
382 383
  384 + public static List<EntityKeyMapping> prepareEntityCountKeyMapping(EntityCountQuery query) {
  385 + Map<EntityKey, List<KeyFilter>> filters =
  386 + query.getKeyFilters() != null ?
  387 + query.getKeyFilters().stream().collect(Collectors.groupingBy(KeyFilter::getKey)) : Collections.emptyMap();
  388 + int index = 2;
  389 + List<EntityKeyMapping> mappings = new ArrayList<>();
  390 + if (!filters.isEmpty()) {
  391 + for (EntityKey filterField : filters.keySet()) {
  392 + EntityKeyMapping mapping = new EntityKeyMapping();
  393 + mapping.setIndex(index);
  394 + mapping.setAlias(String.format("alias%s", index));
  395 + mapping.setKeyFilters(filters.get(filterField));
  396 + mapping.setLatest(!filterField.getType().equals(EntityKeyType.ENTITY_FIELD));
  397 + mapping.setSelection(false);
  398 + mapping.setEntityKey(filterField);
  399 + mappings.add(mapping);
  400 + index += 1;
  401 + }
  402 + }
  403 +
  404 + return mappings;
  405 + }
  406 +
  407 +
383 private String buildAttributeSelection() { 408 private String buildAttributeSelection() {
384 return buildTimeSeriesOrAttrSelection(true); 409 return buildTimeSeriesOrAttrSelection(true);
385 } 410 }