Showing
10 changed files
with
238 additions
and
36 deletions
... | ... | @@ -49,6 +49,7 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService; |
49 | 49 | import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; |
50 | 50 | import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
51 | 51 | import org.thingsboard.server.queue.discovery.PartitionService; |
52 | +import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; | |
52 | 53 | import org.thingsboard.server.queue.util.TbCoreComponent; |
53 | 54 | import org.thingsboard.server.service.queue.TbClusterService; |
54 | 55 | import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; |
... | ... | @@ -106,18 +107,28 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc |
106 | 107 | private SubscriptionManagerService subscriptionManagerService; |
107 | 108 | |
108 | 109 | @Autowired |
110 | + @Lazy | |
111 | + private TbLocalSubscriptionService localSubscriptionService; | |
112 | + | |
113 | + @Autowired | |
109 | 114 | private TimeseriesService tsService; |
110 | 115 | |
116 | + @Autowired | |
117 | + private TbServiceInfoProvider serviceInfoProvider; | |
118 | + | |
111 | 119 | @Value("${database.ts.type}") |
112 | 120 | private String databaseTsType; |
113 | 121 | |
114 | 122 | private ExecutorService wsCallBackExecutor; |
115 | 123 | private boolean tsInSqlDB; |
124 | + private String serviceId; | |
116 | 125 | |
117 | 126 | @PostConstruct |
118 | 127 | public void initExecutor() { |
128 | + serviceId = serviceInfoProvider.getServiceId(); | |
119 | 129 | wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-entity-sub-callback")); |
120 | 130 | tsInSqlDB = databaseTsType.equalsIgnoreCase("sql") || databaseTsType.equalsIgnoreCase("timescale"); |
131 | + | |
121 | 132 | } |
122 | 133 | |
123 | 134 | @PreDestroy |
... | ... | @@ -158,6 +169,9 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc |
158 | 169 | TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId()); |
159 | 170 | if (ctx != null) { |
160 | 171 | log.debug("[{}][{}] Updating existing subscriptions using: {}", session.getSessionId(), cmd.getCmdId(), cmd); |
172 | + if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) { | |
173 | + ctx.clearSubscriptions(); | |
174 | + } | |
161 | 175 | //TODO: cleanup old subscription; |
162 | 176 | } else { |
163 | 177 | log.debug("[{}][{}] Creating new subscription using: {}", session.getSessionId(), cmd.getCmdId(), cmd); |
... | ... | @@ -209,7 +223,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc |
209 | 223 | |
210 | 224 | private TbEntityDataSubCtx createSubCtx(TelemetryWebSocketSessionRef sessionRef, EntityDataCmd cmd) { |
211 | 225 | Map<Integer, TbEntityDataSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new HashMap<>()); |
212 | - TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(sessionRef, cmd.getCmdId()); | |
226 | + TbEntityDataSubCtx ctx = new TbEntityDataSubCtx(serviceId, wsService, sessionRef, cmd.getCmdId()); | |
213 | 227 | ctx.setQuery(cmd.getQuery()); |
214 | 228 | sessionSubs.put(cmd.getCmdId(), ctx); |
215 | 229 | return ctx; |
... | ... | @@ -266,7 +280,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc |
266 | 280 | update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData()); |
267 | 281 | } |
268 | 282 | wsService.sendWsMsg(ctx.getSessionId(), update); |
269 | - //TODO: create context for this (session, cmdId) that contains query, latestCmd and update. Subscribe + periodic updates. | |
283 | + createLatestSubscriptions(ctx, latestCmd); | |
270 | 284 | } |
271 | 285 | |
272 | 286 | @Override |
... | ... | @@ -281,10 +295,16 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc |
281 | 295 | EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null); |
282 | 296 | wsService.sendWsMsg(ctx.getSessionId(), update); |
283 | 297 | } |
284 | - //TODO: create context for this (session, cmdId) that contains query, latestCmd and update. Subscribe + periodic updates. | |
298 | + createLatestSubscriptions(ctx, latestCmd); | |
285 | 299 | } |
286 | 300 | } |
287 | 301 | |
302 | + private void createLatestSubscriptions(TbEntityDataSubCtx ctx, LatestValueCmd latestCmd) { | |
303 | + //TODO: create context for this (session, cmdId) that contains query, latestCmd and update. Subscribe + periodic updates. | |
304 | + List<TbSubscription> tbSubs = ctx.createSubscriptions(latestCmd.getKeys()); | |
305 | + tbSubs.forEach(sub -> localSubscriptionService.addSubscription(sub)); | |
306 | + } | |
307 | + | |
288 | 308 | private Map<String, TsValue> toTsValue(List<TsKvEntry> data) { |
289 | 309 | return data.stream().collect(Collectors.toMap(TsKvEntry::getKey, value -> new TsValue(value.getTs(), value.getValueAsString()))); |
290 | 310 | } | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | * you may not use this file except in compliance with the License. |
6 | 6 | * You may obtain a copy of the License at |
7 | 7 | * |
8 | - * http://www.apache.org/licenses/LICENSE-2.0 | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | 9 | * |
10 | 10 | * Unless required by applicable law or agreed to in writing, software |
11 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
... | ... | @@ -59,9 +59,6 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer |
59 | 59 | private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>(); |
60 | 60 | |
61 | 61 | @Autowired |
62 | - private TelemetryWebSocketService wsService; | |
63 | - | |
64 | - @Autowired | |
65 | 62 | private EntityViewService entityViewService; |
66 | 63 | |
67 | 64 | @Autowired |
... | ... | @@ -155,7 +152,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer |
155 | 152 | update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value)); |
156 | 153 | break; |
157 | 154 | } |
158 | - wsService.sendWsMsg(sessionId, update); | |
155 | + subscription.getUpdateConsumer().accept(sessionId, update); | |
159 | 156 | } |
160 | 157 | callback.onSuccess(); |
161 | 158 | } | ... | ... |
... | ... | @@ -20,8 +20,10 @@ import lombok.Data; |
20 | 20 | import lombok.Getter; |
21 | 21 | import org.thingsboard.server.common.data.id.EntityId; |
22 | 22 | import org.thingsboard.server.common.data.id.TenantId; |
23 | +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; | |
23 | 24 | |
24 | 25 | import java.util.Map; |
26 | +import java.util.function.BiConsumer; | |
25 | 27 | |
26 | 28 | public class TbAttributeSubscription extends TbSubscription { |
27 | 29 | |
... | ... | @@ -31,8 +33,9 @@ public class TbAttributeSubscription extends TbSubscription { |
31 | 33 | |
32 | 34 | @Builder |
33 | 35 | public TbAttributeSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, |
36 | + BiConsumer<String, SubscriptionUpdate> updateConsumer, | |
34 | 37 | boolean allKeys, Map<String, Long> keyStates, TbAttributeSubscriptionScope scope) { |
35 | - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES); | |
38 | + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES, updateConsumer); | |
36 | 39 | this.allKeys = allKeys; |
37 | 40 | this.keyStates = keyStates; |
38 | 41 | this.scope = scope; | ... | ... |
... | ... | @@ -2,17 +2,35 @@ package org.thingsboard.server.service.subscription; |
2 | 2 | |
3 | 3 | import lombok.Data; |
4 | 4 | import org.thingsboard.server.common.data.id.CustomerId; |
5 | +import org.thingsboard.server.common.data.id.EntityId; | |
5 | 6 | import org.thingsboard.server.common.data.id.TenantId; |
6 | 7 | import org.thingsboard.server.common.data.page.PageData; |
7 | 8 | import org.thingsboard.server.common.data.query.EntityData; |
8 | 9 | import org.thingsboard.server.common.data.query.EntityDataQuery; |
10 | +import org.thingsboard.server.common.data.query.EntityKey; | |
11 | +import org.thingsboard.server.common.data.query.EntityKeyType; | |
12 | +import org.thingsboard.server.common.data.query.TsValue; | |
13 | +import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; | |
9 | 14 | import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef; |
15 | +import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; | |
10 | 16 | import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; |
11 | 17 | import org.thingsboard.server.service.telemetry.cmd.v2.TimeSeriesCmd; |
18 | +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; | |
19 | + | |
20 | +import java.util.ArrayList; | |
21 | +import java.util.Arrays; | |
22 | +import java.util.Collections; | |
23 | +import java.util.HashMap; | |
24 | +import java.util.List; | |
25 | +import java.util.Map; | |
26 | +import java.util.stream.Collectors; | |
12 | 27 | |
13 | 28 | @Data |
14 | 29 | public class TbEntityDataSubCtx { |
15 | 30 | |
31 | + public static final int MAX_SUBS_PER_CMD = 1024 * 8; | |
32 | + private final String serviceId; | |
33 | + private final TelemetryWebSocketService wsService; | |
16 | 34 | private final TelemetryWebSocketSessionRef sessionRef; |
17 | 35 | private final int cmdId; |
18 | 36 | private EntityDataQuery query; |
... | ... | @@ -20,8 +38,13 @@ public class TbEntityDataSubCtx { |
20 | 38 | private TimeSeriesCmd tsCmd; |
21 | 39 | private PageData<EntityData> data; |
22 | 40 | private boolean initialDataSent; |
41 | + private List<TbSubscription> tbSubs; | |
42 | + private int internalSubIdx; | |
43 | + private Map<Integer, EntityId> subToEntityIdMap; | |
23 | 44 | |
24 | - public TbEntityDataSubCtx(TelemetryWebSocketSessionRef sessionRef, int cmdId) { | |
45 | + public TbEntityDataSubCtx(String serviceId, TelemetryWebSocketService wsService, TelemetryWebSocketSessionRef sessionRef, int cmdId) { | |
46 | + this.serviceId = serviceId; | |
47 | + this.wsService = wsService; | |
25 | 48 | this.sessionRef = sessionRef; |
26 | 49 | this.cmdId = cmdId; |
27 | 50 | } |
... | ... | @@ -38,9 +61,79 @@ public class TbEntityDataSubCtx { |
38 | 61 | return sessionRef.getSecurityCtx().getCustomerId(); |
39 | 62 | } |
40 | 63 | |
41 | - | |
42 | 64 | public void setData(PageData<EntityData> data) { |
43 | 65 | this.data = data; |
44 | 66 | } |
45 | 67 | |
68 | + public List<TbSubscription> createSubscriptions(List<EntityKey> keys) { | |
69 | + this.subToEntityIdMap = new HashMap<>(); | |
70 | + this.internalSubIdx = cmdId * MAX_SUBS_PER_CMD; | |
71 | + tbSubs = new ArrayList<>(); | |
72 | + List<EntityKey> attrSubKeys = new ArrayList<>(); | |
73 | + List<EntityKey> tsSubKeys = new ArrayList<>(); | |
74 | + for (EntityKey key : keys) { | |
75 | + switch (key.getType()) { | |
76 | + case TIME_SERIES: | |
77 | + tsSubKeys.add(key); | |
78 | + break; | |
79 | + case ATTRIBUTE: | |
80 | + case CLIENT_ATTRIBUTE: | |
81 | + case SHARED_ATTRIBUTE: | |
82 | + case SERVER_ATTRIBUTE: | |
83 | + attrSubKeys.add(key); | |
84 | + } | |
85 | + } | |
86 | + for (EntityData entityData : data.getData()) { | |
87 | + if (!tsSubKeys.isEmpty()) { | |
88 | + tbSubs.add(createTsSub(entityData, tsSubKeys)); | |
89 | + } | |
90 | + } | |
91 | + return tbSubs; | |
92 | + } | |
93 | + | |
94 | + private TbSubscription createTsSub(EntityData entityData, List<EntityKey> tsSubKeys) { | |
95 | + int subIdx = internalSubIdx++; | |
96 | + subToEntityIdMap.put(subIdx, entityData.getEntityId()); | |
97 | + Map<String, Long> keyStates = new HashMap<>(); | |
98 | + tsSubKeys.forEach(key -> keyStates.put(key.getKey(), 0L)); | |
99 | + if (entityData.getLatest() != null) { | |
100 | + Map<String, TsValue> currentValues = entityData.getLatest().get(EntityKeyType.TIME_SERIES); | |
101 | + if (currentValues != null) { | |
102 | + currentValues.forEach((k, v) -> keyStates.put(k, v.getTs())); | |
103 | + } | |
104 | + } | |
105 | + if (entityData.getTimeseries() != null) { | |
106 | + entityData.getTimeseries().forEach((k, v) -> keyStates.put(k, Arrays.stream(v).map(TsValue::getTs).max(Long::compareTo).orElse(0L))); | |
107 | + } | |
108 | + | |
109 | + return TbTimeseriesSubscription.builder() | |
110 | + .serviceId(serviceId) | |
111 | + .sessionId(sessionRef.getSessionId()) | |
112 | + .subscriptionId(subIdx) | |
113 | + .tenantId(sessionRef.getSecurityCtx().getTenantId()) | |
114 | + .entityId(entityData.getEntityId()) | |
115 | + .updateConsumer(this::sendTsWsMsg) | |
116 | + .allKeys(false) | |
117 | + .keyStates(keyStates).build(); | |
118 | + } | |
119 | + | |
120 | + | |
121 | + private void sendTsWsMsg(String sessionId, SubscriptionUpdate subscriptionUpdate) { | |
122 | + EntityId entityId = subToEntityIdMap.get(subscriptionUpdate.getSubscriptionId()); | |
123 | + if (entityId != null) { | |
124 | + Map<String, TsValue> latest = new HashMap<>(); | |
125 | + subscriptionUpdate.getData().forEach((k, v) -> { | |
126 | + Object[] data = (Object[]) v.get(0); | |
127 | + latest.put(k, new TsValue((Long) data[0], (String) data[1])); | |
128 | + }); | |
129 | + Map<EntityKeyType, Map<String, TsValue>> latestMap = Collections.singletonMap(EntityKeyType.TIME_SERIES, latest); | |
130 | + EntityData entityData = new EntityData(entityId, latestMap, null); | |
131 | + wsService.sendWsMsg(sessionId, new EntityDataUpdate(cmdId, null, Collections.singletonList(entityData))); | |
132 | + } | |
133 | + | |
134 | + } | |
135 | + | |
136 | + public void clearSubscriptions() { | |
137 | + subToEntityIdMap.clear(); | |
138 | + } | |
46 | 139 | } | ... | ... |
... | ... | @@ -19,8 +19,10 @@ import lombok.AllArgsConstructor; |
19 | 19 | import lombok.Data; |
20 | 20 | import org.thingsboard.server.common.data.id.EntityId; |
21 | 21 | import org.thingsboard.server.common.data.id.TenantId; |
22 | +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; | |
22 | 23 | |
23 | 24 | import java.util.Objects; |
25 | +import java.util.function.BiConsumer; | |
24 | 26 | |
25 | 27 | @Data |
26 | 28 | @AllArgsConstructor |
... | ... | @@ -32,6 +34,7 @@ public abstract class TbSubscription { |
32 | 34 | private final TenantId tenantId; |
33 | 35 | private final EntityId entityId; |
34 | 36 | private final TbSubscriptionType type; |
37 | + private final BiConsumer<String, SubscriptionUpdate> updateConsumer; | |
35 | 38 | |
36 | 39 | @Override |
37 | 40 | public boolean equals(Object o) { | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | * you may not use this file except in compliance with the License. |
6 | 6 | * You may obtain a copy of the License at |
7 | 7 | * |
8 | - * http://www.apache.org/licenses/LICENSE-2.0 | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | 9 | * |
10 | 10 | * Unless required by applicable law or agreed to in writing, software |
11 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
... | ... | @@ -19,20 +19,27 @@ import lombok.Builder; |
19 | 19 | import lombok.Getter; |
20 | 20 | import org.thingsboard.server.common.data.id.EntityId; |
21 | 21 | import org.thingsboard.server.common.data.id.TenantId; |
22 | +import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; | |
22 | 23 | |
23 | 24 | import java.util.Map; |
25 | +import java.util.function.BiConsumer; | |
24 | 26 | |
25 | 27 | public class TbTimeseriesSubscription extends TbSubscription { |
26 | 28 | |
27 | - @Getter private final boolean allKeys; | |
28 | - @Getter private final Map<String, Long> keyStates; | |
29 | - @Getter private final long startTime; | |
30 | - @Getter private final long endTime; | |
29 | + @Getter | |
30 | + private final boolean allKeys; | |
31 | + @Getter | |
32 | + private final Map<String, Long> keyStates; | |
33 | + @Getter | |
34 | + private final long startTime; | |
35 | + @Getter | |
36 | + private final long endTime; | |
31 | 37 | |
32 | 38 | @Builder |
33 | 39 | public TbTimeseriesSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, |
40 | + BiConsumer<String, SubscriptionUpdate> updateConsumer, | |
34 | 41 | boolean allKeys, Map<String, Long> keyStates, long startTime, long endTime) { |
35 | - super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES); | |
42 | + super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES, updateConsumer); | |
36 | 43 | this.allKeys = allKeys; |
37 | 44 | this.keyStates = keyStates; |
38 | 45 | this.startTime = startTime; | ... | ... |
... | ... | @@ -86,6 +86,7 @@ import java.util.concurrent.ConcurrentHashMap; |
86 | 86 | import java.util.concurrent.ConcurrentMap; |
87 | 87 | import java.util.concurrent.ExecutorService; |
88 | 88 | import java.util.concurrent.Executors; |
89 | +import java.util.function.BiConsumer; | |
89 | 90 | import java.util.function.Consumer; |
90 | 91 | import java.util.stream.Collectors; |
91 | 92 | |
... | ... | @@ -129,6 +130,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
129 | 130 | @Autowired |
130 | 131 | private TbServiceInfoProvider serviceInfoProvider; |
131 | 132 | |
133 | + | |
132 | 134 | @Value("${server.ws.limits.max_subscriptions_per_tenant:0}") |
133 | 135 | private int maxSubscriptionsPerTenant; |
134 | 136 | @Value("${server.ws.limits.max_subscriptions_per_customer:0}") |
... | ... | @@ -398,7 +400,9 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
398 | 400 | .entityId(entityId) |
399 | 401 | .allKeys(false) |
400 | 402 | .keyStates(subState) |
401 | - .scope(scope).build(); | |
403 | + .scope(scope) | |
404 | + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) | |
405 | + .build(); | |
402 | 406 | oldSubService.addSubscription(sub); |
403 | 407 | } |
404 | 408 | |
... | ... | @@ -495,6 +499,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
495 | 499 | .entityId(entityId) |
496 | 500 | .allKeys(true) |
497 | 501 | .keyStates(subState) |
502 | + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) | |
498 | 503 | .scope(scope).build(); |
499 | 504 | oldSubService.addSubscription(sub); |
500 | 505 | } |
... | ... | @@ -575,6 +580,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
575 | 580 | .subscriptionId(cmd.getCmdId()) |
576 | 581 | .tenantId(sessionRef.getSecurityCtx().getTenantId()) |
577 | 582 | .entityId(entityId) |
583 | + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) | |
578 | 584 | .allKeys(true) |
579 | 585 | .keyStates(subState).build(); |
580 | 586 | oldSubService.addSubscription(sub); |
... | ... | @@ -612,6 +618,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi |
612 | 618 | .subscriptionId(cmd.getCmdId()) |
613 | 619 | .tenantId(sessionRef.getSecurityCtx().getTenantId()) |
614 | 620 | .entityId(entityId) |
621 | + .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) | |
615 | 622 | .allKeys(false) |
616 | 623 | .keyStates(subState).build(); |
617 | 624 | oldSubService.addSubscription(sub); | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | * you may not use this file except in compliance with the License. |
6 | 6 | * You may obtain a copy of the License at |
7 | 7 | * |
8 | - * http://www.apache.org/licenses/LICENSE-2.0 | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | 9 | * |
10 | 10 | * Unless required by applicable law or agreed to in writing, software |
11 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
... | ... | @@ -15,6 +15,9 @@ |
15 | 15 | */ |
16 | 16 | package org.thingsboard.server.controller; |
17 | 17 | |
18 | +import com.google.common.util.concurrent.FutureCallback; | |
19 | +import lombok.extern.slf4j.Slf4j; | |
20 | +import org.checkerframework.checker.nullness.qual.Nullable; | |
18 | 21 | import org.junit.After; |
19 | 22 | import org.junit.Assert; |
20 | 23 | import org.junit.Before; |
... | ... | @@ -38,6 +41,7 @@ import org.thingsboard.server.common.data.query.EntityKeyType; |
38 | 41 | import org.thingsboard.server.common.data.query.TsValue; |
39 | 42 | import org.thingsboard.server.common.data.security.Authority; |
40 | 43 | import org.thingsboard.server.dao.timeseries.TimeseriesService; |
44 | +import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService; | |
41 | 45 | import org.thingsboard.server.service.telemetry.cmd.TelemetryPluginCmdsWrapper; |
42 | 46 | import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataCmd; |
43 | 47 | import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; |
... | ... | @@ -46,10 +50,13 @@ import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; |
46 | 50 | |
47 | 51 | import java.util.Arrays; |
48 | 52 | import java.util.Collections; |
53 | +import java.util.List; | |
54 | +import java.util.concurrent.CountDownLatch; | |
49 | 55 | import java.util.concurrent.TimeUnit; |
50 | 56 | |
51 | 57 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; |
52 | 58 | |
59 | +@Slf4j | |
53 | 60 | public class BaseWebsocketApiTest extends AbstractWebsocketTest { |
54 | 61 | |
55 | 62 | private Tenant savedTenant; |
... | ... | @@ -57,7 +64,7 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { |
57 | 64 | private TbTestWebSocketClient wsClient; |
58 | 65 | |
59 | 66 | @Autowired |
60 | - private TimeseriesService tsService; | |
67 | + private TelemetrySubscriptionService tsService; | |
61 | 68 | |
62 | 69 | @Before |
63 | 70 | public void beforeTest() throws Exception { |
... | ... | @@ -129,7 +136,10 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { |
129 | 136 | TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); |
130 | 137 | TsKvEntry dataPoint2 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(2), new LongDataEntry("temperature", 42L)); |
131 | 138 | TsKvEntry dataPoint3 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(3), new LongDataEntry("temperature", 42L)); |
132 | - tsService.save(device.getTenantId(), device.getId(), Arrays.asList(dataPoint1, dataPoint2, dataPoint3), 0).get(); | |
139 | + List<TsKvEntry> tsData = Arrays.asList(dataPoint1, dataPoint2, dataPoint3); | |
140 | + | |
141 | + sendTelemetry(device, tsData); | |
142 | + Thread.sleep(1000); | |
133 | 143 | |
134 | 144 | wsClient.send(mapper.writeValueAsString(wrapper)); |
135 | 145 | msg = wsClient.waitForReply(); |
... | ... | @@ -146,6 +156,22 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { |
146 | 156 | Assert.assertEquals(new TsValue(dataPoint3.getTs(), dataPoint3.getValueAsString()), tsArray[2]); |
147 | 157 | } |
148 | 158 | |
159 | + private void sendTelemetry(Device device, List<TsKvEntry> tsData) throws InterruptedException { | |
160 | + CountDownLatch latch = new CountDownLatch(1); | |
161 | + tsService.saveAndNotify(device.getTenantId(), device.getId(), tsData, 0, new FutureCallback<Void>() { | |
162 | + @Override | |
163 | + public void onSuccess(@Nullable Void result) { | |
164 | + latch.countDown(); | |
165 | + } | |
166 | + | |
167 | + @Override | |
168 | + public void onFailure(Throwable t) { | |
169 | + latch.countDown(); | |
170 | + } | |
171 | + }); | |
172 | + latch.await(3, TimeUnit.SECONDS); | |
173 | + } | |
174 | + | |
149 | 175 | @Test |
150 | 176 | @Ignore |
151 | 177 | public void testEntityDataLatestWsCmd() throws Exception { |
... | ... | @@ -177,12 +203,15 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { |
177 | 203 | Assert.assertNotNull(pageData); |
178 | 204 | Assert.assertEquals(1, pageData.getData().size()); |
179 | 205 | Assert.assertEquals(device.getId(), pageData.getData().get(0).getEntityId()); |
180 | - Assert.assertNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); | |
206 | + Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature")); | |
207 | + Assert.assertEquals(0, pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getTs()); | |
208 | + Assert.assertEquals("", pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature").getValue()); | |
181 | 209 | |
182 | 210 | TsKvEntry dataPoint1 = new BasicTsKvEntry(now - TimeUnit.MINUTES.toMillis(1), new LongDataEntry("temperature", 42L)); |
183 | - tsService.save(device.getTenantId(), device.getId(), Arrays.asList(dataPoint1), 0).get(); | |
211 | + List<TsKvEntry> tsData = Arrays.asList(dataPoint1); | |
212 | + sendTelemetry(device, tsData); | |
184 | 213 | |
185 | - cmd = new EntityDataCmd(2, edq, null, latestCmd, null); | |
214 | + cmd = new EntityDataCmd(1, edq, null, latestCmd, null); | |
186 | 215 | |
187 | 216 | wrapper = new TelemetryPluginCmdsWrapper(); |
188 | 217 | wrapper.setEntityDataCmds(Collections.singletonList(cmd)); |
... | ... | @@ -190,7 +219,7 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { |
190 | 219 | wsClient.send(mapper.writeValueAsString(wrapper)); |
191 | 220 | msg = wsClient.waitForReply(); |
192 | 221 | update = mapper.readValue(msg, EntityDataUpdate.class); |
193 | - Assert.assertEquals(2, update.getCmdId()); | |
222 | + Assert.assertEquals(1, update.getCmdId()); | |
194 | 223 | pageData = update.getData(); |
195 | 224 | Assert.assertNotNull(pageData); |
196 | 225 | Assert.assertEquals(1, pageData.getData().size()); |
... | ... | @@ -198,6 +227,22 @@ public class BaseWebsocketApiTest extends AbstractWebsocketTest { |
198 | 227 | Assert.assertNotNull(pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES)); |
199 | 228 | TsValue tsValue = pageData.getData().get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); |
200 | 229 | Assert.assertEquals(new TsValue(dataPoint1.getTs(), dataPoint1.getValueAsString()), tsValue); |
230 | + | |
231 | + log.error("GOING TO LISTEN FOR UPDATES"); | |
232 | + msg = wsClient.waitForUpdate(); | |
233 | + now = System.currentTimeMillis(); | |
234 | + TsKvEntry dataPoint2 = new BasicTsKvEntry(now, new LongDataEntry("temperature", 52L)); | |
235 | + sendTelemetry(device, Arrays.asList(dataPoint2)); | |
236 | + | |
237 | + update = mapper.readValue(msg, EntityDataUpdate.class); | |
238 | + Assert.assertEquals(1, update.getCmdId()); | |
239 | + List<EntityData> eData = update.getUpdate(); | |
240 | + Assert.assertNotNull(eData); | |
241 | + Assert.assertEquals(1, eData.size()); | |
242 | + Assert.assertEquals(device.getId(), eData.get(0).getEntityId()); | |
243 | + Assert.assertNotNull(eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES)); | |
244 | + tsValue = eData.get(0).getLatest().get(EntityKeyType.TIME_SERIES).get("temperature"); | |
245 | + Assert.assertEquals(new TsValue(dataPoint2.getTs(), dataPoint2.getValueAsString()), tsValue); | |
201 | 246 | } |
202 | 247 | |
203 | 248 | } | ... | ... |
... | ... | @@ -5,7 +5,7 @@ |
5 | 5 | * you may not use this file except in compliance with the License. |
6 | 6 | * You may obtain a copy of the License at |
7 | 7 | * |
8 | - * http://www.apache.org/licenses/LICENSE-2.0 | |
8 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
9 | 9 | * |
10 | 10 | * Unless required by applicable law or agreed to in writing, software |
11 | 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
... | ... | @@ -27,9 +27,11 @@ import java.util.concurrent.TimeUnit; |
27 | 27 | @Slf4j |
28 | 28 | public class TbTestWebSocketClient extends WebSocketClient { |
29 | 29 | |
30 | - private volatile String lastMsg; | |
30 | + private volatile String lastReply; | |
31 | + private volatile String lastUpdate; | |
31 | 32 | private volatile boolean replyReceived; |
32 | 33 | private CountDownLatch reply; |
34 | + private CountDownLatch update; | |
33 | 35 | |
34 | 36 | public TbTestWebSocketClient(URI serverUri) { |
35 | 37 | super(serverUri); |
... | ... | @@ -42,11 +44,22 @@ public class TbTestWebSocketClient extends WebSocketClient { |
42 | 44 | |
43 | 45 | @Override |
44 | 46 | public void onMessage(String s) { |
45 | - if (!replyReceived) { | |
46 | - replyReceived = true; | |
47 | - lastMsg = s; | |
48 | - if (reply != null) { | |
49 | - reply.countDown(); | |
47 | + log.error("RECEIVED: {}", s); | |
48 | + synchronized (this) { | |
49 | + if (!replyReceived) { | |
50 | + replyReceived = true; | |
51 | + lastReply = s; | |
52 | + log.error("LAST REPLY: {}", s); | |
53 | + if (reply != null) { | |
54 | + reply.countDown(); | |
55 | + } | |
56 | + } else { | |
57 | + lastUpdate = s; | |
58 | + log.error("LAST UPDATE: {}", s); | |
59 | + if (update == null) { | |
60 | + update = new CountDownLatch(1); | |
61 | + } | |
62 | + update.countDown(); | |
50 | 63 | } |
51 | 64 | } |
52 | 65 | } |
... | ... | @@ -63,17 +76,31 @@ public class TbTestWebSocketClient extends WebSocketClient { |
63 | 76 | |
64 | 77 | @Override |
65 | 78 | public void send(String text) throws NotYetConnectedException { |
66 | - reply = new CountDownLatch(1); | |
67 | - replyReceived = false; | |
79 | + synchronized (this) { | |
80 | + reply = new CountDownLatch(1); | |
81 | + replyReceived = false; | |
82 | + } | |
68 | 83 | super.send(text); |
69 | 84 | } |
70 | 85 | |
86 | + public String waitForUpdate() { | |
87 | + synchronized (this) { | |
88 | + update = new CountDownLatch(1); | |
89 | + } | |
90 | + try { | |
91 | + update.await(3, TimeUnit.SECONDS); | |
92 | + } catch (InterruptedException e) { | |
93 | + log.warn("Failed to await reply", e); | |
94 | + } | |
95 | + return lastUpdate; | |
96 | + } | |
97 | + | |
71 | 98 | public String waitForReply() { |
72 | 99 | try { |
73 | 100 | reply.await(3, TimeUnit.SECONDS); |
74 | 101 | } catch (InterruptedException e) { |
75 | 102 | log.warn("Failed to await reply", e); |
76 | 103 | } |
77 | - return lastMsg; | |
104 | + return lastReply; | |
78 | 105 | } |
79 | 106 | } | ... | ... |
... | ... | @@ -121,7 +121,7 @@ public class EntityKeyMapping { |
121 | 121 | String join = hasFilter() ? "left join" : "left outer join"; |
122 | 122 | ctx.addStringParameter(alias + "_key_id", entityKey.getKey()); |
123 | 123 | if (entityKey.getType().equals(EntityKeyType.TIME_SERIES)) { |
124 | - return String.format("%s ts_kv_latest %s ON %s.entity_id=to_uuid(entities.id) AND %s.key = (select key_id from ts_kv_dictionary where key = :%s_key_id)", | |
124 | + return String.format("%s ts_kv_latest %s ON %s.entity_id=entities.id AND %s.key = (select key_id from ts_kv_dictionary where key = :%s_key_id)", | |
125 | 125 | join, alias, alias, alias, alias); |
126 | 126 | } else { |
127 | 127 | String query = String.format("%s attribute_kv %s ON %s.entity_id=entities.id AND %s.entity_type=%s AND %s.attribute_key=:%s_key_id", | ... | ... |