/* * Copyright 2016 Kevin Herron * * 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 com.digitalpetri.opcua.sdk.client.subscriptions; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; import com.digitalpetri.opcua.sdk.client.OpcUaClient; import com.digitalpetri.opcua.sdk.client.SessionActivityListener; import com.digitalpetri.opcua.sdk.client.api.UaSession; import com.digitalpetri.opcua.sdk.client.api.subscriptions.UaSubscription; import com.digitalpetri.opcua.sdk.client.api.subscriptions.UaSubscriptionManager; import com.digitalpetri.opcua.stack.core.StatusCodes; import com.digitalpetri.opcua.stack.core.UaException; import com.digitalpetri.opcua.stack.core.types.builtin.DateTime; import com.digitalpetri.opcua.stack.core.types.builtin.ExtensionObject; import com.digitalpetri.opcua.stack.core.types.builtin.NodeId; import com.digitalpetri.opcua.stack.core.types.builtin.StatusCode; import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UByte; import com.digitalpetri.opcua.stack.core.types.builtin.unsigned.UInteger; import com.digitalpetri.opcua.stack.core.types.structured.CreateSubscriptionResponse; import com.digitalpetri.opcua.stack.core.types.structured.DataChangeNotification; import com.digitalpetri.opcua.stack.core.types.structured.EventFieldList; import com.digitalpetri.opcua.stack.core.types.structured.EventNotificationList; import com.digitalpetri.opcua.stack.core.types.structured.ModifySubscriptionResponse; import com.digitalpetri.opcua.stack.core.types.structured.MonitoredItemNotification; import com.digitalpetri.opcua.stack.core.types.structured.NotificationMessage; import com.digitalpetri.opcua.stack.core.types.structured.PublishRequest; import com.digitalpetri.opcua.stack.core.types.structured.PublishResponse; import com.digitalpetri.opcua.stack.core.types.structured.RepublishResponse; import com.digitalpetri.opcua.stack.core.types.structured.RequestHeader; import com.digitalpetri.opcua.stack.core.types.structured.StatusChangeNotification; import com.digitalpetri.opcua.stack.core.types.structured.SubscriptionAcknowledgement; import com.digitalpetri.opcua.stack.core.util.ExecutionQueue; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.digitalpetri.opcua.stack.core.types.builtin.unsigned.Unsigned.ubyte; import static com.digitalpetri.opcua.stack.core.types.builtin.unsigned.Unsigned.uint; import static com.google.common.collect.Lists.newArrayList; public class OpcUaSubscriptionManager implements UaSubscriptionManager { public static final UInteger DEFAULT_MAX_NOTIFICATIONS_PER_PUBLISH = uint(65535); private final Logger logger = LoggerFactory.getLogger(getClass()); private final Map<UInteger, OpcUaSubscription> subscriptions = Maps.newConcurrentMap(); private final List<SubscriptionListener> subscriptionListeners = Lists.newCopyOnWriteArrayList(); private final ConcurrentMap<NodeId, AtomicLong> pendingCountMap = Maps.newConcurrentMap(); private final List<SubscriptionAcknowledgement> acknowledgements = newArrayList(); private final ExecutionQueue deliveryQueue; private final ExecutionQueue processingQueue; private final OpcUaClient client; public OpcUaSubscriptionManager(OpcUaClient client) { this.client = client; deliveryQueue = new ExecutionQueue(client.getConfig().getExecutor()); processingQueue = new ExecutionQueue(client.getConfig().getExecutor()); client.addSessionActivityListener(new SessionActivityListener() { @Override public void onSessionInactive(UaSession session) { // This allows a session that gets re-activated to immediately start // publishing again instead of waiting for outstanding PublishRequests // from before the re-activation to expire/timeout. pendingCountMap.replace(session.getSessionId(), new AtomicLong(0)); } }); } @Override public CompletableFuture<UaSubscription> createSubscription(double requestedPublishingInterval) { // Keep-alive every ~10-12s or every publishing interval if longer. UInteger maxKeepAliveCount = uint(Math.max(1, (int) Math.ceil(10000.0 / requestedPublishingInterval))); // Lifetime must be 3x (or greater) the keep-alive count. UInteger maxLifetimeCount = uint(maxKeepAliveCount.intValue() * 6); return createSubscription( requestedPublishingInterval, maxLifetimeCount, maxKeepAliveCount, DEFAULT_MAX_NOTIFICATIONS_PER_PUBLISH, true, ubyte(0) ); } @Override public CompletableFuture<UaSubscription> createSubscription( double requestedPublishingInterval, UInteger requestedLifetimeCount, UInteger requestedMaxKeepAliveCount, UInteger maxNotificationsPerPublish, boolean publishingEnabled, UByte priority) { CompletableFuture<CreateSubscriptionResponse> future = client.createSubscription( requestedPublishingInterval, requestedLifetimeCount, requestedMaxKeepAliveCount, maxNotificationsPerPublish, publishingEnabled, priority ); return future.thenApply(response -> { OpcUaSubscription subscription = new OpcUaSubscription( client, response.getSubscriptionId(), response.getRevisedPublishingInterval(), response.getRevisedLifetimeCount(), response.getRevisedMaxKeepAliveCount(), maxNotificationsPerPublish, publishingEnabled, priority); subscriptions.put(subscription.getSubscriptionId(), subscription); maybeSendPublishRequests(); return subscription; }); } @Override public CompletableFuture<UaSubscription> modifySubscription( UInteger subscriptionId, double requestedPublishingInterval) { OpcUaSubscription subscription = subscriptions.get(subscriptionId); if (subscription == null) { CompletableFuture<UaSubscription> f = new CompletableFuture<>(); f.completeExceptionally(new UaException(StatusCodes.Bad_SubscriptionIdInvalid)); return f; } // Keep-alive every ~10-12s or every publishing interval if longer. UInteger requestedMaxKeepAliveCount = uint(Math.max(1, (int) Math.ceil(10000.0 / requestedPublishingInterval))); // Lifetime must be 3x (or greater) the keep-alive count. UInteger requestedLifetimeCount = uint(requestedMaxKeepAliveCount.intValue() * 6); CompletableFuture<UaSubscription> future = modifySubscription( subscriptionId, requestedPublishingInterval, requestedLifetimeCount, requestedMaxKeepAliveCount, subscription.getMaxNotificationsPerPublish(), subscription.getPriority() ); future.thenRun(this::maybeSendPublishRequests); return future; } @Override public CompletableFuture<UaSubscription> modifySubscription( UInteger subscriptionId, double requestedPublishingInterval, UInteger requestedLifetimeCount, UInteger requestedMaxKeepAliveCount, UInteger maxNotificationsPerPublish, UByte priority) { OpcUaSubscription subscription = subscriptions.get(subscriptionId); if (subscription == null) { CompletableFuture<UaSubscription> f = new CompletableFuture<>(); f.completeExceptionally(new UaException(StatusCodes.Bad_SubscriptionIdInvalid)); return f; } CompletableFuture<ModifySubscriptionResponse> future = client.modifySubscription( subscriptionId, requestedPublishingInterval, requestedLifetimeCount, requestedMaxKeepAliveCount, maxNotificationsPerPublish, priority ); return future.thenApply(response -> { subscription.setRevisedPublishingInterval(response.getRevisedPublishingInterval()); subscription.setRevisedLifetimeCount(response.getRevisedLifetimeCount()); subscription.setRevisedMaxKeepAliveCount(response.getRevisedMaxKeepAliveCount()); subscription.setMaxNotificationsPerPublish(maxNotificationsPerPublish); subscription.setPriority(priority); maybeSendPublishRequests(); return subscription; }); } @Override public CompletableFuture<UaSubscription> deleteSubscription(UInteger subscriptionId) { List<UInteger> subscriptionIds = newArrayList(subscriptionId); return client.deleteSubscriptions(subscriptionIds).thenApply(r -> { OpcUaSubscription subscription = subscriptions.remove(subscriptionId); maybeSendPublishRequests(); return subscription; }); } public void transferFailed(UInteger subscriptionId, StatusCode statusCode) { OpcUaSubscription subscription = subscriptions.remove(subscriptionId); if (subscription != null) { subscriptionListeners.forEach(l -> l.onSubscriptionTransferFailed(subscription, statusCode)); } } @Override public ImmutableList<UaSubscription> getSubscriptions() { return ImmutableList.copyOf(subscriptions.values()); } @Override public void addSubscriptionListener(SubscriptionListener listener) { subscriptionListeners.add(listener); } @Override public void removeSubscriptionListener(SubscriptionListener listener) { subscriptionListeners.remove(listener); } private long getMaxPendingPublishes() { long maxPendingPublishRequests = client.getConfig().getMaxPendingPublishRequests().longValue(); return Math.min(subscriptions.size() * 2, maxPendingPublishRequests); } private UInteger getTimeoutHint() { double minKeepAlive = subscriptions.values().stream() .map(s -> s.getRevisedPublishingInterval() * s.getRevisedMaxKeepAliveCount().doubleValue()) .min(Comparator.<Double>naturalOrder()) .orElse(client.getConfig().getRequestTimeout().doubleValue()); long timeoutHint = (long) (getMaxPendingPublishes() * minKeepAlive * 1.25); return uint(timeoutHint); } private void maybeSendPublishRequests() { long maxPendingPublishes = getMaxPendingPublishes(); if (maxPendingPublishes == 0) return; client.getSession().thenAccept(session -> { AtomicLong pendingCount = pendingCountMap.computeIfAbsent( session.getSessionId(), id -> new AtomicLong(0L)); for (long i = pendingCount.get(); i < maxPendingPublishes; i++) { if (pendingCount.incrementAndGet() <= maxPendingPublishes) { sendPublishRequest(session, pendingCount); } else { pendingCount.getAndUpdate(p -> (p > 0) ? p - 1 : 0); } } if (pendingCountMap.size() > 1) { // Prune any old sessions... pendingCountMap.entrySet().removeIf(e -> !e.getKey().equals(session.getSessionId())); } }); } private void sendPublishRequest(UaSession session, AtomicLong pendingCount) { SubscriptionAcknowledgement[] subscriptionAcknowledgements; synchronized (acknowledgements) { subscriptionAcknowledgements = acknowledgements.toArray( new SubscriptionAcknowledgement[acknowledgements.size()]); acknowledgements.clear(); } final UInteger requestHandle = client.nextRequestHandle(); RequestHeader requestHeader = new RequestHeader( session.getAuthenticationToken(), DateTime.now(), requestHandle, uint(0), null, getTimeoutHint(), null ); PublishRequest request = new PublishRequest( requestHeader, subscriptionAcknowledgements ); if (logger.isDebugEnabled()) { String[] ackStrings = Arrays.stream(subscriptionAcknowledgements) .map(ack -> String.format("id=%s/seq=%s", ack.getSubscriptionId(), ack.getSequenceNumber())) .toArray(String[]::new); logger.debug( "Sending PublishRequest, requestHandle={}, acknowledgements={}", requestHandle, Arrays.toString(ackStrings)); } client.<PublishResponse>sendRequest(request).whenCompleteAsync((response, ex) -> { pendingCount.getAndUpdate(p -> (p > 0) ? p - 1 : 0); if (response != null) { logger.debug("Received PublishResponse, sequenceNumber={}", response.getNotificationMessage().getSequenceNumber()); processingQueue.submit(() -> onPublishComplete(response)); maybeSendPublishRequests(); } else { StatusCode statusCode = UaException.extract(ex) .map(UaException::getStatusCode) .orElse(StatusCode.BAD); logger.debug("Publish service failure: {}", statusCode, ex); if (statusCode.getValue() != StatusCodes.Bad_TooManyPublishRequests) { maybeSendPublishRequests(); } synchronized (this.acknowledgements) { Collections.addAll(this.acknowledgements, subscriptionAcknowledgements); } UaException uax = UaException.extract(ex).orElse(new UaException(ex)); subscriptionListeners.forEach(l -> l.onPublishFailure(uax)); } }, client.getConfig().getExecutor()); } private void onPublishComplete(PublishResponse response) { logger.debug("onPublishComplete() response for subscriptionId={}", response.getSubscriptionId()); UInteger subscriptionId = response.getSubscriptionId(); OpcUaSubscription subscription = subscriptions.get(subscriptionId); if (subscription == null) return; NotificationMessage notificationMessage = response.getNotificationMessage(); long sequenceNumber = notificationMessage.getSequenceNumber().longValue(); long expectedSequenceNumber = subscription.getLastSequenceNumber() + 1; if (sequenceNumber > expectedSequenceNumber) { logger.warn("[id={}] expected sequence={}, received sequence={}. Calling Republish service...", subscriptionId, expectedSequenceNumber, sequenceNumber); processingQueue.pause(); processingQueue.submitToHead(() -> onPublishComplete(response)); republish(subscriptionId, expectedSequenceNumber, sequenceNumber).whenComplete((dataLost, ex) -> { if (ex != null) { logger.debug("Republish failed: {}", ex.getMessage(), ex); subscriptionListeners.forEach(l -> l.onNotificationDataLost(subscription)); } else { // Republish succeeded, possibly with some data loss, resume processing. if (dataLost) { subscriptionListeners.forEach(l -> l.onNotificationDataLost(subscription)); } } subscription.setLastSequenceNumber(sequenceNumber - 1); processingQueue.resume(); }); return; } subscription.setLastSequenceNumber(sequenceNumber); synchronized (acknowledgements) { for (UInteger available : response.getAvailableSequenceNumbers()) { acknowledgements.add(new SubscriptionAcknowledgement(subscriptionId, available)); } if (logger.isDebugEnabled()) { String[] seqStrings = Arrays.stream(response.getAvailableSequenceNumbers()) .map(sequence -> String.format("id=%s/seq=%s", subscriptionId, sequence)) .toArray(String[]::new); logger.debug( "[id={}] PublishResponse sequence={}, available sequences={}", subscriptionId, sequenceNumber, Arrays.toString(seqStrings)); } } deliveryQueue.submit(() -> onNotificationMessage(subscriptionId, notificationMessage)); } private CompletableFuture<Boolean> republish(UInteger subscriptionId, long fromSequence, long toSequence) { CompletableFuture<Boolean> future = new CompletableFuture<>(); republish(subscriptionId, fromSequence, toSequence, false, future); return future; } private void republish(UInteger subscriptionId, long fromSequence, long toSequence, boolean dataLost, CompletableFuture<Boolean> future) { if (fromSequence == toSequence) { future.complete(dataLost); } else { client.republish(subscriptionId, uint(fromSequence)).whenComplete((response, ex) -> { if (response != null) { try { onRepublishComplete(subscriptionId, response, uint(fromSequence)); republish(subscriptionId, fromSequence + 1, toSequence, dataLost, future); } catch (UaException e) { republish(subscriptionId, fromSequence + 1, toSequence, true, future); } } else { StatusCode statusCode = UaException.extract(ex) .map(UaException::getStatusCode) .orElse(StatusCode.BAD); if (statusCode.getValue() == StatusCodes.Bad_MessageNotAvailable) { republish(subscriptionId, fromSequence + 1, toSequence, true, future); } else { future.completeExceptionally(ex); } } }); } } private void onRepublishComplete(UInteger subscriptionId, RepublishResponse response, UInteger expectedSequenceNumber) throws UaException { NotificationMessage notificationMessage = response.getNotificationMessage(); UInteger sequenceNumber = notificationMessage.getSequenceNumber(); if (!sequenceNumber.equals(expectedSequenceNumber)) { throw new UaException(StatusCodes.Bad_SequenceNumberInvalid, "expected sequence=" + expectedSequenceNumber + ", received sequence=" + sequenceNumber); } deliveryQueue.submit(() -> onNotificationMessage(subscriptionId, notificationMessage)); } private void onNotificationMessage(UInteger subscriptionId, NotificationMessage notificationMessage) { DateTime publishTime = notificationMessage.getPublishTime(); logger.debug("onNotificationMessage(), subscriptionId={}, sequenceNumber={}, publishTime={}", subscriptionId, notificationMessage.getSequenceNumber(), publishTime); OpcUaSubscription subscription = subscriptions.get(subscriptionId); if (subscription == null) return; Map<UInteger, OpcUaMonitoredItem> items = subscription.getItemsByClientHandle(); for (ExtensionObject xo : notificationMessage.getNotificationData()) { Object o = xo.decode(); if (o instanceof DataChangeNotification) { DataChangeNotification dcn = (DataChangeNotification) o; int notificationCount = dcn.getMonitoredItems().length; logger.debug("Received {} MonitoredItemNotifications", notificationCount); for (MonitoredItemNotification min : dcn.getMonitoredItems()) { logger.trace("MonitoredItemNotification: clientHandle={}, value={}", min.getClientHandle(), min.getValue()); OpcUaMonitoredItem item = items.get(min.getClientHandle()); if (item != null) item.onValueArrived(min.getValue()); else logger.warn("no item for clientHandle=" + min.getClientHandle()); } if (notificationCount == 0) { subscriptionListeners.forEach(l -> l.onKeepAlive(subscription, publishTime)); } } else if (o instanceof EventNotificationList) { EventNotificationList enl = (EventNotificationList) o; for (EventFieldList efl : enl.getEvents()) { logger.trace("EventFieldList: clientHandle={}, values={}", efl.getClientHandle(), Arrays.toString(efl.getEventFields())); OpcUaMonitoredItem item = items.get(efl.getClientHandle()); if (item != null) item.onEventArrived(efl.getEventFields()); } } else if (o instanceof StatusChangeNotification) { StatusChangeNotification scn = (StatusChangeNotification) o; logger.debug("StatusChangeNotification: {}", scn.getStatus()); subscriptionListeners.forEach(l -> l.onStatusChanged(subscription, scn.getStatus())); if (scn.getStatus().getValue() == StatusCodes.Bad_Timeout) { subscriptions.remove(subscriptionId); maybeSendPublishRequests(); } } } } public void startPublishing() { maybeSendPublishRequests(); } public void clearSubscriptions() { subscriptions.clear(); } }