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);
}