Search code examples
.netwpfwcfeventswcf-callbacks

Why is my WCF callback timing out?


I have the following service and callback contracts (abridged):

Service Contract:

[ServiceContract(CallbackContract = typeof(ISchedulerServiceCallback))]
public interface ISchedulerService
{
    [OperationContract]
    void Stop();

    [OperationContract]
    void SubscribeStatusUpdate();
}

Callback Contract:

public interface ISchedulerServiceCallback
{
    [OperationContract(IsOneWay = true)] 
    void StatusUpdate(SchedulerStatus status);
}

Service Implementation:

[CallbackBehavior(UseSynchronizationContext = false, ConcurrencyMode = ConcurrencyMode.Multiple)] // Tried Reentrant as well.
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single)] // Single due to a timer in the service that must keep time across calls.
public class SchedulerService : ISchedulerService
{
    private static Action<SchedulerStatus> statusUpdate = delegate { };

    public void Stop()
    {
        Status = SchedulerStatus.Stopped;
        statusUpdate(Status);
    }

    private SchedulerStatus Status { get; set; }

    public void SubscribeStatusUpdate()
    {
        ISchedulerServiceCallback sub = OperationContext.Current.GetCallbackChannel<ISchedulerServiceCallback>();
        statusUpdate += sub.StatusUpdate;
    }
}

Service Consumer:

public class SchedulerViewModel : ViewModelBase,  ISchedulerServiceCallback
{
    private SchedulerServiceClient proxy;

    public SchedulerViewModel()
    {
        StopScheduler = new DelegateCommand(ExecuteStopSchedulerCommand, CanExecuteStopSchedulerCommand);
    }

    public void SubScribeStatusCallback()
    {
        ISchedulerServiceCallback call = this;
        InstanceContext ctx = new InstanceContext(call);
        proxy = new SchedulerServiceClient(ctx);
        proxy.SubscribeStatusUpdate();
    }

    private SchedulerStatus _status;
    private SchedulerStatus Status
    {
        get
        {
            return _status;
        }
        set
        {
            _status = value;
            OnPropertyChanged();
        }
    }

    public void StatusUpdate(SchedulerStatus newStatus)
    {
        Status = newStatus;
        Console.WriteLine("Status: " + newStatus);
    }

    public DelegateCommand StopScheduler { get; private set; }

    bool CanExecuteStopSchedulerCommand()
    {
        return true;
    }

    public void ExecuteStopSchedulerCommand()
    {
        proxy.Stop();
    }
}

The SchedulerViewModel is bound to a simple window with a textbox and a button, through its Status and StopScheduler properties. The WCF is hosted by a simple Console app for debugging: the solution is set to start the service host (console app) first, and then the WCF app.

When I click the button on the main app window, I expect the command to be invoked, i.e. calling proxy.Stop();. This should change the status of the service's status and invoke the callback. I think it does, but the callback times out. The debugger hangs on the line proxy.Stop();, and eventually I get the error message:

This request operation sent to http://localhost:8089/TestService/SchedulerService/ did not receive a reply within the configured timeout (00:00:59.9990000). The time allotted to this operation may have been a portion of a longer timeout. This may be because the service is still processing the operation or because the service was unable to send a reply message. Please consider increasing the operation timeout (by casting the channel/proxy to IContextChannel and setting the OperationTimeout property) and ensure that the service is able to connect to the client.

When I use the SchedulerViewModel in a Console app, the callback works fine, and the viewmodel prints Status: Stopped in the console window. As soon as I involve other threads, the callback no longer works. Other threads being the viewmodel raising OnPropertyChanged to updated the bound textbox, and I don't know if any more threads are involved in enabling/disabling the command.

Nothing in the service method invoked should take more than milliseconds at most, and I believe I am heading in the right direction believing this is a threading and/or UI hangup issue, as I have seen similar problems while doing research. Most were quite different scenarios and deeply technical solutions.

Why is this happening, and is there nothing I can do, using fairly standard WPF and WCF infrastructure and functions, to enable this callback? My sad alternative is for the service to write the status to a file, and the view model to watch the file. How is that for a dirty workaround?


Solution

  • Unfortunately, you are creating a deadlock in WPF.

    1. You block your UI thread when you call Stop synchronously.
    2. Server processes Stop request and before returning back to the client processes all callbacks.
    3. Callback from server is processed synchronously so it blocks returning from Stop until your callback handler in WPF handles StatusUpdate callback. But the StatusUpdate handler cannot start as it needs UI thread - and the UI thread is still waiting for the original request to Stop to finish.

    If you are using NET 4.5, the solution is easy. You "click" handler will be marked as async and you call await client.StopAsync() in your client.

    var ssc = new SchedulerServiceClient(new InstanceContext(callback));
    try
    {
        ssc.SubscribeStatusUpdate();
        await ssc.StopAsync();
    }
    finally
    {
        ssc.Close();
    }
    

    If you are using NET 4.0, you will need to invoke Stop asynchronously in some other way. Most likely through TPL.

    You don't have this problem in your console client because it just fires callback on different thread.

    I've created very simple solution showing the difference between WPF & Console App on GitHub. In the WPF client you will find 3 buttons - showing 2 ways how to fire Stop asynchronously and 1 sync call which will cause the deadlock.

    In addition, it seems to me that you don't handle unsubscribe at all - so once your client disconnects the server will try to invoke dead callback - which can and most likely will also cause calls to Stop from other client(s) to timeout. Therefore in your service class implement something like:

    public void SubscribeStatusUpdate()
    {
        var sub = OperationContext.Current.GetCallbackChannel<ISchedulerServiceCallback>();
    
        EventHandler channelClosed =null;
        channelClosed=new EventHandler(delegate
        {
            statusUpdate -= sub.StatusUpdate;
        });
        OperationContext.Current.Channel.Closed += channelClosed;
        OperationContext.Current.Channel.Faulted += channelClosed;
        statusUpdate += sub.StatusUpdate;
    }