public IDisposable Stream(RequestStreamHandler <TRequest, TUpdate> requestStreamHandler) { return(_subscriber.Subscribe( message => _scheduler.Schedule(() => { var subscriptionId = message.Properties.GetString(RequestStreamKeys.SubscriptionId); var kind = message.Properties.GetString(RequestStreamKeys.RequestKind); if (kind == RequestKind.Subscription) { ProcessSubscription(subscriptionId, message, requestStreamHandler); } else if (kind == RequestKind.Unsubscription) { ProcessUnsubscription(subscriptionId); } else { Log.Warning("Unknown RequestStream RequestKind: '{requestKind}'", kind); } }))); }
private void ProcessSubscription(string subscriptionId, IMessage message, RequestStreamHandler <TRequest, TUpdate> requestStreamHandler) { // In order to prevent races/resource-leaks here we must: // * Monitor session diconnections before attempting to create a request context. // - Otherwise, a session could be destroyed and we would never clean up its resources, as we'd // miss the notification. // * Create the subscription to monitor sessions and add update our state all within a single // critical region. In addition to this, we must ensure session destruction callbacks are // fired on a different thread. // - Otherwise, we may attempt to remove the session's resources before they are added. var sessionId = message.SessionId; var replyDestination = message.ReplyTo; var subscription = new CompositeDisposable(); TRequest request; if (!TryDeserializeRequest(subscriptionId, message, out request)) { return; } var clientId = message.Properties.GetString(OperationKeys.ClientId); if (clientId == null) { Log.Warning("No ClientId found. Ignoring."); return; } var sessionDestroyedHandler = new AnonymousUserSessionHandler(_ => { }, _ => { lock (_subscriptions) { // TODO: Make the Java version clean all resources hooked up here. // ReSharper disable once AccessToDisposedClosure subscription.Dispose(); _subscriptions.Remove(subscriptionId); } }); lock (_subscriptions) { // TODO: How do we clean up when IsSessionRequired == false? if (IsSessionRequired) { subscription.Add(UserSessionCache.Subscribe(sessionId, sessionDestroyedHandler)); } var context = CreateRequestContext(message); if (context == null) { Log.Warning("Failed to create request context. Ignoring."); subscription.Dispose(); // Don't listen for session destruction if it doesn't exist. return; } // At this point we know the session exists or existed and we know it will be cleared up (after we // exit the critical region) by the sessionResourceCleaner if it is destroyed. _subscriptions.Add(subscriptionId, subscription); try { const int notFinished = 0; const int finished = 1; var subscriptionState = notFinished; var notificationSubscription = requestStreamHandler(context, request, new AnonymousStreamHandler <TUpdate>( // TODO: I remove the session from the lookup AND ALSO dipose subscription here. // This is analagous to the AutoDetachObserver<T> in Rx. Should we do the same in the Java version? // Review with John. -ZB update => OnUpdated(subscriptionId, replyDestination, update), error => { if (Interlocked.Exchange(ref subscriptionState, finished) == notFinished) { OnFailed(subscriptionId, replyDestination, error); } }, () => { if (Interlocked.Exchange(ref subscriptionState, finished) == notFinished) { OnCompleted(subscriptionId, replyDestination); } })); subscription.Add(Disposable.Create(() => { var hasAlreadyFinished = Interlocked.Exchange(ref subscriptionState, finished) == finished; if (!hasAlreadyFinished) { notificationSubscription.Dispose(); } })); } catch (Exception e) { const string error = "Failed to process request"; OnFailed(subscriptionId, replyDestination, new MessagingException(error, e)); Log.Error(error, e); } } SendAck(subscriptionId, replyDestination, clientId); }