OpcLabs EasyUAClient BUG Report : 1). Problem Summary Title: The ServerConditionChanged event generates an infinite loop after the server is disconnected, causing the RetroalDelay mechanism to fail and the client to never reconnect. Severity: Critical Impact Version: (Please fill in the version you are using) Component: OpcLabs.EasyOpc.UA.Toolkit.Client 2). Problem description When the OPC UA server suddenly loses power or experiences a network interruption, EasyUAClient will exhibit the following abnormal behavior: A large number of high-frequency, continuous ServerConditionChanged events (with a status of Disconnected), without the expected Connecting → Disconnected state transition Even if the server returns to normal (reboots after 5 minutes), the client continues to report Disconnected and can never automatically reconnect The user configured RetroalDelay=60000ms is completely invalid 3). Reproduce steps Create an EasyUAClient instance and configure Isolated=false Use Subscribe Multiple MonitoredItemes() to subscribe to monitoring items for multiple OPC UA servers Wait for the connection to be established normally and confirm that the data is being received normally Manually disconnect the power of one of the servers (simulating a network interruption) Observe the ServerConditionChanged event log After 5 minutes, restart the server Continue to observe the ServerConditionChanged event csharp easyUAClient = new EasyUAClient() { Isolated = false }; EasyUAClient.AdaptableParameters.SessionParameters.HoldPeriod = 15000; EasyUAClient.AdaptableParameters.SessionParameters.RetrialDelay = 60000; 4). Expected behavior vs actual behavior Expected behavior in the scene, actual behavior Trigger Disconnecting once when the server is disconnected → Disconnected triggers Disconnecting once, and then continues to trigger Disconnected frequently Reconnect attempts trigger Connecting every 60 seconds (RetroalDelay) → Disconnected. There will never be a Connecting event After the server is restored, if the connection is successful on the next attempt to reconnect, it will never be able to reconnect and will continue to report as disconnected Never able to reconnect, continuously reporting Disconnected 5). Root cause analysis After source code analysis, it was found that there is an infinite loop mechanism that causes the RetroalDelay mechanism to completely fail Circular chain: Server power outage ↓ SDK Subscription. Publishing Top=true (KeepAlive timeout detection) ↓ EasyUAClient: iPadOS publishingToppedChange() starts a 5-second timer ↓ 5 seconds later: Publishing Halted=true ↓ SubscriptPublishing HaltedChanged () sets a SessionException (new exception object) ↓ OnSessionExceptionChange() → _sessionState.Exception = value ↓ RetriableState.OnExceptionChange() → Failed(exception) ↓ ★ Failed Tick=Current time (reset!) ★ ↓ Meanwhile: Failed () → DisconnectWait() → Disconnect() ↓ Disconnect() found VirtualSession==null → directly set Disconnected (without Disconnecting) ↓ ★ Trigger ServerConditionChanged (Disconnected) ★ ↓ At the same time, the 5-second timer of the engine triggers the subscription Reconnector() ↓ InternalConnect() calls ConnectKeepAlive() → resets Publishers Top=false ↓ Because VirtualSession==null, InternalConnect() returns directly ↓ SDK detected that Publishing Top=true again ↓ Publishing Topped: From false to true, trigger the PnP Publishing ToppedChange() function ↓ Restart the 5-second timer .. ↓ 🔄 Infinite loop! Why does RetroalDelay fail //RetrachableState. FeedsReconnectNow() - Check if reconnection is required public virtual bool NeedsReconnectNow() { if (_exception != null) { //Need to wait for the Failed Tick+RetroalDelay time to pass return TickCountUtilities.IsIntervalOver(FailureTick, _retrialDelay, currentTick); } return false; } // RetriableState.Failed() - protected virtual void Failed(Exception exception) { FailureTick = _timing.GetTickCount64(); //★ Reset every time! ★ } Problem: The loop calls Failed () every~5 seconds, causing Failed Tick to be constantly reset. The user configured 60 second RetroalDelay can never be reached, and ReconnectRequest() will never be called 6). Code files and line numbers involved Description of file line number issue UAClientSubscriptionBase.cs 569-578 InternalConnect() 在 VirtualSession == null 时仍调用 ConnectKeepAlive() 重置状态 UAClientSubscriptionBase.cs 1370-1378 OnPublishingStoppedChange() Start the 5-second timer UAClientSubscriptionBase.cs 1457-1460 PublishingHaltTimerCallback PublishingHalted = true UAClientSessionBase.cs 717-718 SubscriptionPublishingHaltedChanged() Set a new SessionException UAClientSessionBase.cs 364-368 OnSessionExceptionChange() Trigger status update UAClientSessionBase.cs 436-439 Disconnect() 在 VirtualSession == null Set Disconnected directly without going through Disconnecting UAClientSessionState.cs 30-38 Failed() 调用 DisconnectWait() And reset the Failed Tick RetriableState.cs 69-73 Failed () Reset Failed Tick 7). Suggested repair plan Solution A: Prevent frequent resetting of Failed Ticks Add a conditional check in RetiableState.Failed() or UAClientSessionState.Failed() to avoid resetting Failed Ticks repeatedly in a short period of time protected virtual void Failed(Exception exception) { long currentTick = _timing.GetTickCount64(); if (FailureTick < 0 || (currentTick - FailureTick) > MinimumFailureInterval) { FailureTick = currentTick; } } Option B: Fix the InternalConnect() logic In UAClient Subscription Base. InternalConnect(), ConnectKeepAlive() is only called when VirtualSession exists protected virtual void InternalConnect() { InternalClientSession = ClientSession; UAVirtualSession virtualSession = ClientSession.GetVirtualSession(); if (virtualSession == null) { //Do not call ConnectKeepAlive() here! SubscriptionWarnings.Clear(); SubscriptionException = UAEngineException.Create(6401); return; } ConnectKeepAlive(); // ... } Solution C: Prevent Disconnect() from triggering events in a disconnected state In UAClientSessionBase.Disconnect(), if the current state is already Disconnected, the event should not be triggered again internal void Disconnect() { lock (base.Serialize) { if (GetVirtualSession() == null) { //Event triggered only when the status is not Disconnected if (ConnectedCondition.ConnectionState != ConnectionState.Disconnected) { ConnectedCondition = MakeConnectedCondition(ConnectionState.Disconnected, SessionException); } return; } // ... other code line... } } 8). Temporary avoidance plan During the waiting period for official repairs, you can try: Using Isolated=true mode: Each subscription uses an independent session, which may reduce the impact of issues Implementing anti shake logic in the ServerConditioned Changed processor: Ignoring short repeated Disconnected events 9). Sample Log 2026-03-02 11:18:37.103 Connected, opc.tcp://10.10.100.114:4840 ... running normally for several hours .. 2026-03-02 16:53:50.197 Disconnecting, opc. tcp://10.10.100.114:4840 (Server power outage) 2026-03-02 16:53:50.237 Disconnected, opc.tcp://10.10.100.114:4840 2026-03-02 16:53:51.471 Disconnected, opc.tcp://10.10.100.114:4840 2026-03-02 16:53:51.518 Disconnected, opc.tcp://10.10.100.114:4840 2026-03-02 16:53:55.494 Disconnected, opc.tcp://10.10.100.114:4840 ... lasting for several hours, with a set of Disconnected events every approximately 5 seconds .. The server was restored at 16:58, but the client continued to report Disconnected 2026-03-02 23:59:11.956 Disconnected, opc.tcp://10.10.100.114:4840 Can't we solve this problem by modifying parameters such as RetroalDelay=60000ms to control the reconnection interval? No, modifying the RetroalDelay parameter cannot solve this problem. Reason: Failed Tick is repeatedly reset in a loop plaintext Timeline illustration (RetroalDelay=60 seconds): T=0s: Failed () → Failed Tick=0. It takes T=60s to reconnect T=5s: Loop triggers Failed () → Failed Tick=5. It takes T=65s to reconnect T=10s: Loop triggers Failed () → Failed Tick=10, waiting until T=70s to reconnect T=15s: Loop triggers Failed () → Failed Tick=15. It takes T=75s to reconnect ... We will never reach the target time! What about setting the RetroalDelay very small? For example, set to 1000ms (1 second): plaintext T=0s: Failed () → Failed Tick=0 needs to wait until T=1s to reconnect T=1s: Check: 1s ≥ 1s ✓ Can be reconnected! But at this point, VirtualSession==null InternalConnect() is called → Reset Publishing Top=false SDK detected → Publishing Top=true T=5s: Loop triggers Failed () → Failed Tick=5 ... Conclusion This is a logical flaw in the code, not a problem that parameter configuration can solve. Core defects: InternalConnect() resets Publishing Top even when VirtualSession==null Failed () resets Failed Tick every time it is called These two issues result in ineffective parameter adjustments, and the code logic must be modified to fundamentally solve them.