Почему время ожидания моего обратного вызова WCF истекло?

У меня есть следующие контракты на обслуживание и обратный вызов (сокращенные):

Контракт на обслуживание:

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

    [OperationContract]
    void SubscribeStatusUpdate();
}

Контракт обратного звонка:

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

Внедрение службы:

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

Потребитель услуг:

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

SchedulerViewModel привязан к простому окну с текстовым полем и кнопкой через свойства Status и StopScheduler. WCF размещается в простом консольном приложении для отладки: решение настроено на запуск сначала узла службы (консольного приложения), а затем приложения WCF.

Когда я нажимаю кнопку в главном окне приложения, я ожидаю, что будет запущена команда, то есть вызов proxy.Stop();. Это должно изменить статус статуса службы и вызвать обратный вызов. Я думаю, что это так, но время обратного вызова истекает. Отладчик зависает на строке proxy.Stop();, и в итоге получаю сообщение об ошибке:

Эта операция запроса, отправленная на http://localhost:8089/TestService/SchedulerService/, не получила ответа в течение настроенного времени ожидания (00:00:59.9990000). Время, отведенное на эту операцию, могло быть частью более длительного тайм-аута. Это может быть связано с тем, что служба все еще обрабатывает операцию или потому, что службе не удалось отправить ответное сообщение. Рассмотрите возможность увеличения времени ожидания операции (приведя канал/прокси к IContextChannel и задав свойство OperationTimeout) и убедитесь, что служба может подключиться к клиенту.

Когда я использую SchedulerViewModel в консольном приложении, обратный вызов работает нормально, и модель представления печатает Status: Stopped в окне консоли. Как только я задействую другие потоки, обратный вызов больше не работает. Другие потоки представляют собой модель представления, поднимающую OnPropertyChanged для обновления связанного текстового поля, и я не знаю, участвуют ли еще какие-либо потоки в включении/отключении команды.

Ничто в вызываемом сервисном методе не должно занимать не более миллисекунд, и я считаю, что двигаюсь в правильном направлении, полагая, что это проблема с потоками и/или зависанием пользовательского интерфейса, поскольку я видел подобные проблемы во время исследования. В большинстве были совсем другие сценарии и глубоко технические решения.

Почему это происходит, и я ничего не могу сделать, используя довольно стандартную инфраструктуру и функции WPF и WCF, чтобы включить этот обратный вызов? Моя грустная альтернатива — чтобы сервис писал статус в файл, а модель представления смотрела файл. Как это для грязного обходного пути?


person ProfK    schedule 03.03.2015    source источник


Ответы (1)


К сожалению, вы создаете тупик в WPF.

  1. Вы блокируете свой поток пользовательского интерфейса, когда вы вызываете Stop синхронно.
  2. Сервер обрабатывает Stop запрос и, прежде чем вернуться к клиенту, обрабатывает все обратные вызовы.
  3. Обратный вызов с сервера обрабатывается синхронно, поэтому он блокирует возврат из Stop до тех пор, пока ваш обработчик обратного вызова в WPF не обработает обратный вызов StatusUpdate. Но обработчик StatusUpdate не может запуститься, так как ему нужен поток пользовательского интерфейса, а поток пользовательского интерфейса все еще ожидает завершения исходного запроса к Stop.

Если вы используете NET 4.5, решение простое. Обработчик вашего клика будет помечен как async, и вы вызовете await client.StopAsync() в своем клиенте.

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

Если вы используете NET 4.0, вам нужно будет вызывать Stop асинхронно каким-то другим способом. Скорее всего через TPL.

У вас нет этой проблемы в вашем консольном клиенте, потому что он просто запускает обратный вызов в другом потоке.

Я создал очень простое решение, показывающее разницу между WPF и консольным приложением на GitHub. В клиенте WPF вы найдете 3 кнопки, показывающие 2 способа запуска Stop асинхронно и 1 вызов синхронизации, который вызовет взаимоблокировку.

Кроме того, мне кажется, что вы вообще не обрабатываете отмену подписки — поэтому, как только ваш клиент отключится, сервер попытается вызвать мертвый обратный вызов, который может и, скорее всего, также вызовет вызовы Stop из другой клиент (ы) для тайм-аута. Поэтому в вашем классе обслуживания реализуйте что-то вроде:

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;
}
person milanio    schedule 10.03.2015
comment
Спасибо за такой исчерпывающий ответ. Я не могу проверить это сразу, но я думаю, что это стоит принять в любом случае. Я только что удалил все «исходящие сообщения» из WCF и только поднимал события на свой хост, чтобы сигнализировать об исключениях. Обновления состояния обрабатываются только моделью таймера, которая вызывает WCF и обновляет модель представления. Из пользовательского интерфейса вызывается только один «длительный» метод WCF, который вызывается с использованием await. Я хочу вернуться к обратным вызовам с помощью async, но я не могу вносить больше изменений, пока не выпущу хотя бы альфа-версию. - person ProfK; 10.03.2015
comment
Не беспокойтесь, удачи с альфой! Как только вы сможете протестировать его, просто используйте пример проекта, который прикреплен к ответу - он показывает различные подходы к обработке обратного вызова. - person milanio; 10.03.2015
comment
Я внес несколько тактических изменений с тех пор, как задал этот вопрос, и только сейчас пробую ваш ответ под совершенно другим углом, но вы помогли мне заставить обратный вызов работать, хотя и по-другому. Тем не менее, у меня все еще есть некоторые проблемы, может быть, по другому вопросу, но в вашем примере с GitHub я вижу, что вы создаете и подписываете прокси-сервер в каждом обработчике. Есть ли причина для этого, а не для этого в моем модели представления? - person ProfK; 13.03.2015
comment
Привет @ProfK, нет причин, по которым вам нужно подписываться каждый раз - пример GitHub был просто для демонстрации того, как возникает взаимоблокировка и как ее избежать. Вы можете подписаться на виртуальную машину при ее инициализации, а затем отключиться только при удалении виртуальной машины. Клиент должен держать канал открытым, иначе он может пропустить некоторые события, запущенные на сервере. - person milanio; 15.03.2015
comment
Да, спасибо, @milanio. Я не был уверен, потому что у меня наконец-то заработал обратный вызов, но у меня было несколько проблем. Во-первых, я не могу подписаться на своей виртуальной машине, пока WCF не будет размещен; исправлено путем повторного создания экземпляра и подписки виртуальной машины, когда ее опрос сообщает, что хост работает. - person ProfK; 15.03.2015