Refactoring to implement command unsubscribe for alarm status ws cmd
This commit is contained in:
parent
c498b26485
commit
64581b4fef
@ -444,75 +444,23 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleCmd(WebSocketSessionRef sessionRef, AlarmStatusCmd cmd) {
|
public void handleCmd(WebSocketSessionRef session, AlarmStatusCmd cmd) {
|
||||||
log.debug("[{}] Handling alarm status subscription cmd (cmdId: {})", sessionRef.getSessionId(), cmd.getCmdId());
|
log.debug("[{}] Handling alarm status subscription cmd (cmdId: {})", session.getSessionId(), cmd.getCmdId());
|
||||||
SecurityUser securityCtx = sessionRef.getSecurityCtx();
|
TbAlarmStatusSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
|
||||||
|
if (ctx == null) {
|
||||||
TbAlarmStatusSubscription subscription = TbAlarmStatusSubscription.builder()
|
ctx = createSubCtx(session, cmd);
|
||||||
.serviceId(serviceInfoProvider.getServiceId())
|
long start = System.currentTimeMillis();
|
||||||
.sessionId(sessionRef.getSessionId())
|
ctx.fetchActiveAlarms();
|
||||||
.subscriptionId(cmd.getCmdId())
|
long end = System.currentTimeMillis();
|
||||||
.tenantId(securityCtx.getTenantId())
|
stats.getAlarmQueryInvocationCnt().incrementAndGet();
|
||||||
.entityId(cmd.getOriginatorId())
|
stats.getAlarmQueryTimeSpent().addAndGet(end - start);
|
||||||
.typeList(cmd.getTypeList())
|
ctx.sendUpdate();
|
||||||
.severityList(cmd.getSeverityList())
|
|
||||||
.updateProcessor(this::handleAlarmStatusSubscriptionUpdate)
|
|
||||||
.build();
|
|
||||||
localSubscriptionService.addSubscription(subscription, sessionRef);
|
|
||||||
|
|
||||||
fetchActiveAlarms(subscription);
|
|
||||||
sendUpdate(sessionRef.getSessionId(), subscription.createUpdate());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchActiveAlarms(TbAlarmStatusSubscription subscription) {
|
|
||||||
log.trace("[{}, subId: {}] Fetching active alarms from DB", subscription.getSessionId(), subscription.getSubscriptionId());
|
|
||||||
OriginatorAlarmFilter originatorAlarmFilter = new OriginatorAlarmFilter(subscription.getEntityId(), subscription.getTypeList(), subscription.getSeverityList());
|
|
||||||
List<UUID> alarmIds = alarmService.findActiveOriginatorAlarms(subscription.getTenantId(), originatorAlarmFilter, alarmsPerAlarmStatusSubscriptionCacheSize);
|
|
||||||
|
|
||||||
subscription.getAlarmIds().addAll(alarmIds);
|
|
||||||
subscription.setCacheFull(alarmIds.size() == alarmsPerAlarmStatusSubscriptionCacheSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendUpdate(String sessionId, CmdUpdate update) {
|
|
||||||
log.trace("[{}, cmdId: {}] Sending WS update: {}", sessionId, update.getCmdId(), update);
|
|
||||||
wsService.sendUpdate(sessionId, update);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleAlarmStatusSubscriptionUpdate(TbSubscription<AlarmSubscriptionUpdate> sub, AlarmSubscriptionUpdate subscriptionUpdate) {
|
|
||||||
TbAlarmStatusSubscription subscription = (TbAlarmStatusSubscription) sub;
|
|
||||||
try {
|
|
||||||
AlarmInfo alarm = subscriptionUpdate.getAlarm();
|
|
||||||
Set<UUID> alarmsIds = subscription.getAlarmIds();
|
|
||||||
if (alarmsIds.contains(alarm.getId().getId())) {
|
|
||||||
if (!subscription.matches(alarm) || subscriptionUpdate.isAlarmDeleted()) {
|
|
||||||
alarmsIds.remove(alarm.getId().getId());
|
|
||||||
if (alarmsIds.isEmpty()) {
|
|
||||||
if (subscription.isCacheFull()) {
|
|
||||||
fetchActiveAlarms(subscription);
|
|
||||||
if (alarmsIds.isEmpty()) {
|
|
||||||
sendUpdate(subscription.getSessionId(), subscription.createUpdate());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
sendUpdate(subscription.getSessionId(), subscription.createUpdate());
|
log.debug("[{}][{}] Received duplicate command: {}", session.getSessionId(), cmd.getCmdId(), cmd);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (subscription.matches(alarm)) {
|
|
||||||
if (alarmsIds.size() < alarmsPerAlarmStatusSubscriptionCacheSize) {
|
|
||||||
alarmsIds.add(alarm.getId().getId());
|
|
||||||
if (alarmsIds.size() == 1) {
|
|
||||||
sendUpdate(subscription.getSessionId(), subscription.createUpdate());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
subscription.setCacheFull(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("[{}, subId: {}] Failed to handle update for alarm status subscription: {}", subscription.getSessionId(), subscription.getSubscriptionId(), subscriptionUpdate, e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean validate(TbAbstractSubCtx<?> finalCtx) {
|
private boolean validate(TbAbstractSubCtx finalCtx) {
|
||||||
if (finalCtx.isStopped()) {
|
if (finalCtx.isStopped()) {
|
||||||
log.warn("[{}][{}][{}] Received validation task for already stopped context.", finalCtx.getTenantId(), finalCtx.getSessionId(), finalCtx.getCmdId());
|
log.warn("[{}][{}][{}] Received validation task for already stopped context.", finalCtx.getTenantId(), finalCtx.getSessionId(), finalCtx.getCmdId());
|
||||||
return false;
|
return false;
|
||||||
@ -528,7 +476,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshDynamicQuery(TbAbstractSubCtx<?> finalCtx) {
|
private void refreshDynamicQuery(TbAbstractEntityQuerySubCtx<?> finalCtx) {
|
||||||
try {
|
try {
|
||||||
if (validate(finalCtx)) {
|
if (validate(finalCtx)) {
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
@ -616,6 +564,15 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TbAlarmStatusSubCtx createSubCtx(WebSocketSessionRef sessionRef, AlarmStatusCmd cmd) {
|
||||||
|
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.computeIfAbsent(sessionRef.getSessionId(), k -> new ConcurrentHashMap<>());
|
||||||
|
TbAlarmStatusSubCtx ctx = new TbAlarmStatusSubCtx(serviceId, wsService, localSubscriptionService,
|
||||||
|
stats, alarmService, alarmsPerAlarmStatusSubscriptionCacheSize, sessionRef, cmd.getCmdId());
|
||||||
|
ctx.createSubscription(cmd);
|
||||||
|
sessionSubs.put(cmd.getCmdId(), ctx);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private <T extends TbAbstractSubCtx> T getSubCtx(String sessionId, int cmdId) {
|
private <T extends TbAbstractSubCtx> T getSubCtx(String sessionId, int cmdId) {
|
||||||
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.get(sessionId);
|
Map<Integer, TbAbstractSubCtx> sessionSubs = subscriptionsBySessionId.get(sessionId);
|
||||||
|
|||||||
@ -414,7 +414,7 @@ public class DefaultTbLocalSubscriptionService implements TbLocalSubscriptionSer
|
|||||||
|
|
||||||
private void onAlarmUpdate(UUID entityId, AlarmSubscriptionUpdate update, TbCallback callback) {
|
private void onAlarmUpdate(UUID entityId, AlarmSubscriptionUpdate update, TbCallback callback) {
|
||||||
processSubscriptionData(entityId,
|
processSubscriptionData(entityId,
|
||||||
sub -> TbSubscriptionType.ALARMS.equals(sub.getType()) || TbSubscriptionType.ALARM_STATUS.equals(sub.getType()),
|
sub -> TbSubscriptionType.ALARMS.equals(sub.getType()),
|
||||||
update, callback);
|
update, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,7 +44,7 @@ import java.util.function.Function;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends EntityDataPageLink>> extends TbAbstractSubCtx<T> {
|
public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends EntityDataPageLink>> extends TbAbstractEntityQuerySubCtx<T> {
|
||||||
|
|
||||||
protected final Map<Integer, EntityId> subToEntityIdMap;
|
protected final Map<Integer, EntityId> subToEntityIdMap;
|
||||||
@Getter
|
@Getter
|
||||||
|
|||||||
@ -0,0 +1,295 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2024 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 com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.thingsboard.server.common.data.AttributeScope;
|
||||||
|
import org.thingsboard.server.common.data.id.CustomerId;
|
||||||
|
import org.thingsboard.server.common.data.id.EntityId;
|
||||||
|
import org.thingsboard.server.common.data.id.TenantId;
|
||||||
|
import org.thingsboard.server.common.data.id.UserId;
|
||||||
|
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
|
||||||
|
import org.thingsboard.server.common.data.query.ComplexFilterPredicate;
|
||||||
|
import org.thingsboard.server.common.data.query.DynamicValue;
|
||||||
|
import org.thingsboard.server.common.data.query.DynamicValueSourceType;
|
||||||
|
import org.thingsboard.server.common.data.query.EntityCountQuery;
|
||||||
|
import org.thingsboard.server.common.data.query.FilterPredicateType;
|
||||||
|
import org.thingsboard.server.common.data.query.KeyFilter;
|
||||||
|
import org.thingsboard.server.common.data.query.KeyFilterPredicate;
|
||||||
|
import org.thingsboard.server.common.data.query.SimpleKeyFilterPredicate;
|
||||||
|
import org.thingsboard.server.common.data.query.TsValue;
|
||||||
|
import org.thingsboard.server.dao.attributes.AttributesService;
|
||||||
|
import org.thingsboard.server.dao.entity.EntityService;
|
||||||
|
import org.thingsboard.server.service.ws.WebSocketService;
|
||||||
|
import org.thingsboard.server.service.ws.WebSocketSessionRef;
|
||||||
|
import org.thingsboard.server.service.ws.telemetry.sub.TelemetrySubscriptionUpdate;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ScheduledFuture;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public abstract class TbAbstractEntityQuerySubCtx<T extends EntityCountQuery> extends TbAbstractSubCtx {
|
||||||
|
|
||||||
|
protected final EntityService entityService;
|
||||||
|
protected final AttributesService attributesService;
|
||||||
|
protected final Set<Integer> subToDynamicValueKeySet;
|
||||||
|
@Getter
|
||||||
|
protected final Map<DynamicValueKey, List<DynamicValue>> dynamicValues;
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
protected T query;
|
||||||
|
@Setter
|
||||||
|
protected volatile ScheduledFuture<?> refreshTask;
|
||||||
|
|
||||||
|
public TbAbstractEntityQuerySubCtx(String serviceId, WebSocketService wsService, EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
|
||||||
|
AttributesService attributesService, SubscriptionServiceStatistics stats, WebSocketSessionRef sessionRef, int cmdId) {
|
||||||
|
super(serviceId, wsService, localSubscriptionService, stats, sessionRef, cmdId);
|
||||||
|
this.entityService = entityService;
|
||||||
|
this.attributesService = attributesService;
|
||||||
|
this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet();
|
||||||
|
this.dynamicValues = new ConcurrentHashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void fetchData();
|
||||||
|
|
||||||
|
protected abstract void update();
|
||||||
|
|
||||||
|
public void clearSubscriptions() {
|
||||||
|
clearDynamicValueSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop() {
|
||||||
|
super.stop();
|
||||||
|
cancelTasks();
|
||||||
|
clearSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAndResolveQuery(T query) {
|
||||||
|
dynamicValues.clear();
|
||||||
|
this.query = query;
|
||||||
|
if (query != null && query.getKeyFilters() != null) {
|
||||||
|
for (KeyFilter filter : query.getKeyFilters()) {
|
||||||
|
registerDynamicValues(filter.getPredicate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resolve(getTenantId(), getCustomerId(), getUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) {
|
||||||
|
List<ListenableFuture<DynamicValueKeySub>> futures = new ArrayList<>();
|
||||||
|
for (DynamicValueKey key : dynamicValues.keySet()) {
|
||||||
|
switch (key.getSourceType()) {
|
||||||
|
case CURRENT_TENANT:
|
||||||
|
futures.add(resolveEntityValue(tenantId, tenantId, key));
|
||||||
|
break;
|
||||||
|
case CURRENT_CUSTOMER:
|
||||||
|
if (customerId != null && !customerId.isNullUid()) {
|
||||||
|
futures.add(resolveEntityValue(tenantId, customerId, key));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case CURRENT_USER:
|
||||||
|
if (userId != null && !userId.isNullUid()) {
|
||||||
|
futures.add(resolveEntityValue(tenantId, userId, key));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Map<EntityId, Map<String, DynamicValueKeySub>> tmpSubMap = new HashMap<>();
|
||||||
|
for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) {
|
||||||
|
tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub);
|
||||||
|
}
|
||||||
|
for (EntityId entityId : tmpSubMap.keySet()) {
|
||||||
|
Map<String, Long> keyStates = new HashMap<>();
|
||||||
|
Map<String, DynamicValueKeySub> dynamicValueKeySubMap = tmpSubMap.get(entityId);
|
||||||
|
dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs()));
|
||||||
|
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
|
||||||
|
TbAttributeSubscription sub = TbAttributeSubscription.builder()
|
||||||
|
.serviceId(serviceId)
|
||||||
|
.sessionId(sessionRef.getSessionId())
|
||||||
|
.subscriptionId(subIdx)
|
||||||
|
.tenantId(sessionRef.getSecurityCtx().getTenantId())
|
||||||
|
.entityId(entityId)
|
||||||
|
.updateProcessor((subscription, subscriptionUpdate) -> dynamicValueSubUpdate(subscription.getSessionId(), subscriptionUpdate, dynamicValueKeySubMap))
|
||||||
|
.queryTs(createdTime)
|
||||||
|
.allKeys(false)
|
||||||
|
.keyStates(keyStates)
|
||||||
|
.scope(TbAttributeSubscriptionScope.SERVER_SCOPE)
|
||||||
|
.build();
|
||||||
|
subToDynamicValueKeySet.add(subIdx);
|
||||||
|
localSubscriptionService.addSubscription(sub, sessionRef);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate,
|
||||||
|
Map<String, DynamicValueKeySub> dynamicValueKeySubMap) {
|
||||||
|
Map<String, TsValue> latestUpdate = new HashMap<>();
|
||||||
|
subscriptionUpdate.getData().forEach((k, v) -> {
|
||||||
|
Object[] data = (Object[]) v.get(0);
|
||||||
|
latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));
|
||||||
|
});
|
||||||
|
|
||||||
|
boolean invalidateFilter = false;
|
||||||
|
for (Map.Entry<String, TsValue> entry : latestUpdate.entrySet()) {
|
||||||
|
String k = entry.getKey();
|
||||||
|
TsValue tsValue = entry.getValue();
|
||||||
|
DynamicValueKeySub sub = dynamicValueKeySubMap.get(k);
|
||||||
|
if (sub.updateValue(tsValue)) {
|
||||||
|
invalidateFilter = true;
|
||||||
|
updateDynamicValuesByKey(sub, tsValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invalidateFilter) {
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
private static class DynamicValueKeySub {
|
||||||
|
private final DynamicValueKey key;
|
||||||
|
private final EntityId entityId;
|
||||||
|
private long lastUpdateTs;
|
||||||
|
private String lastUpdateValue;
|
||||||
|
|
||||||
|
boolean updateValue(TsValue value) {
|
||||||
|
if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) {
|
||||||
|
this.lastUpdateTs = value.getTs();
|
||||||
|
this.lastUpdateValue = value.getValue();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<DynamicValueKeySub> resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) {
|
||||||
|
ListenableFuture<Optional<AttributeKvEntry>> entry = attributesService.find(tenantId, entityId,
|
||||||
|
AttributeScope.SERVER_SCOPE, key.getSourceAttribute());
|
||||||
|
return Futures.transform(entry, attributeOpt -> {
|
||||||
|
DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId);
|
||||||
|
if (attributeOpt.isPresent()) {
|
||||||
|
AttributeKvEntry attribute = attributeOpt.get();
|
||||||
|
sub.setLastUpdateTs(attribute.getLastUpdateTs());
|
||||||
|
sub.setLastUpdateValue(attribute.getValueAsString());
|
||||||
|
updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString()));
|
||||||
|
}
|
||||||
|
return sub;
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
protected void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) {
|
||||||
|
DynamicValueKey dvk = sub.getKey();
|
||||||
|
switch (dvk.getPredicateType()) {
|
||||||
|
case STRING:
|
||||||
|
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue()));
|
||||||
|
break;
|
||||||
|
case NUMERIC:
|
||||||
|
try {
|
||||||
|
Double dValue = Double.parseDouble(tsValue.getValue());
|
||||||
|
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BOOLEAN:
|
||||||
|
Boolean bValue = Boolean.parseBoolean(tsValue.getValue());
|
||||||
|
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private void registerDynamicValues(KeyFilterPredicate predicate) {
|
||||||
|
switch (predicate.getType()) {
|
||||||
|
case STRING:
|
||||||
|
case NUMERIC:
|
||||||
|
case BOOLEAN:
|
||||||
|
Optional<DynamicValue> value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate);
|
||||||
|
if (value.isPresent()) {
|
||||||
|
DynamicValue dynamicValue = value.get();
|
||||||
|
DynamicValueKey key = new DynamicValueKey(
|
||||||
|
predicate.getType(),
|
||||||
|
dynamicValue.getSourceType(),
|
||||||
|
dynamicValue.getSourceAttribute());
|
||||||
|
dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case COMPLEX:
|
||||||
|
((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<DynamicValue<T>> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate<T> predicate) {
|
||||||
|
if (predicate.getValue().getUserValue() == null) {
|
||||||
|
return Optional.ofNullable(predicate.getValue().getDynamicValue());
|
||||||
|
} else {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void clearDynamicValueSubscriptions() {
|
||||||
|
if (subToDynamicValueKeySet != null) {
|
||||||
|
for (Integer subId : subToDynamicValueKeySet) {
|
||||||
|
localSubscriptionService.cancelSubscription(getTenantId(), sessionRef.getSessionId(), subId);
|
||||||
|
}
|
||||||
|
subToDynamicValueKeySet.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRefreshTask(ScheduledFuture<?> task) {
|
||||||
|
if (!stopped) {
|
||||||
|
this.refreshTask = task;
|
||||||
|
} else {
|
||||||
|
task.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancelTasks() {
|
||||||
|
if (this.refreshTask != null) {
|
||||||
|
log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId);
|
||||||
|
this.refreshTask.cancel(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class DynamicValueKey {
|
||||||
|
@Getter
|
||||||
|
private final FilterPredicateType predicateType;
|
||||||
|
@Getter
|
||||||
|
private final DynamicValueSourceType sourceType;
|
||||||
|
@Getter
|
||||||
|
private final String sourceAttribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -58,230 +58,37 @@ import java.util.concurrent.locks.ReentrantLock;
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Data
|
@Data
|
||||||
public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
|
public abstract class TbAbstractSubCtx {
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
protected final Lock wsLock = new ReentrantLock(true);
|
protected final Lock wsLock = new ReentrantLock(true);
|
||||||
protected final String serviceId;
|
protected final String serviceId;
|
||||||
protected final SubscriptionServiceStatistics stats;
|
protected final SubscriptionServiceStatistics stats;
|
||||||
private final WebSocketService wsService;
|
private final WebSocketService wsService;
|
||||||
protected final EntityService entityService;
|
|
||||||
protected final TbLocalSubscriptionService localSubscriptionService;
|
protected final TbLocalSubscriptionService localSubscriptionService;
|
||||||
protected final AttributesService attributesService;
|
|
||||||
protected final WebSocketSessionRef sessionRef;
|
protected final WebSocketSessionRef sessionRef;
|
||||||
protected final int cmdId;
|
protected final int cmdId;
|
||||||
protected final Set<Integer> subToDynamicValueKeySet;
|
|
||||||
@Getter
|
|
||||||
protected final Map<DynamicValueKey, List<DynamicValue>> dynamicValues;
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
protected T query;
|
|
||||||
@Setter
|
|
||||||
protected volatile ScheduledFuture<?> refreshTask;
|
|
||||||
protected volatile boolean stopped;
|
protected volatile boolean stopped;
|
||||||
@Getter
|
@Getter
|
||||||
protected long createdTime;
|
protected long createdTime;
|
||||||
|
|
||||||
public TbAbstractSubCtx(String serviceId, WebSocketService wsService,
|
public TbAbstractSubCtx(String serviceId, WebSocketService wsService,
|
||||||
EntityService entityService, TbLocalSubscriptionService localSubscriptionService,
|
TbLocalSubscriptionService localSubscriptionService,
|
||||||
AttributesService attributesService, SubscriptionServiceStatistics stats,
|
SubscriptionServiceStatistics stats,
|
||||||
WebSocketSessionRef sessionRef, int cmdId) {
|
WebSocketSessionRef sessionRef, int cmdId) {
|
||||||
this.createdTime = System.currentTimeMillis();
|
this.createdTime = System.currentTimeMillis();
|
||||||
this.serviceId = serviceId;
|
this.serviceId = serviceId;
|
||||||
this.wsService = wsService;
|
this.wsService = wsService;
|
||||||
this.entityService = entityService;
|
|
||||||
this.localSubscriptionService = localSubscriptionService;
|
this.localSubscriptionService = localSubscriptionService;
|
||||||
this.attributesService = attributesService;
|
|
||||||
this.stats = stats;
|
this.stats = stats;
|
||||||
this.sessionRef = sessionRef;
|
this.sessionRef = sessionRef;
|
||||||
this.cmdId = cmdId;
|
this.cmdId = cmdId;
|
||||||
this.subToDynamicValueKeySet = ConcurrentHashMap.newKeySet();
|
|
||||||
this.dynamicValues = new ConcurrentHashMap<>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAndResolveQuery(T query) {
|
|
||||||
dynamicValues.clear();
|
|
||||||
this.query = query;
|
|
||||||
if (query != null && query.getKeyFilters() != null) {
|
|
||||||
for (KeyFilter filter : query.getKeyFilters()) {
|
|
||||||
registerDynamicValues(filter.getPredicate());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolve(getTenantId(), getCustomerId(), getUserId());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resolve(TenantId tenantId, CustomerId customerId, UserId userId) {
|
|
||||||
List<ListenableFuture<DynamicValueKeySub>> futures = new ArrayList<>();
|
|
||||||
for (DynamicValueKey key : dynamicValues.keySet()) {
|
|
||||||
switch (key.getSourceType()) {
|
|
||||||
case CURRENT_TENANT:
|
|
||||||
futures.add(resolveEntityValue(tenantId, tenantId, key));
|
|
||||||
break;
|
|
||||||
case CURRENT_CUSTOMER:
|
|
||||||
if (customerId != null && !customerId.isNullUid()) {
|
|
||||||
futures.add(resolveEntityValue(tenantId, customerId, key));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case CURRENT_USER:
|
|
||||||
if (userId != null && !userId.isNullUid()) {
|
|
||||||
futures.add(resolveEntityValue(tenantId, userId, key));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Map<EntityId, Map<String, DynamicValueKeySub>> tmpSubMap = new HashMap<>();
|
|
||||||
for (DynamicValueKeySub sub : Futures.successfulAsList(futures).get()) {
|
|
||||||
tmpSubMap.computeIfAbsent(sub.getEntityId(), tmp -> new HashMap<>()).put(sub.getKey().getSourceAttribute(), sub);
|
|
||||||
}
|
|
||||||
for (EntityId entityId : tmpSubMap.keySet()) {
|
|
||||||
Map<String, Long> keyStates = new HashMap<>();
|
|
||||||
Map<String, DynamicValueKeySub> dynamicValueKeySubMap = tmpSubMap.get(entityId);
|
|
||||||
dynamicValueKeySubMap.forEach((k, v) -> keyStates.put(k, v.getLastUpdateTs()));
|
|
||||||
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
|
|
||||||
TbAttributeSubscription sub = TbAttributeSubscription.builder()
|
|
||||||
.serviceId(serviceId)
|
|
||||||
.sessionId(sessionRef.getSessionId())
|
|
||||||
.subscriptionId(subIdx)
|
|
||||||
.tenantId(sessionRef.getSecurityCtx().getTenantId())
|
|
||||||
.entityId(entityId)
|
|
||||||
.updateProcessor((subscription, subscriptionUpdate) -> dynamicValueSubUpdate(subscription.getSessionId(), subscriptionUpdate, dynamicValueKeySubMap))
|
|
||||||
.queryTs(createdTime)
|
|
||||||
.allKeys(false)
|
|
||||||
.keyStates(keyStates)
|
|
||||||
.scope(TbAttributeSubscriptionScope.SERVER_SCOPE)
|
|
||||||
.build();
|
|
||||||
subToDynamicValueKeySet.add(subIdx);
|
|
||||||
localSubscriptionService.addSubscription(sub, sessionRef);
|
|
||||||
}
|
|
||||||
} catch (InterruptedException | ExecutionException e) {
|
|
||||||
log.info("[{}][{}][{}] Failed to resolve dynamic values: {}", tenantId, customerId, userId, dynamicValues.keySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void dynamicValueSubUpdate(String sessionId, TelemetrySubscriptionUpdate subscriptionUpdate,
|
|
||||||
Map<String, DynamicValueKeySub> dynamicValueKeySubMap) {
|
|
||||||
Map<String, TsValue> latestUpdate = new HashMap<>();
|
|
||||||
subscriptionUpdate.getData().forEach((k, v) -> {
|
|
||||||
Object[] data = (Object[]) v.get(0);
|
|
||||||
latestUpdate.put(k, new TsValue((Long) data[0], (String) data[1]));
|
|
||||||
});
|
|
||||||
|
|
||||||
boolean invalidateFilter = false;
|
|
||||||
for (Map.Entry<String, TsValue> entry : latestUpdate.entrySet()) {
|
|
||||||
String k = entry.getKey();
|
|
||||||
TsValue tsValue = entry.getValue();
|
|
||||||
DynamicValueKeySub sub = dynamicValueKeySubMap.get(k);
|
|
||||||
if (sub.updateValue(tsValue)) {
|
|
||||||
invalidateFilter = true;
|
|
||||||
updateDynamicValuesByKey(sub, tsValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (invalidateFilter) {
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract boolean isDynamic();
|
public abstract boolean isDynamic();
|
||||||
|
|
||||||
public abstract void fetchData();
|
|
||||||
|
|
||||||
protected abstract void update();
|
|
||||||
|
|
||||||
public void clearSubscriptions() {
|
|
||||||
clearDynamicValueSubscriptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
stopped = true;
|
stopped = true;
|
||||||
cancelTasks();
|
|
||||||
clearSubscriptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
private static class DynamicValueKeySub {
|
|
||||||
private final DynamicValueKey key;
|
|
||||||
private final EntityId entityId;
|
|
||||||
private long lastUpdateTs;
|
|
||||||
private String lastUpdateValue;
|
|
||||||
|
|
||||||
boolean updateValue(TsValue value) {
|
|
||||||
if (value.getTs() > lastUpdateTs && (lastUpdateValue == null || !lastUpdateValue.equals(value.getValue()))) {
|
|
||||||
this.lastUpdateTs = value.getTs();
|
|
||||||
this.lastUpdateValue = value.getValue();
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private ListenableFuture<DynamicValueKeySub> resolveEntityValue(TenantId tenantId, EntityId entityId, DynamicValueKey key) {
|
|
||||||
ListenableFuture<Optional<AttributeKvEntry>> entry = attributesService.find(tenantId, entityId,
|
|
||||||
AttributeScope.SERVER_SCOPE, key.getSourceAttribute());
|
|
||||||
return Futures.transform(entry, attributeOpt -> {
|
|
||||||
DynamicValueKeySub sub = new DynamicValueKeySub(key, entityId);
|
|
||||||
if (attributeOpt.isPresent()) {
|
|
||||||
AttributeKvEntry attribute = attributeOpt.get();
|
|
||||||
sub.setLastUpdateTs(attribute.getLastUpdateTs());
|
|
||||||
sub.setLastUpdateValue(attribute.getValueAsString());
|
|
||||||
updateDynamicValuesByKey(sub, new TsValue(attribute.getLastUpdateTs(), attribute.getValueAsString()));
|
|
||||||
}
|
|
||||||
return sub;
|
|
||||||
}, MoreExecutors.directExecutor());
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
protected void updateDynamicValuesByKey(DynamicValueKeySub sub, TsValue tsValue) {
|
|
||||||
DynamicValueKey dvk = sub.getKey();
|
|
||||||
switch (dvk.getPredicateType()) {
|
|
||||||
case STRING:
|
|
||||||
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(tsValue.getValue()));
|
|
||||||
break;
|
|
||||||
case NUMERIC:
|
|
||||||
try {
|
|
||||||
Double dValue = Double.parseDouble(tsValue.getValue());
|
|
||||||
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(dValue));
|
|
||||||
} catch (NumberFormatException e) {
|
|
||||||
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(null));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case BOOLEAN:
|
|
||||||
Boolean bValue = Boolean.parseBoolean(tsValue.getValue());
|
|
||||||
dynamicValues.get(dvk).forEach(dynamicValue -> dynamicValue.setResolvedValue(bValue));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private void registerDynamicValues(KeyFilterPredicate predicate) {
|
|
||||||
switch (predicate.getType()) {
|
|
||||||
case STRING:
|
|
||||||
case NUMERIC:
|
|
||||||
case BOOLEAN:
|
|
||||||
Optional<DynamicValue> value = getDynamicValueFromSimplePredicate((SimpleKeyFilterPredicate) predicate);
|
|
||||||
if (value.isPresent()) {
|
|
||||||
DynamicValue dynamicValue = value.get();
|
|
||||||
DynamicValueKey key = new DynamicValueKey(
|
|
||||||
predicate.getType(),
|
|
||||||
dynamicValue.getSourceType(),
|
|
||||||
dynamicValue.getSourceAttribute());
|
|
||||||
dynamicValues.computeIfAbsent(key, tmp -> new ArrayList<>()).add(dynamicValue);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case COMPLEX:
|
|
||||||
((ComplexFilterPredicate) predicate).getPredicates().forEach(this::registerDynamicValues);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<DynamicValue<T>> getDynamicValueFromSimplePredicate(SimpleKeyFilterPredicate<T> predicate) {
|
|
||||||
if (predicate.getValue().getUserValue() == null) {
|
|
||||||
return Optional.ofNullable(predicate.getValue().getDynamicValue());
|
|
||||||
} else {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
@ -300,40 +107,6 @@ public abstract class TbAbstractSubCtx<T extends EntityCountQuery> {
|
|||||||
return sessionRef.getSecurityCtx().getId();
|
return sessionRef.getSecurityCtx().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void clearDynamicValueSubscriptions() {
|
|
||||||
if (subToDynamicValueKeySet != null) {
|
|
||||||
for (Integer subId : subToDynamicValueKeySet) {
|
|
||||||
localSubscriptionService.cancelSubscription(getTenantId(), sessionRef.getSessionId(), subId);
|
|
||||||
}
|
|
||||||
subToDynamicValueKeySet.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRefreshTask(ScheduledFuture<?> task) {
|
|
||||||
if (!stopped) {
|
|
||||||
this.refreshTask = task;
|
|
||||||
} else {
|
|
||||||
task.cancel(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelTasks() {
|
|
||||||
if (this.refreshTask != null) {
|
|
||||||
log.trace("[{}][{}] Canceling old refresh task", sessionRef.getSessionId(), cmdId);
|
|
||||||
this.refreshTask.cancel(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class DynamicValueKey {
|
|
||||||
@Getter
|
|
||||||
private final FilterPredicateType predicateType;
|
|
||||||
@Getter
|
|
||||||
private final DynamicValueSourceType sourceType;
|
|
||||||
@Getter
|
|
||||||
private final String sourceAttribute;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendWsMsg(CmdUpdate update) {
|
public void sendWsMsg(CmdUpdate update) {
|
||||||
wsLock.lock();
|
wsLock.lock();
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -29,7 +29,7 @@ import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
|
|||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ToString(callSuper = true)
|
@ToString(callSuper = true)
|
||||||
public class TbAlarmCountSubCtx extends TbAbstractSubCtx<AlarmCountQuery> {
|
public class TbAlarmCountSubCtx extends TbAbstractEntityQuerySubCtx<AlarmCountQuery> {
|
||||||
|
|
||||||
private final AlarmService alarmService;
|
private final AlarmService alarmService;
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,11 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx<AlarmDataQuery> {
|
|||||||
this.alarmsMap = new HashMap<>();
|
this.alarmsMap = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void clearSubscriptions() {
|
||||||
|
super.clearSubscriptions();
|
||||||
|
}
|
||||||
|
|
||||||
public void fetchAlarms() {
|
public void fetchAlarms() {
|
||||||
alarmInvocationAttempts++;
|
alarmInvocationAttempts++;
|
||||||
log.trace("[{}] Fetching alarms: {}", cmdId, alarmInvocationAttempts);
|
log.trace("[{}] Fetching alarms: {}", cmdId, alarmInvocationAttempts);
|
||||||
|
|||||||
@ -0,0 +1,129 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2024 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.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
import lombok.ToString;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.thingsboard.server.common.data.alarm.AlarmInfo;
|
||||||
|
import org.thingsboard.server.common.data.query.AlarmCountQuery;
|
||||||
|
import org.thingsboard.server.common.data.query.OriginatorAlarmFilter;
|
||||||
|
import org.thingsboard.server.dao.alarm.AlarmService;
|
||||||
|
import org.thingsboard.server.dao.attributes.AttributesService;
|
||||||
|
import org.thingsboard.server.dao.entity.EntityService;
|
||||||
|
import org.thingsboard.server.service.security.model.SecurityUser;
|
||||||
|
import org.thingsboard.server.service.ws.WebSocketService;
|
||||||
|
import org.thingsboard.server.service.ws.WebSocketSessionRef;
|
||||||
|
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmCountUpdate;
|
||||||
|
import org.thingsboard.server.service.ws.telemetry.cmd.v2.AlarmStatusCmd;
|
||||||
|
import org.thingsboard.server.service.ws.telemetry.sub.AlarmSubscriptionUpdate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class TbAlarmStatusSubCtx extends TbAbstractSubCtx {
|
||||||
|
|
||||||
|
private final AlarmService alarmService;
|
||||||
|
private final int alarmsPerAlarmStatusSubscriptionCacheSize;
|
||||||
|
|
||||||
|
private volatile TbAlarmStatusSubscription subscription;
|
||||||
|
|
||||||
|
public TbAlarmStatusSubCtx(String serviceId, WebSocketService wsService,
|
||||||
|
TbLocalSubscriptionService localSubscriptionService,
|
||||||
|
SubscriptionServiceStatistics stats, AlarmService alarmService,
|
||||||
|
int alarmsPerAlarmStatusSubscriptionCacheSize,
|
||||||
|
WebSocketSessionRef sessionRef, int cmdId) {
|
||||||
|
super(serviceId, wsService, localSubscriptionService, stats, sessionRef, cmdId);
|
||||||
|
this.alarmService = alarmService;
|
||||||
|
this.alarmsPerAlarmStatusSubscriptionCacheSize = alarmsPerAlarmStatusSubscriptionCacheSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isDynamic() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stop() {
|
||||||
|
super.stop();
|
||||||
|
localSubscriptionService.cancelSubscription(getTenantId(), sessionRef.getSessionId(), subscription.getSubscriptionId());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createSubscription(AlarmStatusCmd cmd) {
|
||||||
|
SecurityUser securityCtx = sessionRef.getSecurityCtx();
|
||||||
|
subscription = TbAlarmStatusSubscription.builder()
|
||||||
|
.serviceId(serviceId)
|
||||||
|
.sessionId(sessionRef.getSessionId())
|
||||||
|
.subscriptionId(sessionRef.getSessionSubIdSeq().incrementAndGet())
|
||||||
|
.tenantId(securityCtx.getTenantId())
|
||||||
|
.entityId(cmd.getOriginatorId())
|
||||||
|
.typeList(cmd.getTypeList())
|
||||||
|
.severityList(cmd.getSeverityList())
|
||||||
|
.updateProcessor(this::handleAlarmStatusSubscriptionUpdate)
|
||||||
|
.build();
|
||||||
|
localSubscriptionService.addSubscription(subscription, sessionRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendUpdate() {
|
||||||
|
sendWsMsg(subscription.createUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fetchActiveAlarms() {
|
||||||
|
log.trace("[{}, subId: {}] Fetching active alarms from DB", subscription.getSessionId(), subscription.getSubscriptionId());
|
||||||
|
OriginatorAlarmFilter originatorAlarmFilter = new OriginatorAlarmFilter(subscription.getEntityId(), subscription.getTypeList(), subscription.getSeverityList());
|
||||||
|
List<UUID> alarmIds = alarmService.findActiveOriginatorAlarms(subscription.getTenantId(), originatorAlarmFilter, alarmsPerAlarmStatusSubscriptionCacheSize);
|
||||||
|
|
||||||
|
subscription.getAlarmIds().addAll(alarmIds);
|
||||||
|
subscription.setHasMoreAlarmsInDB(alarmIds.size() == alarmsPerAlarmStatusSubscriptionCacheSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleAlarmStatusSubscriptionUpdate(TbSubscription<AlarmSubscriptionUpdate> sub, AlarmSubscriptionUpdate subscriptionUpdate) {
|
||||||
|
try {
|
||||||
|
AlarmInfo alarm = subscriptionUpdate.getAlarm();
|
||||||
|
Set<UUID> alarmsIds = subscription.getAlarmIds();
|
||||||
|
if (alarmsIds.contains(alarm.getId().getId())) {
|
||||||
|
if (!subscription.matches(alarm) || subscriptionUpdate.isAlarmDeleted()) {
|
||||||
|
alarmsIds.remove(alarm.getId().getId());
|
||||||
|
if (alarmsIds.isEmpty()) {
|
||||||
|
if (subscription.isHasMoreAlarmsInDB()) {
|
||||||
|
fetchActiveAlarms();
|
||||||
|
if (alarmsIds.isEmpty()) {
|
||||||
|
sendUpdate();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sendUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (subscription.matches(alarm)) {
|
||||||
|
if (alarmsIds.size() < alarmsPerAlarmStatusSubscriptionCacheSize) {
|
||||||
|
alarmsIds.add(alarm.getId().getId());
|
||||||
|
if (alarmsIds.size() == 1) {
|
||||||
|
sendUpdate();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
subscription.setHasMoreAlarmsInDB(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[{}, subId: {}] Failed to handle update for alarm status subscription: {}", subscription.getSessionId(), subscription.getSubscriptionId(), subscriptionUpdate, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -38,7 +38,7 @@ public class TbAlarmStatusSubscription extends TbSubscription<AlarmSubscriptionU
|
|||||||
private final Set<UUID> alarmIds = new HashSet<>();
|
private final Set<UUID> alarmIds = new HashSet<>();
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
private boolean cacheFull;
|
private boolean hasMoreAlarmsInDB;
|
||||||
@Getter
|
@Getter
|
||||||
private final List<String> typeList;
|
private final List<String> typeList;
|
||||||
@Getter
|
@Getter
|
||||||
@ -48,7 +48,7 @@ public class TbAlarmStatusSubscription extends TbSubscription<AlarmSubscriptionU
|
|||||||
public TbAlarmStatusSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
|
public TbAlarmStatusSubscription(String serviceId, String sessionId, int subscriptionId, TenantId tenantId, EntityId entityId,
|
||||||
BiConsumer<TbSubscription<AlarmSubscriptionUpdate>, AlarmSubscriptionUpdate> updateProcessor,
|
BiConsumer<TbSubscription<AlarmSubscriptionUpdate>, AlarmSubscriptionUpdate> updateProcessor,
|
||||||
List<String> typeList, List<AlarmSeverity> severityList) {
|
List<String> typeList, List<AlarmSeverity> severityList) {
|
||||||
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARM_STATUS, updateProcessor);
|
super(serviceId, sessionId, subscriptionId, tenantId, entityId, TbSubscriptionType.ALARMS, updateProcessor);
|
||||||
this.typeList = typeList;
|
this.typeList = typeList;
|
||||||
this.severityList = severityList;
|
this.severityList = severityList;
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ public class TbAlarmStatusSubscription extends TbSubscription<AlarmSubscriptionU
|
|||||||
public AlarmStatusUpdate createUpdate() {
|
public AlarmStatusUpdate createUpdate() {
|
||||||
return AlarmStatusUpdate.builder()
|
return AlarmStatusUpdate.builder()
|
||||||
.cmdId(getSubscriptionId())
|
.cmdId(getSubscriptionId())
|
||||||
.active(alarmIds.size() > 0)
|
.active(!alarmIds.isEmpty())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import org.thingsboard.server.service.ws.WebSocketSessionRef;
|
|||||||
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
|
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TbEntityCountSubCtx extends TbAbstractSubCtx<EntityCountQuery> {
|
public class TbEntityCountSubCtx extends TbAbstractEntityQuerySubCtx<EntityCountQuery> {
|
||||||
|
|
||||||
private volatile int result;
|
private volatile int result;
|
||||||
|
|
||||||
|
|||||||
@ -74,7 +74,6 @@ public class TbEntityLocalSubsInfo {
|
|||||||
stateChanged = true;
|
stateChanged = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ALARM_STATUS:
|
|
||||||
case ALARMS:
|
case ALARMS:
|
||||||
if (!newState.alarms) {
|
if (!newState.alarms) {
|
||||||
newState.alarms = true;
|
newState.alarms = true;
|
||||||
@ -169,7 +168,6 @@ public class TbEntityLocalSubsInfo {
|
|||||||
case NOTIFICATIONS_COUNT:
|
case NOTIFICATIONS_COUNT:
|
||||||
state.notifications = false;
|
state.notifications = false;
|
||||||
break;
|
break;
|
||||||
case ALARM_STATUS:
|
|
||||||
case ALARMS:
|
case ALARMS:
|
||||||
state.alarms = false;
|
state.alarms = false;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -16,5 +16,5 @@
|
|||||||
package org.thingsboard.server.service.subscription;
|
package org.thingsboard.server.service.subscription;
|
||||||
|
|
||||||
public enum TbSubscriptionType {
|
public enum TbSubscriptionType {
|
||||||
TIMESERIES, ATTRIBUTES, ALARMS, ALARM_STATUS, NOTIFICATIONS, NOTIFICATIONS_COUNT
|
TIMESERIES, ATTRIBUTES, ALARMS, NOTIFICATIONS, NOTIFICATIONS_COUNT
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,6 +174,7 @@ public class DefaultWebSocketService implements WebSocketService {
|
|||||||
cmdsHandlers.put(WsCmdType.ALARM_DATA_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
|
cmdsHandlers.put(WsCmdType.ALARM_DATA_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
|
||||||
cmdsHandlers.put(WsCmdType.ENTITY_COUNT_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
|
cmdsHandlers.put(WsCmdType.ENTITY_COUNT_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
|
||||||
cmdsHandlers.put(WsCmdType.ALARM_COUNT_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
|
cmdsHandlers.put(WsCmdType.ALARM_COUNT_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
|
||||||
|
cmdsHandlers.put(WsCmdType.ALARM_STATUS_UNSUBSCRIBE, newCmdHandler(this::handleWsDataUnsubscribeCmd));
|
||||||
cmdsHandlers.put(WsCmdType.NOTIFICATIONS, newCmdHandler(notificationCmdsHandler::handleUnreadNotificationsSubCmd));
|
cmdsHandlers.put(WsCmdType.NOTIFICATIONS, newCmdHandler(notificationCmdsHandler::handleUnreadNotificationsSubCmd));
|
||||||
cmdsHandlers.put(WsCmdType.NOTIFICATIONS_COUNT, newCmdHandler(notificationCmdsHandler::handleUnreadNotificationsCountSubCmd));
|
cmdsHandlers.put(WsCmdType.NOTIFICATIONS_COUNT, newCmdHandler(notificationCmdsHandler::handleUnreadNotificationsCountSubCmd));
|
||||||
cmdsHandlers.put(WsCmdType.MARK_NOTIFICATIONS_AS_READ, newCmdHandler(notificationCmdsHandler::handleMarkAsReadCmd));
|
cmdsHandlers.put(WsCmdType.MARK_NOTIFICATIONS_AS_READ, newCmdHandler(notificationCmdsHandler::handleMarkAsReadCmd));
|
||||||
|
|||||||
@ -25,6 +25,7 @@ public enum WsCmdType {
|
|||||||
ENTITY_COUNT,
|
ENTITY_COUNT,
|
||||||
ALARM_DATA,
|
ALARM_DATA,
|
||||||
ALARM_COUNT,
|
ALARM_COUNT,
|
||||||
|
ALARM_STATUS,
|
||||||
|
|
||||||
NOTIFICATIONS,
|
NOTIFICATIONS,
|
||||||
NOTIFICATIONS_COUNT,
|
NOTIFICATIONS_COUNT,
|
||||||
@ -36,5 +37,5 @@ public enum WsCmdType {
|
|||||||
ENTITY_DATA_UNSUBSCRIBE,
|
ENTITY_DATA_UNSUBSCRIBE,
|
||||||
ENTITY_COUNT_UNSUBSCRIBE,
|
ENTITY_COUNT_UNSUBSCRIBE,
|
||||||
NOTIFICATIONS_UNSUBSCRIBE,
|
NOTIFICATIONS_UNSUBSCRIBE,
|
||||||
ALARM_STATUS
|
ALARM_STATUS_UNSUBSCRIBE
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copyright © 2016-2024 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.ws.telemetry.cmd.v2;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.thingsboard.server.service.ws.WsCmdType;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class AlarmStatusUnsubscribeCmd implements UnsubscribeCmd {
|
||||||
|
|
||||||
|
private final int cmdId;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WsCmdType getType() {
|
||||||
|
return WsCmdType.ALARM_STATUS_UNSUBSCRIBE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -428,7 +428,7 @@ public class WebsocketApiTest extends AbstractControllerTest {
|
|||||||
String alarmId = alarms.get(i).getId().getId().toString();
|
String alarmId = alarms.get(i).getId().getId().toString();
|
||||||
doPost("/api/alarm/" + alarmId + "/clear", Alarm.class);
|
doPost("/api/alarm/" + alarmId + "/clear", Alarm.class);
|
||||||
}
|
}
|
||||||
AlarmStatusUpdate alarmStatusUpdate = JacksonUtil.fromString(getWsClient().waitForUpdate(), AlarmStatusUpdate.class);
|
AlarmStatusUpdate alarmStatusUpdate = JacksonUtil.fromString(getWsClient().waitForUpdate(TimeUnit.SECONDS.toMillis(5)), AlarmStatusUpdate.class);
|
||||||
Assert.assertNull(alarmStatusUpdate);
|
Assert.assertNull(alarmStatusUpdate);
|
||||||
|
|
||||||
//clear 6-th alarm should send update
|
//clear 6-th alarm should send update
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user