31 changed files with 1541 additions and 790 deletions
@ -0,0 +1,230 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.context.annotation.Lazy; |
||||
|
import org.springframework.context.event.EventListener; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.EntityView; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityViewId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.dao.entityview.EntityViewService; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos; |
||||
|
import org.thingsboard.server.queue.TbQueueProducer; |
||||
|
import org.thingsboard.server.queue.common.TbProtoQueueMsg; |
||||
|
import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionService; |
||||
|
import org.thingsboard.server.queue.discovery.ServiceType; |
||||
|
import org.thingsboard.server.queue.discovery.TopicPartitionInfo; |
||||
|
import org.thingsboard.server.queue.provider.TbCoreQueueProvider; |
||||
|
import org.thingsboard.server.service.queue.TbMsgCallback; |
||||
|
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService; |
||||
|
import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.util.Collections; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.Map; |
||||
|
import java.util.Set; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.stream.Collectors; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class DefaultLocalSubscriptionService implements LocalSubscriptionService { |
||||
|
|
||||
|
private final Set<TopicPartitionInfo> currentPartitions = ConcurrentHashMap.newKeySet(); |
||||
|
private final Map<String, Map<Integer, TbSubscription>> subscriptionsBySessionId = new ConcurrentHashMap<>(); |
||||
|
|
||||
|
@Autowired |
||||
|
private TelemetryWebSocketService wsService; |
||||
|
|
||||
|
@Autowired |
||||
|
private EntityViewService entityViewService; |
||||
|
|
||||
|
@Autowired |
||||
|
private PartitionService partitionService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TbCoreQueueProvider coreQueueProvider; |
||||
|
|
||||
|
@Autowired |
||||
|
@Lazy |
||||
|
private SubscriptionManagerService subscriptionManagerService; |
||||
|
|
||||
|
private ExecutorService wsCallBackExecutor; |
||||
|
private TbQueueProducer<TbProtoQueueMsg<TransportProtos.ToCoreMsg>> toCoreProducer; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void initExecutor() { |
||||
|
wsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ws-sub-callback")); |
||||
|
toCoreProducer = coreQueueProvider.getTbCoreMsgProducer(); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
public void shutdownExecutor() { |
||||
|
if (wsCallBackExecutor != null) { |
||||
|
wsCallBackExecutor.shutdownNow(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
@EventListener(PartitionChangeEvent.class) |
||||
|
public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { |
||||
|
if (ServiceType.TB_CORE.equals(partitionChangeEvent.getServiceKey().getServiceType())) { |
||||
|
currentPartitions.clear(); |
||||
|
currentPartitions.addAll(partitionChangeEvent.getPartitions()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
@EventListener(ClusterTopologyChangeEvent.class) |
||||
|
public void onApplicationEvent(ClusterTopologyChangeEvent event) { |
||||
|
if (event.getServiceKeys().stream().anyMatch(key -> ServiceType.TB_CORE.equals(key.getServiceType()))) { |
||||
|
/* |
||||
|
* If the cluster topology has changed, we need to push all current subscriptions to SubscriptionManagerService again. |
||||
|
* Otherwise, the SubscriptionManagerService may "forget" those subscriptions in case of restart. |
||||
|
* Although this is resource consuming operation, it is cheaper than sending ping/pong commands periodically |
||||
|
* It is also cheaper then caching the subscriptions by entity id and then lookup of those caches every time we have new telemetry in SubscriptionManagerService. |
||||
|
* Even if we cache locally the list of active subscriptions by entity id, it is still time consuming operation to get them from cache |
||||
|
* Since number of subscriptions is usually much less then number of devices that are pushing data. |
||||
|
*/ |
||||
|
subscriptionsBySessionId.values().forEach(map -> map.values() |
||||
|
.forEach(sub -> pushSubscriptionToManagerService(sub, false))); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//TODO 3.1: replace null callbacks with callbacks from websocket service.
|
||||
|
@Override |
||||
|
public void addSubscription(TbSubscription subscription) { |
||||
|
EntityId entityId = subscription.getEntityId(); |
||||
|
// Telemetry subscription on Entity Views are handled differently, because we need to allow only certain keys and time ranges;
|
||||
|
if (entityId.getEntityType().equals(EntityType.ENTITY_VIEW) && TbSubscriptionType.TIMESERIES.equals(subscription.getType())) { |
||||
|
subscription = resolveEntityViewSubscription((TbTimeseriesSubscription) subscription); |
||||
|
} |
||||
|
pushSubscriptionToManagerService(subscription, true); |
||||
|
registerSubscription(subscription); |
||||
|
} |
||||
|
|
||||
|
private void pushSubscriptionToManagerService(TbSubscription subscription, boolean pushToLocalService) { |
||||
|
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId()); |
||||
|
if (currentPartitions.contains(tpi)) { |
||||
|
// Subscription is managed on the same server;
|
||||
|
if (pushToLocalService) { |
||||
|
subscriptionManagerService.addSubscription(subscription, TbMsgCallback.EMPTY); |
||||
|
} |
||||
|
} else { |
||||
|
// Push to the queue;
|
||||
|
TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toNewSubscriptionProto(subscription); |
||||
|
toCoreProducer.send(tpi, new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg), null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbMsgCallback callback) { |
||||
|
TbSubscription subscription = subscriptionsBySessionId |
||||
|
.getOrDefault(sessionId, Collections.emptyMap()).get(update.getSubscriptionId()); |
||||
|
if (subscription != null) { |
||||
|
switch (subscription.getType()) { |
||||
|
case TIMESERIES: |
||||
|
TbTimeseriesSubscription tsSub = (TbTimeseriesSubscription) subscription; |
||||
|
update.getLatestValues().forEach((key, value) -> tsSub.getKeyStates().put(key, value)); |
||||
|
break; |
||||
|
case ATTRIBUTES: |
||||
|
TbAttributeSubscription attrSub = (TbAttributeSubscription) subscription; |
||||
|
update.getLatestValues().forEach((key, value) -> attrSub.getKeyStates().put(key, value)); |
||||
|
break; |
||||
|
} |
||||
|
wsService.sendWsMsg(sessionId, update); |
||||
|
} |
||||
|
callback.onSuccess(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void cancelSubscription(String sessionId, int subscriptionId) { |
||||
|
log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); |
||||
|
Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsBySessionId.get(sessionId); |
||||
|
if (sessionSubscriptions != null) { |
||||
|
TbSubscription subscription = sessionSubscriptions.remove(subscriptionId); |
||||
|
if (subscription != null) { |
||||
|
if (sessionSubscriptions.isEmpty()) { |
||||
|
subscriptionsBySessionId.remove(sessionId); |
||||
|
} |
||||
|
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId()); |
||||
|
if (currentPartitions.contains(tpi)) { |
||||
|
// Subscription is managed on the same server;
|
||||
|
subscriptionManagerService.cancelSubscription(sessionId, subscriptionId, TbMsgCallback.EMPTY); |
||||
|
} else { |
||||
|
// Push to the queue;
|
||||
|
TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toCloseSubscriptionProto(subscription); |
||||
|
toCoreProducer.send(tpi, new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg), null); |
||||
|
} |
||||
|
} else { |
||||
|
log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); |
||||
|
} |
||||
|
} else { |
||||
|
log.debug("[{}] No session subscriptions found!", sessionId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void cancelAllSessionSubscriptions(String sessionId) { |
||||
|
Map<Integer, TbSubscription> subscriptions = subscriptionsBySessionId.get(sessionId); |
||||
|
if (subscriptions != null) { |
||||
|
Set<Integer> toRemove = new HashSet<>(subscriptions.keySet()); |
||||
|
toRemove.forEach(id -> cancelSubscription(sessionId, id)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private TbSubscription resolveEntityViewSubscription(TbTimeseriesSubscription subscription) { |
||||
|
EntityView entityView = entityViewService.findEntityViewById(TenantId.SYS_TENANT_ID, new EntityViewId(subscription.getEntityId().getId())); |
||||
|
|
||||
|
Map<String, Long> keyStates; |
||||
|
if (subscription.isAllKeys()) { |
||||
|
keyStates = entityView.getKeys().getTimeseries().stream().collect(Collectors.toMap(k -> k, k -> 0L)); |
||||
|
} else { |
||||
|
keyStates = subscription.getKeyStates().entrySet() |
||||
|
.stream().filter(entry -> entityView.getKeys().getTimeseries().contains(entry.getKey())) |
||||
|
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); |
||||
|
} |
||||
|
|
||||
|
return TbTimeseriesSubscription.builder() |
||||
|
.serviceId(subscription.getServiceId()) |
||||
|
.sessionId(subscription.getSessionId()) |
||||
|
.subscriptionId(subscription.getSubscriptionId()) |
||||
|
.tenantId(subscription.getTenantId()) |
||||
|
.entityId(entityView.getEntityId()) |
||||
|
.startTime(entityView.getStartTimeMs()) |
||||
|
.endTime(entityView.getEndTimeMs()) |
||||
|
.allKeys(false) |
||||
|
.keyStates(keyStates).build(); |
||||
|
} |
||||
|
|
||||
|
private void registerSubscription(TbSubscription subscription) { |
||||
|
Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsBySessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()); |
||||
|
sessionSubscriptions.put(subscription.getSubscriptionId(), subscription); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,377 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Service; |
||||
|
import org.springframework.util.StringUtils; |
||||
|
import org.thingsboard.common.util.DonAsynchron; |
||||
|
import org.thingsboard.common.util.ThingsBoardThreadFactory; |
||||
|
import org.thingsboard.rule.engine.api.msg.DeviceAttributesEventNotificationMsg; |
||||
|
import org.thingsboard.server.actors.service.ActorService; |
||||
|
import org.thingsboard.server.common.data.DataConstants; |
||||
|
import org.thingsboard.server.common.data.EntityType; |
||||
|
import org.thingsboard.server.common.data.id.DeviceId; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.kv.Aggregation; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.ReadTsKvQuery; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.common.msg.cluster.SendToClusterMsg; |
||||
|
import org.thingsboard.server.dao.attributes.AttributesService; |
||||
|
import org.thingsboard.server.dao.timeseries.TimeseriesService; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateValueListProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; |
||||
|
import org.thingsboard.server.queue.TbQueueProducer; |
||||
|
import org.thingsboard.server.queue.common.TbProtoQueueMsg; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionService; |
||||
|
import org.thingsboard.server.queue.discovery.ServiceType; |
||||
|
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; |
||||
|
import org.thingsboard.server.queue.discovery.TopicPartitionInfo; |
||||
|
import org.thingsboard.server.queue.provider.TbCoreQueueProvider; |
||||
|
import org.thingsboard.server.service.queue.TbMsgCallback; |
||||
|
import org.thingsboard.server.service.state.DefaultDeviceStateService; |
||||
|
import org.thingsboard.server.service.state.DeviceStateService; |
||||
|
import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import javax.annotation.PreDestroy; |
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashSet; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.Objects; |
||||
|
import java.util.Set; |
||||
|
import java.util.TreeMap; |
||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||
|
import java.util.concurrent.ConcurrentMap; |
||||
|
import java.util.concurrent.ExecutorService; |
||||
|
import java.util.concurrent.Executors; |
||||
|
import java.util.function.Function; |
||||
|
import java.util.function.Predicate; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Service |
||||
|
public class DefaultSubscriptionManagerService implements SubscriptionManagerService { |
||||
|
|
||||
|
@Autowired |
||||
|
private AttributesService attrService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TimeseriesService tsService; |
||||
|
|
||||
|
@Autowired |
||||
|
private PartitionService partitionService; |
||||
|
|
||||
|
@Autowired |
||||
|
private TbServiceInfoProvider serviceInfoProvider; |
||||
|
|
||||
|
@Autowired |
||||
|
private TbCoreQueueProvider coreQueueProvider; |
||||
|
|
||||
|
@Autowired |
||||
|
private LocalSubscriptionService localSubscriptionService; |
||||
|
|
||||
|
@Autowired |
||||
|
private DeviceStateService deviceStateService; |
||||
|
|
||||
|
@Autowired |
||||
|
private ActorService actorService; |
||||
|
|
||||
|
private final Map<EntityId, Set<TbSubscription>> subscriptionsByEntityId = new ConcurrentHashMap<>(); |
||||
|
private final Map<String, Map<Integer, TbSubscription>> subscriptionsByWsSessionId = new ConcurrentHashMap<>(); |
||||
|
private final ConcurrentMap<TopicPartitionInfo, Set<TbSubscription>> partitionedSubscriptions = new ConcurrentHashMap<>(); |
||||
|
private final Set<TopicPartitionInfo> currentPartitions = ConcurrentHashMap.newKeySet(); |
||||
|
|
||||
|
private ExecutorService tsCallBackExecutor; |
||||
|
private String serviceId; |
||||
|
private TbQueueProducer<TbProtoQueueMsg<ToCoreMsg>> toCoreProducer; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void initExecutor() { |
||||
|
tsCallBackExecutor = Executors.newSingleThreadExecutor(ThingsBoardThreadFactory.forName("ts-sub-callback")); |
||||
|
serviceId = serviceInfoProvider.getServiceId(); |
||||
|
toCoreProducer = coreQueueProvider.getTbCoreMsgProducer(); |
||||
|
} |
||||
|
|
||||
|
@PreDestroy |
||||
|
public void shutdownExecutor() { |
||||
|
if (tsCallBackExecutor != null) { |
||||
|
tsCallBackExecutor.shutdownNow(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void addSubscription(TbSubscription subscription, TbMsgCallback callback) { |
||||
|
log.trace("[{}][{}][{}] Registering remote subscription for entity [{}]", |
||||
|
subscription.getServiceId(), subscription.getSessionId(), subscription.getSubscriptionId(), subscription.getEntityId()); |
||||
|
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, subscription.getTenantId(), subscription.getEntityId()); |
||||
|
if (currentPartitions.contains(tpi)) { |
||||
|
partitionedSubscriptions.computeIfAbsent(tpi, k -> ConcurrentHashMap.newKeySet()).add(subscription); |
||||
|
callback.onSuccess(); |
||||
|
} else { |
||||
|
log.warn("[{}][{}] Entity belongs to external partition. Probably rebalancing is in progress. Topic: {}" |
||||
|
, subscription.getTenantId(), subscription.getEntityId(), tpi.getFullTopicName()); |
||||
|
callback.onFailure(new RuntimeException("Entity belongs to external partition " + tpi.getFullTopicName() + "!")); |
||||
|
} |
||||
|
boolean newSubscription = subscriptionsByEntityId |
||||
|
.computeIfAbsent(subscription.getEntityId(), k -> ConcurrentHashMap.newKeySet()).add(subscription); |
||||
|
subscriptionsByWsSessionId.computeIfAbsent(subscription.getSessionId(), k -> new ConcurrentHashMap<>()).put(subscription.getSubscriptionId(), subscription); |
||||
|
if (newSubscription) { |
||||
|
switch (subscription.getType()) { |
||||
|
case TIMESERIES: |
||||
|
handleNewTelemetrySubscription((TbTimeseriesSubscription) subscription); |
||||
|
break; |
||||
|
case ATTRIBUTES: |
||||
|
handleNewAttributeSubscription((TbAttributeSubscription) subscription); |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void cancelSubscription(String sessionId, int subscriptionId, TbMsgCallback callback) { |
||||
|
log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId); |
||||
|
Map<Integer, TbSubscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId); |
||||
|
if (sessionSubscriptions != null) { |
||||
|
TbSubscription subscription = sessionSubscriptions.remove(subscriptionId); |
||||
|
if (subscription != null) { |
||||
|
removeSubscriptionFromEntityMap(subscription); |
||||
|
removeSubscriptionFromPartitionMap(subscription); |
||||
|
if (sessionSubscriptions.isEmpty()) { |
||||
|
subscriptionsByWsSessionId.remove(sessionId); |
||||
|
} |
||||
|
} else { |
||||
|
log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId); |
||||
|
} |
||||
|
} else { |
||||
|
log.debug("[{}] No session subscriptions found!", sessionId); |
||||
|
} |
||||
|
callback.onSuccess(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onApplicationEvent(PartitionChangeEvent partitionChangeEvent) { |
||||
|
Set<TopicPartitionInfo> removedPartitions = new HashSet<>(currentPartitions); |
||||
|
removedPartitions.removeAll(partitionChangeEvent.getPartitions()); |
||||
|
|
||||
|
currentPartitions.clear(); |
||||
|
currentPartitions.addAll(partitionChangeEvent.getPartitions()); |
||||
|
|
||||
|
// We no longer manage current partition of devices;
|
||||
|
removedPartitions.forEach(partition -> { |
||||
|
Set<TbSubscription> subs = partitionedSubscriptions.remove(partition); |
||||
|
if (subs != null) { |
||||
|
subs.forEach(this::removeSubscriptionFromEntityMap); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onTimeseriesDataUpdate(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, TbMsgCallback callback) { |
||||
|
onLocalSubUpdate(entityId, |
||||
|
s -> { |
||||
|
if (TbSubscriptionType.TIMESERIES.equals(s.getType())) { |
||||
|
return (TbTimeseriesSubscription) s; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
}, s -> true, s -> { |
||||
|
List<TsKvEntry> subscriptionUpdate = null; |
||||
|
for (TsKvEntry kv : ts) { |
||||
|
if (isInTimeRange(s, kv.getTs()) && (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey())))) { |
||||
|
if (subscriptionUpdate == null) { |
||||
|
subscriptionUpdate = new ArrayList<>(); |
||||
|
} |
||||
|
subscriptionUpdate.add(kv); |
||||
|
} |
||||
|
} |
||||
|
return subscriptionUpdate; |
||||
|
}); |
||||
|
callback.onSuccess(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, TbMsgCallback callback) { |
||||
|
onLocalSubUpdate(entityId, |
||||
|
s -> { |
||||
|
if (TbSubscriptionType.ATTRIBUTES.equals(s.getType())) { |
||||
|
return (TbAttributeSubscription) s; |
||||
|
} else { |
||||
|
return null; |
||||
|
} |
||||
|
}, |
||||
|
s -> (StringUtils.isEmpty(s.getScope()) || scope.equals(s.getScope().name())), |
||||
|
s -> { |
||||
|
List<TsKvEntry> subscriptionUpdate = null; |
||||
|
for (AttributeKvEntry kv : attributes) { |
||||
|
if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) { |
||||
|
if (subscriptionUpdate == null) { |
||||
|
subscriptionUpdate = new ArrayList<>(); |
||||
|
} |
||||
|
subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv)); |
||||
|
} |
||||
|
} |
||||
|
return subscriptionUpdate; |
||||
|
}); |
||||
|
if (entityId.getEntityType() == EntityType.DEVICE) { |
||||
|
if (TbAttributeSubscriptionScope.SERVER_SCOPE.name().equalsIgnoreCase(scope)) { |
||||
|
for (AttributeKvEntry attribute : attributes) { |
||||
|
if (attribute.getKey().equals(DefaultDeviceStateService.INACTIVITY_TIMEOUT)) { |
||||
|
deviceStateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), attribute.getLongValue().orElse(0L)); |
||||
|
} |
||||
|
} |
||||
|
} else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope)) { |
||||
|
DeviceAttributesEventNotificationMsg notificationMsg = DeviceAttributesEventNotificationMsg.onUpdate(tenantId, |
||||
|
new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)); |
||||
|
actorService.onMsg(notificationMsg); |
||||
|
} |
||||
|
} |
||||
|
callback.onSuccess(); |
||||
|
} |
||||
|
|
||||
|
private <T extends TbSubscription> void onLocalSubUpdate(EntityId entityId, |
||||
|
Function<TbSubscription, T> castFunction, |
||||
|
Predicate<T> filterFunction, |
||||
|
Function<T, List<TsKvEntry>> processFunction) { |
||||
|
Set<TbSubscription> entitySubscriptions = subscriptionsByEntityId.get(entityId); |
||||
|
if (entitySubscriptions != null) { |
||||
|
entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> { |
||||
|
List<TsKvEntry> subscriptionUpdate = processFunction.apply(s); |
||||
|
if (subscriptionUpdate != null && !subscriptionUpdate.isEmpty()) { |
||||
|
if (serviceId.equals(s.getServiceId())) { |
||||
|
SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate); |
||||
|
localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbMsgCallback.EMPTY); |
||||
|
} else { |
||||
|
TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); |
||||
|
toCoreProducer.send(tpi, toProto(s, subscriptionUpdate), null); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} else { |
||||
|
log.debug("[{}] No device subscriptions to process!", entityId); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private boolean isInTimeRange(TbTimeseriesSubscription subscription, long kvTime) { |
||||
|
return (subscription.getStartTime() == 0 || subscription.getStartTime() <= kvTime) |
||||
|
&& (subscription.getEndTime() == 0 || subscription.getEndTime() >= kvTime); |
||||
|
} |
||||
|
|
||||
|
private void removeSubscriptionFromEntityMap(TbSubscription sub) { |
||||
|
Set<TbSubscription> entitySubSet = subscriptionsByEntityId.get(sub.getEntityId()); |
||||
|
if (entitySubSet != null) { |
||||
|
entitySubSet.remove(sub); |
||||
|
if (entitySubSet.isEmpty()) { |
||||
|
subscriptionsByEntityId.remove(sub.getEntityId()); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void removeSubscriptionFromPartitionMap(TbSubscription sub) { |
||||
|
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, sub.getTenantId(), sub.getEntityId()); |
||||
|
Set<TbSubscription> subs = partitionedSubscriptions.get(tpi); |
||||
|
if (subs != null) { |
||||
|
subs.remove(sub); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void handleNewAttributeSubscription(TbAttributeSubscription subscription) { |
||||
|
log.trace("[{}][{}][{}] Processing remote attribute subscription for entity [{}]", |
||||
|
serviceId, subscription.getSessionId(), subscription.getSubscriptionId(), subscription.getEntityId()); |
||||
|
|
||||
|
final Map<String, Long> keyStates = subscription.getKeyStates(); |
||||
|
DonAsynchron.withCallback(attrService.find(subscription.getTenantId(), subscription.getEntityId(), DataConstants.CLIENT_SCOPE, keyStates.keySet()), values -> { |
||||
|
List<TsKvEntry> missedUpdates = new ArrayList<>(); |
||||
|
values.forEach(latestEntry -> { |
||||
|
if (latestEntry.getLastUpdateTs() > keyStates.get(latestEntry.getKey())) { |
||||
|
missedUpdates.add(new BasicTsKvEntry(latestEntry.getLastUpdateTs(), latestEntry)); |
||||
|
} |
||||
|
}); |
||||
|
if (!missedUpdates.isEmpty()) { |
||||
|
TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); |
||||
|
toCoreProducer.send(tpi, toProto(subscription, missedUpdates), null); |
||||
|
} |
||||
|
}, |
||||
|
e -> log.error("Failed to fetch missed updates.", e), tsCallBackExecutor); |
||||
|
} |
||||
|
|
||||
|
private void handleNewTelemetrySubscription(TbTimeseriesSubscription subscription) { |
||||
|
log.trace("[{}][{}][{}] Processing remote telemetry subscription for entity [{}]", |
||||
|
serviceId, subscription.getSessionId(), subscription.getSubscriptionId(), subscription.getEntityId()); |
||||
|
|
||||
|
long curTs = System.currentTimeMillis(); |
||||
|
List<ReadTsKvQuery> queries = new ArrayList<>(); |
||||
|
subscription.getKeyStates().forEach((key, value) -> { |
||||
|
if (curTs > value) { |
||||
|
long startTs = subscription.getStartTime() > 0 ? Math.max(subscription.getStartTime(), value + 1L) : (value + 1L); |
||||
|
long endTs = subscription.getEndTime() > 0 ? Math.min(subscription.getEndTime(), curTs) : curTs; |
||||
|
queries.add(new BaseReadTsKvQuery(key, startTs, endTs, 0, 1000, Aggregation.NONE)); |
||||
|
} |
||||
|
}); |
||||
|
if (!queries.isEmpty()) { |
||||
|
DonAsynchron.withCallback(tsService.findAll(subscription.getTenantId(), subscription.getEntityId(), queries), |
||||
|
missedUpdates -> { |
||||
|
if (missedUpdates != null && !missedUpdates.isEmpty()) { |
||||
|
TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_CORE, subscription.getServiceId()); |
||||
|
toCoreProducer.send(tpi, toProto(subscription, missedUpdates), null); |
||||
|
} |
||||
|
}, |
||||
|
e -> log.error("Failed to fetch missed updates.", e), |
||||
|
tsCallBackExecutor); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private TbProtoQueueMsg<ToCoreMsg> toProto(TbSubscription subscription, List<TsKvEntry> updates) { |
||||
|
TbSubscriptionUpdateProto.Builder builder = TbSubscriptionUpdateProto.newBuilder(); |
||||
|
|
||||
|
builder.setSessionId(subscription.getSessionId()); |
||||
|
builder.setSubscriptionId(subscription.getSubscriptionId()); |
||||
|
|
||||
|
Map<String, List<Object>> data = new TreeMap<>(); |
||||
|
for (TsKvEntry tsEntry : updates) { |
||||
|
List<Object> values = data.computeIfAbsent(tsEntry.getKey(), k -> new ArrayList<>()); |
||||
|
Object[] value = new Object[2]; |
||||
|
value[0] = tsEntry.getTs(); |
||||
|
value[1] = tsEntry.getValueAsString(); |
||||
|
values.add(value); |
||||
|
} |
||||
|
|
||||
|
data.forEach((key, value) -> { |
||||
|
TbSubscriptionUpdateValueListProto.Builder dataBuilder = TbSubscriptionUpdateValueListProto.newBuilder(); |
||||
|
dataBuilder.setKey(key); |
||||
|
value.forEach(v -> { |
||||
|
Object[] array = (Object[]) v; |
||||
|
dataBuilder.addTs((long) array[0]); |
||||
|
dataBuilder.addValue((String) array[1]); |
||||
|
}); |
||||
|
builder.addData(dataBuilder.build()); |
||||
|
}); |
||||
|
|
||||
|
ToCoreMsg toCoreMsg = ToCoreMsg.newBuilder().setToLocalSubscriptionServiceMsg( |
||||
|
LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) |
||||
|
.build(); |
||||
|
return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
import org.thingsboard.server.queue.discovery.ClusterTopologyChangeEvent; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
||||
|
import org.thingsboard.server.service.queue.TbMsgCallback; |
||||
|
import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
||||
|
|
||||
|
public interface LocalSubscriptionService { |
||||
|
|
||||
|
void addSubscription(TbSubscription subscription); |
||||
|
|
||||
|
void cancelSubscription(String sessionId, int subscriptionId); |
||||
|
|
||||
|
void cancelAllSessionSubscriptions(String sessionId); |
||||
|
|
||||
|
void onSubscriptionUpdate(String sessionId, SubscriptionUpdate update, TbMsgCallback callback); |
||||
|
|
||||
|
void onApplicationEvent(PartitionChangeEvent event); |
||||
|
|
||||
|
void onApplicationEvent(ClusterTopologyChangeEvent event); |
||||
|
} |
||||
@ -0,0 +1,37 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
import org.springframework.context.ApplicationListener; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.queue.discovery.PartitionChangeEvent; |
||||
|
import org.thingsboard.server.service.queue.TbMsgCallback; |
||||
|
|
||||
|
import java.util.List; |
||||
|
|
||||
|
public interface SubscriptionManagerService extends ApplicationListener<PartitionChangeEvent> { |
||||
|
|
||||
|
void addSubscription(TbSubscription subscription, TbMsgCallback callback); |
||||
|
|
||||
|
void cancelSubscription(String sessionId, int subscriptionId, TbMsgCallback callback); |
||||
|
|
||||
|
void onTimeseriesDataUpdate(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, TbMsgCallback callback); |
||||
|
|
||||
|
void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, TbMsgCallback callback); |
||||
|
} |
||||
@ -0,0 +1,50 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
import lombok.Builder; |
||||
|
import lombok.Data; |
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class TbAttributeSubscription extends TbSubscription { |
||||
|
|
||||
|
@Getter private final boolean allKeys; |
||||
|
@Getter private final Map<String, Long> keyStates; |
||||
|
@Getter private final TbAttributeSubscriptionScope scope; |
||||
|
|
||||
|
@Builder |
||||
|
public TbAttributeSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, |
||||
|
boolean allKeys, Map<String, Long> keyStates, TbAttributeSubscriptionScope scope) { |
||||
|
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ATTRIBUTES); |
||||
|
this.allKeys = allKeys; |
||||
|
this.keyStates = keyStates; |
||||
|
this.scope = scope; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean equals(Object o) { |
||||
|
return super.equals(o); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int hashCode() { |
||||
|
return super.hashCode(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,22 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
public enum TbAttributeSubscriptionScope { |
||||
|
|
||||
|
CLIENT_SCOPE, SHARED_SCOPE, SERVER_SCOPE |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
import lombok.AllArgsConstructor; |
||||
|
import lombok.Data; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
import java.util.Objects; |
||||
|
|
||||
|
@Data |
||||
|
@AllArgsConstructor |
||||
|
public abstract class TbSubscription { |
||||
|
|
||||
|
private final String serviceId; |
||||
|
private final String sessionId; |
||||
|
private final int subscriptionId; |
||||
|
private final TenantId tenantId; |
||||
|
private final EntityId entityId; |
||||
|
private final TbSubscriptionType type; |
||||
|
|
||||
|
@Override |
||||
|
public boolean equals(Object o) { |
||||
|
if (this == o) return true; |
||||
|
if (o == null || getClass() != o.getClass()) return false; |
||||
|
TbSubscription that = (TbSubscription) o; |
||||
|
return subscriptionId == that.subscriptionId && |
||||
|
sessionId.equals(that.sessionId) && |
||||
|
tenantId.equals(that.tenantId) && |
||||
|
entityId.equals(that.entityId) && |
||||
|
type == that.type; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int hashCode() { |
||||
|
return Objects.hash(sessionId, subscriptionId, tenantId, entityId, type); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
public enum TbSubscriptionType { |
||||
|
TIMESERIES, ATTRIBUTES |
||||
|
} |
||||
@ -0,0 +1,247 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.EntityIdFactory; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BasicTsKvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.BooleanDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.DataType; |
||||
|
import org.thingsboard.server.common.data.kv.DoubleDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.JsonDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.KvEntry; |
||||
|
import org.thingsboard.server.common.data.kv.LongDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.StringDataEntry; |
||||
|
import org.thingsboard.server.common.data.kv.TsKvEntry; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.KeyValueProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.KeyValueType; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.SubscriptionMgrMsgProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeSubscriptionProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbAttributeUpdateProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionCloseProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionKetStateProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbSubscriptionUpdateProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesSubscriptionProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TbTimeSeriesUpdateProto; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg; |
||||
|
import org.thingsboard.server.gen.transport.TransportProtos.TsKvProto; |
||||
|
import org.thingsboard.server.service.telemetry.sub.SubscriptionErrorCode; |
||||
|
import org.thingsboard.server.service.telemetry.sub.SubscriptionUpdate; |
||||
|
|
||||
|
import java.util.ArrayList; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.List; |
||||
|
import java.util.Map; |
||||
|
import java.util.TreeMap; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
public class TbSubscriptionUtils { |
||||
|
|
||||
|
public static ToCoreMsg toNewSubscriptionProto(TbSubscription subscription) { |
||||
|
SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); |
||||
|
TbSubscriptionProto subscriptionProto = TbSubscriptionProto.newBuilder() |
||||
|
.setServiceId(subscription.getServiceId()) |
||||
|
.setSessionId(subscription.getSessionId()) |
||||
|
.setSubscriptionId(subscription.getSubscriptionId()) |
||||
|
.setTenantIdMSB(subscription.getTenantId().getId().getMostSignificantBits()) |
||||
|
.setTenantIdLSB(subscription.getTenantId().getId().getLeastSignificantBits()) |
||||
|
.setEntityType(subscription.getEntityId().getEntityType().name()) |
||||
|
.setEntityIdMSB(subscription.getEntityId().getId().getMostSignificantBits()) |
||||
|
.setEntityIdLSB(subscription.getEntityId().getId().getLeastSignificantBits()).build(); |
||||
|
|
||||
|
switch (subscription.getType()) { |
||||
|
case TIMESERIES: |
||||
|
TbTimeseriesSubscription tSub = (TbTimeseriesSubscription) subscription; |
||||
|
TbTimeSeriesSubscriptionProto.Builder tSubProto = TbTimeSeriesSubscriptionProto.newBuilder() |
||||
|
.setSub(subscriptionProto) |
||||
|
.setAllKeys(tSub.isAllKeys()); |
||||
|
tSub.getKeyStates().forEach((key, value) -> tSubProto.addKeyStates( |
||||
|
TbSubscriptionKetStateProto.newBuilder().setKey(key).setTs(value).build())); |
||||
|
tSubProto.setStartTime(tSub.getStartTime()); |
||||
|
tSubProto.setEndTime(tSub.getEndTime()); |
||||
|
msgBuilder.setTelemetrySub(tSubProto.build()); |
||||
|
break; |
||||
|
case ATTRIBUTES: |
||||
|
TbAttributeSubscription aSub = (TbAttributeSubscription) subscription; |
||||
|
TbAttributeSubscriptionProto.Builder aSubProto = TbAttributeSubscriptionProto.newBuilder() |
||||
|
.setSub(subscriptionProto) |
||||
|
.setAllKeys(aSub.isAllKeys()) |
||||
|
.setScope(aSub.getScope().name()); |
||||
|
aSub.getKeyStates().forEach((key, value) -> aSubProto.addKeyStates( |
||||
|
TbSubscriptionKetStateProto.newBuilder().setKey(key).setTs(value).build())); |
||||
|
msgBuilder.setAttributeSub(aSubProto.build()); |
||||
|
break; |
||||
|
} |
||||
|
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); |
||||
|
} |
||||
|
|
||||
|
public static ToCoreMsg toCloseSubscriptionProto(TbSubscription subscription) { |
||||
|
SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); |
||||
|
TbSubscriptionCloseProto closeProto = TbSubscriptionCloseProto.newBuilder() |
||||
|
.setSessionId(subscription.getSessionId()) |
||||
|
.setSubscriptionId(subscription.getSubscriptionId()).build(); |
||||
|
msgBuilder.setSubClose(closeProto); |
||||
|
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); |
||||
|
} |
||||
|
|
||||
|
public static TbSubscription fromProto(TbAttributeSubscriptionProto attributeSub) { |
||||
|
TbSubscriptionProto subProto = attributeSub.getSub(); |
||||
|
TbAttributeSubscription.TbAttributeSubscriptionBuilder builder = TbAttributeSubscription.builder() |
||||
|
.serviceId(subProto.getServiceId()) |
||||
|
.sessionId(subProto.getSessionId()) |
||||
|
.subscriptionId(subProto.getSubscriptionId()) |
||||
|
.entityId(EntityIdFactory.getByTypeAndUuid(subProto.getEntityType(), new UUID(subProto.getEntityIdMSB(), subProto.getEntityIdLSB()))) |
||||
|
.tenantId(new TenantId(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); |
||||
|
|
||||
|
builder.scope(TbAttributeSubscriptionScope.valueOf(attributeSub.getScope())); |
||||
|
builder.allKeys(attributeSub.getAllKeys()); |
||||
|
Map<String, Long> keyStates = new HashMap<>(); |
||||
|
attributeSub.getKeyStatesList().forEach(ksProto -> keyStates.put(ksProto.getKey(), ksProto.getTs())); |
||||
|
builder.keyStates(keyStates); |
||||
|
return builder.build(); |
||||
|
} |
||||
|
|
||||
|
public static TbSubscription fromProto(TbTimeSeriesSubscriptionProto telemetrySub) { |
||||
|
TbSubscriptionProto subProto = telemetrySub.getSub(); |
||||
|
TbTimeseriesSubscription.TbTimeseriesSubscriptionBuilder builder = TbTimeseriesSubscription.builder() |
||||
|
.serviceId(subProto.getServiceId()) |
||||
|
.sessionId(subProto.getSessionId()) |
||||
|
.subscriptionId(subProto.getSubscriptionId()) |
||||
|
.entityId(EntityIdFactory.getByTypeAndUuid(subProto.getEntityType(), new UUID(subProto.getEntityIdMSB(), subProto.getEntityIdLSB()))) |
||||
|
.tenantId(new TenantId(new UUID(subProto.getTenantIdMSB(), subProto.getTenantIdLSB()))); |
||||
|
|
||||
|
builder.allKeys(telemetrySub.getAllKeys()); |
||||
|
Map<String, Long> keyStates = new HashMap<>(); |
||||
|
telemetrySub.getKeyStatesList().forEach(ksProto -> keyStates.put(ksProto.getKey(), ksProto.getTs())); |
||||
|
builder.startTime(telemetrySub.getStartTime()); |
||||
|
builder.endTime(telemetrySub.getEndTime()); |
||||
|
builder.keyStates(keyStates); |
||||
|
return builder.build(); |
||||
|
} |
||||
|
|
||||
|
public static SubscriptionUpdate fromProto(TbSubscriptionUpdateProto proto) { |
||||
|
if (proto.getErrorCode() > 0) { |
||||
|
return new SubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); |
||||
|
} else { |
||||
|
Map<String, List<Object>> data = new TreeMap<>(); |
||||
|
proto.getDataList().forEach(v -> { |
||||
|
List<Object> values = data.computeIfAbsent(v.getKey(), k -> new ArrayList<>()); |
||||
|
for (int i = 0; i < v.getTsCount(); i++) { |
||||
|
Object[] value = new Object[2]; |
||||
|
value[0] = v.getTs(i); |
||||
|
value[1] = v.getValue(i); |
||||
|
values.add(value); |
||||
|
} |
||||
|
}); |
||||
|
return new SubscriptionUpdate(proto.getSubscriptionId(), data); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static ToCoreMsg toTimeseriesUpdateProto(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts) { |
||||
|
TbTimeSeriesUpdateProto.Builder builder = TbTimeSeriesUpdateProto.newBuilder(); |
||||
|
builder.setEntityType(entityId.getEntityType().name()); |
||||
|
builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); |
||||
|
builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); |
||||
|
builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); |
||||
|
builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); |
||||
|
ts.forEach(v -> builder.addData(toKeyValueProto(v.getTs(), v).build())); |
||||
|
SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); |
||||
|
msgBuilder.setTsUpdate(builder); |
||||
|
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); |
||||
|
} |
||||
|
|
||||
|
public static ToCoreMsg toAttributesUpdateProto(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes) { |
||||
|
TbAttributeUpdateProto.Builder builder = TbAttributeUpdateProto.newBuilder(); |
||||
|
builder.setEntityType(entityId.getEntityType().name()); |
||||
|
builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); |
||||
|
builder.setEntityIdLSB(entityId.getId().getLeastSignificantBits()); |
||||
|
builder.setTenantIdMSB(tenantId.getId().getMostSignificantBits()); |
||||
|
builder.setTenantIdLSB(tenantId.getId().getLeastSignificantBits()); |
||||
|
builder.setScope(scope); |
||||
|
attributes.forEach(v -> builder.addData(toKeyValueProto(v.getLastUpdateTs(), v).build())); |
||||
|
|
||||
|
SubscriptionMgrMsgProto.Builder msgBuilder = SubscriptionMgrMsgProto.newBuilder(); |
||||
|
msgBuilder.setAttrUpdate(builder); |
||||
|
return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); |
||||
|
} |
||||
|
|
||||
|
private static TsKvProto.Builder toKeyValueProto(long ts, KvEntry attr) { |
||||
|
KeyValueProto.Builder dataBuilder = KeyValueProto.newBuilder(); |
||||
|
dataBuilder.setKey(attr.getKey()); |
||||
|
dataBuilder.setType(KeyValueType.forNumber(attr.getDataType().ordinal())); |
||||
|
switch (attr.getDataType()) { |
||||
|
case BOOLEAN: |
||||
|
attr.getBooleanValue().ifPresent(dataBuilder::setBoolV); |
||||
|
break; |
||||
|
case LONG: |
||||
|
attr.getLongValue().ifPresent(dataBuilder::setLongV); |
||||
|
break; |
||||
|
case DOUBLE: |
||||
|
attr.getDoubleValue().ifPresent(dataBuilder::setDoubleV); |
||||
|
break; |
||||
|
case JSON: |
||||
|
attr.getJsonValue().ifPresent(dataBuilder::setJsonV); |
||||
|
break; |
||||
|
case STRING: |
||||
|
attr.getStrValue().ifPresent(dataBuilder::setStringV); |
||||
|
break; |
||||
|
} |
||||
|
return TsKvProto.newBuilder().setTs(ts).setKv(dataBuilder); |
||||
|
} |
||||
|
|
||||
|
public static EntityId toEntityId(String entityType, long entityIdMSB, long entityIdLSB) { |
||||
|
return EntityIdFactory.getByTypeAndUuid(entityType, new UUID(entityIdMSB, entityIdLSB)); |
||||
|
} |
||||
|
|
||||
|
public static List<TsKvEntry> toTsKvEntityList(List<TsKvProto> dataList) { |
||||
|
List<TsKvEntry> result = new ArrayList<>(dataList.size()); |
||||
|
dataList.forEach(proto -> result.add(new BasicTsKvEntry(proto.getTs(), getKvEntry(proto.getKv())))); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
public static List<AttributeKvEntry> toAttributeKvList(List<TsKvProto> dataList) { |
||||
|
List<AttributeKvEntry> result = new ArrayList<>(dataList.size()); |
||||
|
dataList.forEach(proto -> result.add(new BaseAttributeKvEntry(getKvEntry(proto.getKv()), proto.getTs()))); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
private static KvEntry getKvEntry(KeyValueProto proto) { |
||||
|
KvEntry entry = null; |
||||
|
DataType type = DataType.values()[proto.getType().getNumber()]; |
||||
|
switch (type) { |
||||
|
case BOOLEAN: |
||||
|
entry = new BooleanDataEntry(proto.getKey(), proto.getBoolV()); |
||||
|
break; |
||||
|
case LONG: |
||||
|
entry = new LongDataEntry(proto.getKey(), proto.getLongV()); |
||||
|
break; |
||||
|
case DOUBLE: |
||||
|
entry = new DoubleDataEntry(proto.getKey(), proto.getDoubleV()); |
||||
|
break; |
||||
|
case STRING: |
||||
|
entry = new StringDataEntry(proto.getKey(), proto.getStringV()); |
||||
|
break; |
||||
|
case JSON: |
||||
|
entry = new JsonDataEntry(proto.getKey(), proto.getJsonV()); |
||||
|
break; |
||||
|
} |
||||
|
return entry; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,51 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.service.subscription; |
||||
|
|
||||
|
import lombok.Builder; |
||||
|
import lombok.Getter; |
||||
|
import org.thingsboard.server.common.data.id.EntityId; |
||||
|
import org.thingsboard.server.common.data.id.TenantId; |
||||
|
|
||||
|
import java.util.Map; |
||||
|
|
||||
|
public class TbTimeseriesSubscription extends TbSubscription { |
||||
|
|
||||
|
@Getter private final boolean allKeys; |
||||
|
@Getter private final Map<String, Long> keyStates; |
||||
|
@Getter private final long startTime; |
||||
|
@Getter private final long endTime; |
||||
|
|
||||
|
@Builder |
||||
|
public TbTimeseriesSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId, |
||||
|
boolean allKeys, Map<String, Long> keyStates, long startTime, long endTime) { |
||||
|
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.TIMESERIES); |
||||
|
this.allKeys = allKeys; |
||||
|
this.keyStates = keyStates; |
||||
|
this.startTime = startTime; |
||||
|
this.endTime = endTime; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public boolean equals(Object o) { |
||||
|
return super.equals(o); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public int hashCode() { |
||||
|
return super.hashCode(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
/** |
||||
|
* Copyright © 2016-2020 The Thingsboard Authors |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
*/ |
||||
|
package org.thingsboard.server.queue.discovery; |
||||
|
|
||||
|
import lombok.Getter; |
||||
|
import org.springframework.context.ApplicationEvent; |
||||
|
|
||||
|
import java.util.Set; |
||||
|
|
||||
|
|
||||
|
public class ClusterTopologyChangeEvent extends ApplicationEvent { |
||||
|
|
||||
|
@Getter |
||||
|
private final Set<ServiceKey> serviceKeys; |
||||
|
|
||||
|
public ClusterTopologyChangeEvent(Object source, Set<ServiceKey> serviceKeys) { |
||||
|
super(source); |
||||
|
this.serviceKeys = serviceKeys; |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue