You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
During a reconnect to a session the method CreateOrModifySubscription in Subscription.cs is called. In line 2022 the m_transferId and hence the TransferId is overwritten even if previously the TransferId was set.
Expected Behavior
The TransferId does not change so the new subscription can be associated with the previous subscription.
Steps To Reproduce
Operating system: Microsoft Windows 10
Tested with commit 0b23e5f on branch release/1.5.374
Create a new C# console project with the name OpcUaExample and .NET 8
Add the project Opc.Ua.Client.csproj from UA-.NETStandard/Libraries/Opc.Ua.Client_ to the solution
Add a project reference to Opc.Ua.Client to OpcUaExample
Replace the contents of Program.cs with the following:
usingOpc.Ua;usingOpc.Ua.Client;usingOpc.Ua.Configuration;usingISession=Opc.Ua.Client.ISession;namespaceOpcUaExample;/// <summary>/// Class containing the entry point of the program./// </summary>publicclassProgram{/// <summary>/// Entry point of the program./// </summary>/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>publicstaticasyncTaskMain(){usingOpcUaSessionKeeperopcUaSessionKeeper=new("opc.tcp://localhost:62541/Quickstarts/ReferenceServer");awaitopcUaSessionKeeper.ConnectOpcUaSession();Console.WriteLine("Executing {0}",nameof(opcUaSessionKeeper.MonitorNodeValue));opcUaSessionKeeper.MonitorNodeValue();opcUaSessionKeeper.WriteNodeValue(100);awaitTask.Delay(TimeSpan.FromSeconds(2));Console.WriteLine();Console.WriteLine("Please restart the OPC UA server to see the reconnection handler in action. Press any key to continue after the restart.");Console.WriteLine();Console.ReadKey(intercept:true);opcUaSessionKeeper.WriteNodeValue(101);awaitTask.Delay(TimeSpan.FromSeconds(2));opcUaSessionKeeper.RemoveSubscriptions();Console.WriteLine();awaitopcUaSessionKeeper.DisconnectOpcUaSession();Console.WriteLine("Press any key to exit");Console.ReadKey(intercept:true);}}/// <summary>/// Class to manage a OPC UA session./// </summary>publicclassOpcUaSessionKeeper:IDisposable{privatereadonlystring_opcUaUri;privateISession?_opcUaSession;privateSessionReconnectHandler?_sessionReconnectHandler;privatereadonlyushort_opcUaCertificateLifetimeInMonths=180;privatereadonlyList<Subscription>_subscriptions=[];/// <summary>/// Constructor to instantiate an <see cref="OpcUaSessionKeeper"/>./// </summary>publicOpcUaSessionKeeper(stringuri){_opcUaUri=uri;}/// <inheritdoc/>publicvoidDispose(){DisconnectOpcUaSession().Wait();}/// <summary>/// Creates a new OPC UA <see cref="ISession"/> and connects to it./// </summary>publicasyncTaskConnectOpcUaSession(){// define the OPC UA client applicationApplicationInstanceapplication=new(){ApplicationType=ApplicationType.Client,};// load the application configurationstringapplicationConfigurationFilePath=Path.Combine(AppContext.BaseDirectory,"OpcUaExample.Config.xml");ApplicationConfigurationconfig=awaitapplication.LoadApplicationConfiguration(applicationConfigurationFilePath,silent:false);try{// check the application certificate.awaitapplication.CheckApplicationInstanceCertificate(silent:false,minimumKeySize:0,_opcUaCertificateLifetimeInMonths);}catch(Exceptionex){stringbaseLoggingMessage="Exception occured during check of certificate.";Console.WriteLine("{0} Cause: {1}: {2}.",baseLoggingMessage,ex.GetType(),ex.Message);// try deleting the old certificate and creating a new oneawaitapplication.DeleteApplicationInstanceCertificate();application.ApplicationConfiguration.SecurityConfiguration.ApplicationCertificate.Certificate=null;// create a new application certificateawaitapplication.CheckApplicationInstanceCertificate(silent:false,minimumKeySize:0,_opcUaCertificateLifetimeInMonths);Console.WriteLine("Deleted the old application certificate and created a new one successfully.");}// SessionTimeOut >= KeepAliveTimeoutuintsessionTimeOutMs=(uint)TimeSpan.FromSeconds(60).TotalMilliseconds;intkeepAliveIntervalMs=(int)TimeSpan.FromSeconds(30).TotalMilliseconds;stringserverUri=_opcUaUri;UserIdentity?userIdentity=null;Console.WriteLine("Connecting to OPC UA server {0}.",serverUri);// configure endpoint for OPC UAEndpointDescriptionendpointDescription=CoreClientUtils.SelectEndpoint(config,serverUri,useSecurity:true);EndpointConfigurationendpointConfiguration=EndpointConfiguration.Create(config);ConfiguredEndpointendpoint=new(null,endpointDescription,endpointConfiguration);Sessionsession=awaitSession.Create(config,endpoint,updateBeforeConnect:false,sessionName:config.ApplicationName,sessionTimeOutMs,userIdentity,preferredLocales:null);session.KeepAliveInterval=keepAliveIntervalMs;session.DeleteSubscriptionsOnClose=false;session.TransferSubscriptionsOnReconnect=true;Console.WriteLine("New session for OPC UA created with session name {0} for server {1}.",session.SessionName,serverUri);_sessionReconnectHandler=newSessionReconnectHandler(reconnectAbort:true,maxReconnectPeriod:(int)TimeSpan.FromMinutes(5).TotalMilliseconds);session.KeepAlive+=RecoverSessionOnError;_opcUaSession=session;}privatevoidRecoverSessionOnError(ISessionsession,KeepAliveEventArgse){if(e.Status?.StatusCode.Code!=StatusCodes.BadSecureChannelClosed){return;}Console.WriteLine("Received bad status {0} for session with name {1} for server {2}. Recovering session with id {3}.",e.Status,session.SessionName,string.Join(", ",session.ServerUris.ToArray()),session.SessionId);_sessionReconnectHandler!.BeginReconnect(session,(int)TimeSpan.FromSeconds(1).TotalMilliseconds,ReconnectCompleted);// Cancel sending a new keep alive request because reconnect is triggered.e.CancelKeepAlive=true;}privatevoidReconnectCompleted(object?sender,EventArgse){SessionReconnectHandlersessionReconnectHandler=(SessionReconnectHandler)sender!;if(sessionReconnectHandler.Session==null){Console.WriteLine("Session which was tried to recover recovered itself.");return;}ISessionoriginalSession=_opcUaSession!;// ensure only a new instance is disposed// after reactivate, the same session instance may be returnedif(ReferenceEquals(originalSession,sessionReconnectHandler.Session)){Console.WriteLine("Session with name {0} for server {1} was reactivated.",sessionReconnectHandler.Session.SessionName,string.Join(", ",sessionReconnectHandler.Session.ServerUris.ToArray()));return;}_opcUaSession=sessionReconnectHandler.Session;originalSession.Dispose();Console.WriteLine("Reconnected to a new session with name {0} for server {1}.",sessionReconnectHandler.Session.SessionName,string.Join(", ",sessionReconnectHandler.Session.ServerUris.ToArray()));}/// <summary>/// Closes the OPC UA <see cref="ISession"/> if a connection exists./// </summary>publicasyncTaskDisconnectOpcUaSession(){if(_opcUaSession==null){return;}_opcUaSession.KeepAlive-=RecoverSessionOnError;await_opcUaSession.CloseAsync();_opcUaSession.Dispose();}/// <summary>/// Updates the value of a node./// </summary>/// <param name="value">The new value for a node.</param>/// <exception cref="InvalidOperationException">/// <see cref="InvalidOperationException"/> is thrown when this/// function is called when the client is not connected./// </exception>publicvoidWriteNodeValue(intvalue){if(_opcUaSession==null){thrownewInvalidOperationException("Session is null");}NodeIdnodeToWrite=new(value:2044,namespaceIndex:3);WriteValueCollectionwriteValueCollection=[];WriteValuewriteValue=new(){NodeId=nodeToWrite,AttributeId=Attributes.Value,Value=newDataValue(value),};writeValueCollection.Add(writeValue);ResponseHeaderresponseHeader=_opcUaSession.Write(null,writeValueCollection,outStatusCodeCollectionstatusCodeCollection,outDiagnosticInfoCollectiondiagnosticInfoCollection);}/// <summary>/// Creates a subscription to monitor value changes of a node./// </summary>/// <exception cref="InvalidOperationException">/// <see cref="InvalidOperationException"/> is thrown when this/// function is called when the client is not connected./// </exception>publicvoidMonitorNodeValue(){if(_opcUaSession==null){thrownewInvalidOperationException("Session is null");}NodeIdnodeToMonitor=new(value:2044,namespaceIndex:3);intpublishingIntervalMs=(int)TimeSpan.FromSeconds(1).TotalMilliseconds;intsamplingIntervalMs=(int)TimeSpan.FromSeconds(0.5).TotalMilliseconds;uintqueueSize=(uint)Math.Ceiling((double)publishingIntervalMs/samplingIntervalMs);Subscriptionsubscription=new(_opcUaSession.DefaultSubscription){DisplayName=$"Subscription of OpcUaExample",PublishingEnabled=true,PublishingInterval=publishingIntervalMs,MinLifetimeInterval=(uint)TimeSpan.FromMinutes(2).TotalMilliseconds,};_opcUaSession.AddSubscription(subscription);// Create the subscription on server sidesubscription.Create();// Create MonitoredItems for data changesMonitoredItemmonitoredItem=new(subscription.DefaultItem){StartNodeId=nodeToMonitor,AttributeId=Attributes.Value,QueueSize=queueSize,SamplingInterval=samplingIntervalMs,DiscardOldest=true,};monitoredItem.Notification+=NotificationEventHandler;subscription.AddItem(monitoredItem);// Create the monitored items on server sidesubscription.ApplyChanges();Console.WriteLine("MonitoredItems created for SubscriptionId = {0}.",subscription.Id);_subscriptions.Add(subscription);}/// <summary>/// A function to print details of updates of a monitored node./// </summary>/// <param name="opcUaMonitoredItem">The monitored node which was updated.</param>/// <param name="e">Additional information for the monitored node.</param>publicvoidNotificationEventHandler(MonitoredItemopcUaMonitoredItem,MonitoredItemNotificationEventArgse){MonitoredItemNotificationnotification=(MonitoredItemNotification)e.NotificationValue;Console.WriteLine("Subscription id: {0}, sequence number: {1}, node id: {2}, sampling time: {3}, value: {4}",opcUaMonitoredItem.Subscription.Id,opcUaMonitoredItem.Subscription.SequenceNumber,opcUaMonitoredItem.StartNodeId,notification.Value.SourceTimestamp,notification.Value.Value);}/// <summary>/// Removes all subscriptions./// </summary>/// <exception cref="InvalidOperationException">/// <see cref="InvalidOperationException"/> is thrown when this/// function is called when the client is not connected./// </exception>publicvoidRemoveSubscriptions(){if(_opcUaSession==null){return;}Console.WriteLine("Removing subscriptions with ids [{0}].",string.Join(", ",_subscriptions.Select(subscription =>subscription.Id)));_opcUaSession.RemoveSubscriptions(_subscriptions);_subscriptions.Clear();}}
Create a file OpcUaExample.Config.xml and paste the following:
<?xml version="1.0" encoding="utf-8"?>
<ApplicationConfigurationxmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:ua="http://opcfoundation.org/UA/2008/02/Types.xsd"xmlns="http://opcfoundation.org/UA/SDK/Configuration.xsd"
>
<ApplicationName>OpcUaExample</ApplicationName>
<ApplicationUri>urn:localhost:OpcUaExample</ApplicationUri>
<ApplicationType>Client_1</ApplicationType>
<SecurityConfiguration>
<!-- Where the application instance certificate is stored (MachineDefault) -->
<ApplicationCertificate>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/own</StorePath>
<SubjectName>CN=OpcUaExample,DC=localhost</SubjectName>
</ApplicationCertificate>
<!-- Where the issuer certificate are stored (certificate authorities) -->
<TrustedIssuerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/issuer</StorePath>
</TrustedIssuerCertificates>
<!-- Where the trust list is stored -->
<TrustedPeerCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/trusted</StorePath>
</TrustedPeerCertificates>
<!-- The directory used to store invalid certficates for later review by the administrator. -->
<RejectedCertificateStore>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/rejected</StorePath>
</RejectedCertificateStore>
<!-- WARNING: The following setting (to automatically accept untrusted certificates) should be used for easy debugging purposes ONLY and turned off for production deployments! -->
<AutoAcceptUntrustedCertificates>true</AutoAcceptUntrustedCertificates>
<!-- WARNING: SHA1 signed certficates are by default rejected and should be phased out. only nano and embedded profiles are allowed to use sha1 signed certificates. -->
<RejectSHA1SignedCertificates>true</RejectSHA1SignedCertificates>
<RejectUnknownRevocationStatus>true</RejectUnknownRevocationStatus>
<MinimumCertificateKeySize>2048</MinimumCertificateKeySize>
<AddAppCertToTrustedStore>false</AddAppCertToTrustedStore>
<SendCertificateChain>true</SendCertificateChain>
<!-- Where the User trust list is stored-->
<TrustedUserCertificates>
<StoreType>Directory</StoreType>
<StorePath>%LocalFolder%/Certificates/trustedUser</StorePath>
</TrustedUserCertificates>
</SecurityConfiguration>
<TransportConfigurations></TransportConfigurations>
<TransportQuotas>
<OperationTimeout>120000</OperationTimeout>
<MaxStringLength>4194304</MaxStringLength>
<MaxByteStringLength>4194304</MaxByteStringLength>
<MaxArrayLength>65535</MaxArrayLength>
<MaxMessageSize>4194304</MaxMessageSize>
<MaxBufferSize>65535</MaxBufferSize>
<ChannelLifetime>300000</ChannelLifetime>
<SecurityTokenLifetime>3600000</SecurityTokenLifetime>
</TransportQuotas>
<ClientConfiguration>
<DefaultSessionTimeout>60000</DefaultSessionTimeout>
<WellKnownDiscoveryUrls>
<ua:String>opc.tcp://{0}:4840</ua:String>
<ua:String>http://{0}:52601/UADiscovery</ua:String>
<ua:String>http://{0}/UADiscovery/Default.svc</ua:String>
</WellKnownDiscoveryUrls>
<DiscoveryServers></DiscoveryServers>
<MinSubscriptionLifetime>10000</MinSubscriptionLifetime>
<OperationLimits>
<MaxNodesPerRead>2500</MaxNodesPerRead>
<MaxNodesPerHistoryReadData>1000</MaxNodesPerHistoryReadData>
<MaxNodesPerHistoryReadEvents>1000</MaxNodesPerHistoryReadEvents>
<MaxNodesPerWrite>2500</MaxNodesPerWrite>
<MaxNodesPerHistoryUpdateData>1000</MaxNodesPerHistoryUpdateData>
<MaxNodesPerHistoryUpdateEvents>1000</MaxNodesPerHistoryUpdateEvents>
<MaxNodesPerMethodCall>2500</MaxNodesPerMethodCall>
<MaxNodesPerBrowse>2500</MaxNodesPerBrowse>
<MaxNodesPerRegisterNodes>2500</MaxNodesPerRegisterNodes>
<MaxNodesPerTranslateBrowsePathsToNodeIds>2500</MaxNodesPerTranslateBrowsePathsToNodeIds>
<MaxNodesPerNodeManagement>2500</MaxNodesPerNodeManagement>
<MaxMonitoredItemsPerCall>2500</MaxMonitoredItemsPerCall>
</OperationLimits>
</ClientConfiguration>
</ApplicationConfiguration>
Make sure that the configuration file is copied to output directory, e.g. with adding the following to OpcUaExample.csproj:
Open the solution UA-.NETStandard/UA Reference.sln
Start the project ConsoleReferenceServer
Start the project OpcUaExample
OpcUaExample fails with ServiceResultException due to an untrusted certificate
Move the rejected certificate from ~\AppData\Local\OPC Foundation\pki\rejected\certs to ~\AppData\Local\OPC Foundation\pki\trusted\certs
Start OpcUaExample again
Wait until "Please restart the OPC UA server to see the reconnection handler in action. Press any key to continue after the restart." is printed to the console
Set a breakpoint in Opc.Ua.Client/Subscription/Subscription.cs in line 2022 (in solution with OpcUaExample)
Restart the ConsoleReferenceServer
When the breakpoint is hit check the value of `m_transferId_ which should have the value of the previous subscription id (compare with value printed in console)
Step over the line and check the value of m_transferId which is now overwritten
Environment
- OS: Microsoft Windows 10
- Environment: Visual Studio 2022 17.11.5
- Runtime: .NET 8.0
- Component: Opc.Ua.Client
- Server: Reference Server
- Client: self-made
Anything else?
No response
The text was updated successfully, but these errors were encountered:
@ganko-pi If I execute the test in the way you are describing and restart the server, the server has no knowledge of the existing subscription, therefore the transfer fails and the client recreates the subscription with the same Monitored Items. I see no real bug in the implementation.
I am sorry the links I provided in Current behavior and step 3 point to the master branch and hence the target of the link was altered since the creation of the original post. Now the line of the current state of master (commit b38406c) for setting the breakpoint is line 2054. I updated the original links to point to commit 0b23e5f I tested with and added an step 3a.
Nonetheless the bug still exists. I carefully followed the description again with a new clone of the repository and a new project to ensure the correctness of it. When hitting the breakpoint m_transferId is the subscription id of the previous subscription before the restart of the server. But the line where the breakpoint is set overrides m_transferId. To my understanding m_transferId is a client side variable storing the id of a subscription before it was altered due to a reconnection to the server. If this is not the case and m_transferId should be set by the server then I agree with you that the server can not have knowledge about a subscription id before a restart of the server and this issue can be closed.
@ganko-pi the client uses the m_transferId to Transfer the subscription after a reconnect to a new Session. However with your Testcase the Transfer of the Session fails and therefore the client creates a new subscription overwriting the existing Transfer id that provides No value any more as the subscription Transfer failed.
You can se the cause of this in my opinnion correct behaviour in the Session Claas.
Type of issue
Current Behavior
During a reconnect to a session the method
CreateOrModifySubscription
in Subscription.cs is called. In line 2022 the m_transferId and hence the TransferId is overwritten even if previously the TransferId was set.Expected Behavior
The TransferId does not change so the new subscription can be associated with the previous subscription.
Steps To Reproduce
3a (added later) switch to commit 0b23e5f
m_transferId
which is now overwrittenEnvironment
Anything else?
No response
The text was updated successfully, but these errors were encountered: