К основному контенту

Паралельный вызов удаленных методов за один http-запрос используя WCF [Parallel executing remote methods in a one http-request using WCF]


Окружение
В данной статья будет рассмотрено клиент-серверное приложение. Клиента - Silverlight приложение, сервер - ASP.NET, коммуникация осуществляется WCF'ом. Но зачем нам все это...

Задача
Требование привычное, это повседневная форма редактирования полей сущности.
Допустим сущность - Книга, с полями: Название, Автор, Категория, Язык.
UI должен содержать контролы:
  • Название - текстовое поле;
  • Автор - выпадающий список, с перечнем всех авторов;
  • Категория - выпадающий список, с перечнем доступных категорий;
  • Язык - выпадающий список, с перечнем всех языков
  • Кнопка сохранения 
Вроде бы все предельно просто и понятно, какие могут быть проблемы?

Проблема
Что бы загрузить все данные, необходимые для нашей формы редактирования, требуется вызвать 4 удаленных серверных метода:
  1. Загрузить конкретную Книгу по ее идентификатору;
  2. Загрузить список всех Авторов, для заполнения ими выпадающего списка;
  3. Загрузить список доступных Категорий, для заполнения выпадающего списка;
  4. Загрузить список всех Языков, для заполнения выпадающего списка.
То есть осуществить 4 http-запроса на сервер. Вот и проблема -  при увеличении сложности сущности нам нужно будет увеличивать количество запросов на сервер. А это означает, что время загрузки формы будет увеличиваться. А возможно ли вообще сделать это всего лишь за один запроса на сервер? Об этом далее.

Решение
Каждый из запросов независим от других, значит их можно распараллеливать. Выход из такой ситуации довольно прост:
  1. Добавляем DTOшку Instruction на сервер. Собственно это инструкция, описывающая вызов метода MethodName c аргументами Arguments у типа InstanceType
    [DataContract]
    public class Instruction
    {
     [DataMember]
     public string InstanceType { get; set; }
    
     [DataMember]
     public string MethodName { get; set; }
    
     [DataMember]
     public object[] Arguments { get; set; }
    }
    
  2. Добавляем метод ParallelInvoke к нашей WCF службе, который будет обрабатывать список инструкций, выполнять их, и возвращать список результатов
    [ServiceContract]
    [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
    public class MyService
    {
    ...
    ...
    ...
     [OperationContract]
     public object[] ParallelInvoke(Instruction[] instruction)
     {
      ....
     }
    }
    
  3. Осталось реализовать ParallelInvoke, это просто: рефлекшином ищем тип указанный в инструкции, пропускаем/разрешаем его через IoC контейнер, ищем рефлекшином метод и запускаем его.
Данное решение является не типизированным, то есть на клиенте нам придется формировать инструкцию, в которой тип представлен строкой. Но для быстрой оптимизации тормозных мест решение пойдет.

Типизированная обертка. Здесь поле для фантазии, что-либо просте придумать так и не удалось, по этому опишу как я это сделал у себя.
  1. Все обращения к серверу осуществляются через объект-посредник, также я прикрутил вот эту штуку так что все мои запросы к серверу синхронны. В результате, например, для того чтобы загрузить сущность на клиент получим:
    [Dependency]
    public IBookRepository BookRepository { get; set; }
    
    private void OnLoad(){
     ...
     Book book = BookRepository.Load(bookId);
     ...
    }
    
  2. Внутри объект-посредник, делает всю грязную работу: формирует инструкцию и дергает серверный метод ParallelInvoke. В данном случае отправляется массив с одной инструкцией. Также, реализацию интерфейсов-посредников я осуществил используя DynamicProxy. Да, но это пока вызов одного метода, как быть с параллельным вызовом.
  3. Я поставил цель, что бы код приведенный ниже работал:
    [Dependency]
    public IRemoteCall RemoteCall { get; set; }
    [Dependency]
    public IBookRepository BookRepository { get; set; }
    [Dependency]
    public IAuthorRepository AuthorRepository { get; set; }
    [Dependency]
    public ICategoryRepository CategoryRepository { get; set; }
    [Dependency]
    public ILanguageRepository LanguageRepository { get; set; }
    
    private void OnLoad(){
     ...
     Book book = null;
     IList<Author> authors = null;
     IList<Category> categories = null;
     IList<Language> languages = null;
     RemoteCall.Parallel(
      () => book = BookRepository.Load(bookId),
      () => authors = AuthorRepository.LoadAll(),
      () => categories = CategoryRepository.LoadAll(),
      () => languages = LanguageRepository.LoadAll()
     );
     ...
    }
    
    Потратив пару часов девелопмента и код заработал. Словами описать решение я не рискнул, так что код ниже. Важное правило: все объекты-посредники (BookRepository, AuthorRepository, CategoryRepository, LanguageRepository) должны внутри вызывать метод public object Invoke(Instruction instruction) представленный в типе RemoteCallManager.

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Threading;
    
    public class RemoteCallManager
    {
        private class ProcessInfo
        {
            public Instruction Instruction;
            public object Result;
            public ManualResetEvent InstructionReady = new ManualResetEvent(false);
            public ManualResetEvent ResultReady = new ManualResetEvent(false);
            public ManualResetEvent ProcessIsDone = new ManualResetEvent(false);
        }
    
        public void Parallel(params Action[] actions)
        {
            IList<ProcessInfo> processes = new List<ProcessInfo>();
    
            ExtractInstructions(processes, actions);
    
            object[] results = ExtractResults(processes);
    
            ReturnResults(processes, results);
        }
    
        public object Invoke(Instruction instruction)
        {
            object deferredResult;
            if (TryReturnDeferredResult(instruction, out deferredResult))
            {
                return deferredResult;
            }
            else
            {
                return ParallelInvoke(new[] { instruction }).First();
            }
        }
    
        private static object[] ParallelInvoke(Instruction[] instructions)
        {
            // var client = GetServiceClient();
            IAsyncResult asyncResult = client.BeginParallelInvoke(new ObservableCollection<Instruction>(instructions), null, null);
            return client.EndParallelInvoke(asyncResult);
            return results.ToArray();
        }
    
        #region Deferred call engine
    
        [ThreadStatic]
        private static ProcessInfo deferredCall;
    
        private const int InstructionReadyTimeout = 10000;
    
        private static void ExtractInstructions(IList<ProcessInfo> processes, Action[] actions)
        {
            foreach (Action action in actions)
            {
                ProcessInfo process = new ProcessInfo();
                processes.Add(process);
                Thread thread = new Thread(() =>
                {
                    deferredCall = process;
                    action();
                    deferredCall = null;
                    process.ProcessIsDone.Set();
                });
                thread.Start();
                if (process.InstructionReady.WaitOne(InstructionReadyTimeout) == false)
                {
                    throw new NotSupportedException("Action must use only Mediator inside");
                }
            }
        }
    
        private static bool TryReturnDeferredResult(Instruction instruction, out object deferredResult)
        {
            deferredResult = null;
            if (deferredCall != null)
            {
                ProcessInfo processInfo = deferredCall;
                processInfo.Instruction = instruction;
                processInfo.InstructionReady.Set();
                processInfo.ResultReady.WaitOne();
                deferredResult = processInfo.Result;
                return true;
            }
            else
            {
                return false;
            }
        }
    
        private static object[] ExtractResults(IList<ProcessInfo> processes)
        {
            Instruction[] instructions = processes.Select(e => e.Instruction).ToArray();
            return ParallelInvoke(instructions);
        }
    
        private static void ReturnResults(IList<ProcessInfo> processes, object[] results)
        {
            if (results.Length != processes.Count)
            {
                throw new NotSupportedException("Number of client calls is not equal to number of server results");
            }
            for (int i = 0; i < results.Length; i++)
            {
                ProcessInfo processInfo = processes[i];
                processInfo.Result = results[i];
                processInfo.ResultReady.Set();
                processInfo.ProcessIsDone.WaitOne();
            }
        }
    
        #endregion
    }
    
    

P.S.: Буду признателен за комментарии, вопросы и идеи.

Комментарии