diff --git a/source/Zyan.Communication/ZyanConnection.cs b/source/Zyan.Communication/ZyanConnection.cs index 446fe839..fb4426d1 100644 --- a/source/Zyan.Communication/ZyanConnection.cs +++ b/source/Zyan.Communication/ZyanConnection.cs @@ -253,6 +253,10 @@ public ZyanConnection(string serverUrl, IClientProtocolSetup protocolSetup, Auth throw ex.PreserveStackTrace(); } + var reconnectEvents = new Action(ReconnectRemoteEventsCore); + var debounceInterval = ZyanSettings.ReconnectRemoteEventsDebounceInterval.TotalMilliseconds; + ReconnectRemoteEvents = reconnectEvents.Debounce((int)debounceInterval); + StartKeepSessionAliveTimer(); lock (_connections) { @@ -983,6 +987,16 @@ public void RefreshRegisteredComponents() _registeredComponents = new List(RemoteDispatcher.GetRegisteredComponents()); } + /// + /// Gets the remote events lock object. + /// + internal object RemoteEventsLock { get; } = new object(); + + /// + /// Reconnects to all remote events or delegates after a server restart. + /// + private Action ReconnectRemoteEvents { get; } + /// /// Reconnects to all remote events or delegates of any known proxy for this connection, after a server restart. /// @@ -991,36 +1005,27 @@ public void RefreshRegisteredComponents() /// private void ReconnectRemoteEventsCore() { - var subscriptions = AliveProxies.Select(p => p.GetActiveSubscriptions()).Where(s => !s.DelegateCorrelationSet.IsNullOrEmpty()).ToArray(); - var correlationSets = subscriptions.SelectMany(s => s.DelegateCorrelationSet).ToArray(); - PrepareCallContext(false); - RemoteDispatcher.ReconnectEventHandlers(subscriptions); - _localSubscriptionTracker.Reset(correlationSets); - } - - /// - /// Reconnects to all remote events or delegates after a server restart. - /// - private void ReconnectRemoteEvents() - { - if (!ZyanSettings.LegacyAsyncResubscriptions) + if (_isDisposed) { - ReconnectRemoteEventsCore(); return; } - // legacy method is unsafe as it silently swallows the exceptions - ThreadPool.QueueUserWorkItem(x => + try { - try - { - ReconnectRemoteEventsCore(); - } - catch (Exception ex) + lock (RemoteEventsLock) { - Trace.WriteLine("Error while restoring client subscriptions: {0}", ex); + var subscriptions = AliveProxies.Select(p => p.GetActiveSubscriptions()).Where(s => !s.DelegateCorrelationSet.IsNullOrEmpty()).ToArray(); + var correlationSets = subscriptions.SelectMany(s => s.DelegateCorrelationSet).ToArray(); + _localSubscriptionTracker.Reset(correlationSets); + + PrepareCallContext(false); + RemoteDispatcher.ReconnectEventHandlers(subscriptions); } - }); + } + catch (Exception ex) + { + Trace.WriteLine("Error while restoring client subscriptions: {0}", ex); + } } /// diff --git a/source/Zyan.Communication/ZyanProxy.cs b/source/Zyan.Communication/ZyanProxy.cs index a386d06a..5a51d503 100644 --- a/source/Zyan.Communication/ZyanProxy.cs +++ b/source/Zyan.Communication/ZyanProxy.cs @@ -416,8 +416,11 @@ private void AddRemoteEventHandlers(List correlationSet if (count > 0) { _connection.TrackRemoteSubscriptions(correlationSet); - _connection.PrepareCallContext(false); - _connection.RemoteDispatcher.AddEventHandlers(_interfaceType.FullName, correlationSet, _uniqueName); + lock (_connection.RemoteEventsLock) + { + _connection.PrepareCallContext(false); + _connection.RemoteDispatcher.AddEventHandlers(_interfaceType.FullName, correlationSet, _uniqueName); + } } } @@ -432,10 +435,13 @@ private void RemoveRemoteEventHandlers(List correlation var count = correlationSet.Count; if (count > 0) - { - _connection.UntrackRemoteSubscriptions(correlationSet); - _connection.PrepareCallContext(false); - _connection.RemoteDispatcher.RemoveEventHandlers(_interfaceType.FullName, correlationSet, _uniqueName); + { + lock (_connection.RemoteEventsLock) + { + _connection.UntrackRemoteSubscriptions(correlationSet); + _connection.PrepareCallContext(false); + _connection.RemoteDispatcher.RemoveEventHandlers(_interfaceType.FullName, correlationSet, _uniqueName); + } } } diff --git a/source/Zyan.Communication/ZyanSettings.cs b/source/Zyan.Communication/ZyanSettings.cs index 38378bfe..8b469528 100644 --- a/source/Zyan.Communication/ZyanSettings.cs +++ b/source/Zyan.Communication/ZyanSettings.cs @@ -54,11 +54,13 @@ public static class ZyanSettings public static bool LegacyIgnoreDuplicateRegistrations { get; set; } /// - /// Gets or sets a value indicating whether ZyanConnection restores subscriptions asynchronously. + /// Gets or sets a delay before ZyanConnection restores subscriptions. /// /// - /// Zyan v2.11 and below used to restore missing event subscriptions asynchronously. + /// Zyan v2.11 and below used to restore missing event subscriptions asynchronously, without any delay. + /// Zyan v2.12 optionally debounces this method, zero interval means that subscriptions are restored synchronously. + /// This setting affects new ZyanConnection instances and doesn't change any existing connections. /// - public static bool LegacyAsyncResubscriptions { get; set; } + public static TimeSpan ReconnectRemoteEventsDebounceInterval { get; set; } = TimeSpan.FromSeconds(1); } } diff --git a/source/Zyan.Tests/EventsTests.cs b/source/Zyan.Tests/EventsTests.cs index a0fc4229..1f48eb9b 100644 --- a/source/Zyan.Tests/EventsTests.cs +++ b/source/Zyan.Tests/EventsTests.cs @@ -179,8 +179,9 @@ private static ZyanComponentHost CreateZyanHost(int port, string name) return zyanHost; } - private static ZyanConnection CreateZyanConnection(int port, string name) + private static ZyanConnection CreateZyanConnection(int port, string name, int debounceInterval = 0) { + ZyanSettings.ReconnectRemoteEventsDebounceInterval = debounceInterval; return new ZyanConnection("null://NullChannel:" + port + "/" + name, true); } @@ -254,6 +255,58 @@ public void ZyanHostSubscriptionRelatedEventsAreRaised() ZyanHost.SubscriptionCanceled -= canceledHandler; } + [TestMethod] + public void ExceptionInEventHandlerCancelsTheSubscription() + { + ZyanSettings.LegacyBlockingEvents = true; + ZyanSettings.LegacyBlockingSubscriptions = true; + ZyanSettings.LegacyUnprotectedEventHandlers = true; + + var host = CreateZyanHost(5432, "ThirdServer"); + var conn = CreateZyanConnection(5432, "ThirdServer", 1000); + var proxy = conn.CreateProxy("Singleton2"); + + var handled = false; + var eventHandler = new EventHandler((s, e) => + { + handled = true; + throw new Exception(); + }); + + proxy.TestEvent += eventHandler; + + var subscriptionCanceled = false; + var clientSideException = default(Exception); + host.SubscriptionCanceled += (s, e) => + { + subscriptionCanceled = true; + clientSideException = e.Exception; + }; + + var subscriptionsRestored = false; + var restoredHandler = new EventHandler((s, e) => subscriptionsRestored = true); + host.SubscriptionsRestored += restoredHandler; + + // raise an event, catch exception and unsubscribe automatically + proxy.RaiseTestEvent(); + Assert.IsTrue(handled); + Assert.IsTrue(subscriptionCanceled); + Assert.IsFalse(subscriptionsRestored); + + // raise an event and check if it's ignored because reconnection isn't called yet + // due to the large debounce interval used in ZyanConnection + handled = false; + subscriptionsRestored = false; + proxy.RaiseTestEvent(); + Assert.IsFalse(handled); + Assert.IsFalse(subscriptionsRestored); + + // Note: dispose connection before the host + // so that it can unsubscribe from all remove events + conn.Dispose(); + host.Dispose(); + } + // The subscription gets restored on the next remote call to RaiseTestEvent: // local subscription checksum doesn't match remote checksum => re-subscribe => ok! [TestMethod] @@ -262,7 +315,6 @@ public void ExceptionInEventHandlerCancelsSubscriptionButTheConnetionResubscribe ZyanSettings.LegacyBlockingEvents = true; ZyanSettings.LegacyBlockingSubscriptions = true; ZyanSettings.LegacyUnprotectedEventHandlers = true; - ZyanSettings.LegacyAsyncResubscriptions = false; var handled = false; var eventHandler = new EventHandler((s, e) => @@ -308,11 +360,9 @@ public void ExceptionInEventHandlerCancelsSubscriptionButTheConnetionResubscribe [TestMethod] public void ZyanConnectionResubscribesAfterServerRestart() { - Trace.WriteLine("ZyanConnectionResubscribesAfterServerRestart"); ZyanSettings.LegacyBlockingEvents = true; ZyanSettings.LegacyBlockingSubscriptions = true; ZyanSettings.LegacyUnprotectedEventHandlers = true; - ZyanSettings.LegacyAsyncResubscriptions = false; var host = CreateZyanHost(5432, "SecondServer"); var conn = CreateZyanConnection(5432, "SecondServer");