Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

Управление длительными процессами: ASP.Net + Web service
Пользователи должны иметь возможность запуска длительных серверных процессов (загрузка объемных данных из внешних источников, архивирование и пр.). Время выполнения процессов может достигать десятков минут. Процессы могут по желанию пользователя объединяться в цепочки, в которых отдельные процессы выполняются последовательно. Необходимо предусмотреть блокировку от одновременного запуска одного и того же процесса разными пользователями.

Постановка задачи

Пользователи должны иметь возможность запуска длительных серверных процессов (загрузка объемных данных из внешних источников, архивирование и пр.). Время выполнения процессов может достигать десятков минут. Процессы могут по желанию пользователя объединяться в цепочки, в которых отдельные процессы выполняются последовательно. Необходимо предусмотреть блокировку от одновременного запуска одного и того же процесса разными пользователями.

Управление запуском и контроль за ходом выполнения процессов должны осуществляться через Web-интерфейс. При этом отключения от сервера, например, выключение браузера или переход на произвольный адрес, не должны прерывать выполняемые процессы, а при повторном подключении к странице управления должно показываться текущее состояние процессов. Подгружаемые на клиент модули желательно не использовать.

Непосредственное управление процессами должно быть реализовано в виде Web-сервиса, т.к. в перспективе возможно построение альтернативных клиентских интерфейсов (в т.ч. и Windows-программ) или встраивание описанной функциональности в другие прикладные программы.

Примечание: система безопасности в целях упрощения не рассматривается.

Решение задачи

Известно, что клиентский прокси-класс, генерируемый для обращения к Web-сервису, содержит как синхронный вариант вызова методов сервиса, так и асинхронный. Если, например, Web-сервис имеет метод GetData, то в прокси будет сгенерирован соответствующий синхронный метод GetData и пара методов для асинхронного вызова - BeginGetData и EndGetData.

Очевидно, что синхронный вариант для запуска длительных серверных процессов не подходит, т.к. время реакции системы, исчисляемое десятками минут, просто неприемлемо.

Посмотрим, помогут ли решению нашей задачи возможности клиентского прокси-класса по асинхронному вызову методов Web-сервиса.

Предлагаемая разработчиками Microsoft схема асинхронного использования Web-методов может быть представлена в виде следующей диаграммы:

Если учесть, что клиентом для Web-сервиса в нашей задаче выступает aspx-страница (еще точнее - ее серверный код), то становится понятным, что время отклика опять окажется непозволительно большим.

Разработаем такую архитектуру системы, которая позволила бы запускать длительные процессы и не дожидаться их завершения, а контролировать ход выполнения будем по мере необходимости из любого, в том числе и из вновь созданного клиентского объекта:

Здесь СостояниеПроцессов - некоторый глобальный (в контексте Web-сервиса) объект, который хранит информацию о выполнении процессов, а Процесс - объект, непосредственно выполняющий назначенный процесс. Причем со стороны Web-сервиса объект СостояниеПроцессов доступен только по чтению, а Процесс имеет возможность записывать в него информацию о своем выполнении.

В результате работа всей системы распадается на два относительно независимых сценария.

Сценарий чтения клиентом информации о выполняемых процессах:

  1. Создается клиентский объект Клиент.
  2. Клиент обращается к Web-сервису для чтения информации о выполняемых процессах.
  3. Web-сервис опрашивает объект СостояниеПроцессов и возвращает информацию Клиенту.
  4. Объект Клиента выполняет необходимую обработку полученной информации и уничтожается.

Сценарий запуска процессов:

  1. Создается клиентский объект Клиент.
  2. Клиент передает Web-сервису команду на запуск процесса.
  3. Web-сервис опрашивает объект СостояниеПроцессов и, если назначенный процесс не находится в активном состоянии, создает объект Процесс и запускает его. Управление возвращается клиенту, не дожидаясь окончания процесса.
  4. Клиент, обработав полученную информацию о запуске процесса, уничтожается.
  5. Процесс по завершении записывает необходимую информацию в объект СостояниеПроцессов и уничтожается.

Таким образом, время отклика на клиентской стороне становится минимальным и практически не превышает время активизации Web-сервиса и чтения им информации из объекта СостояниеПроцессов. Худшая ситуация - это когда Web-сервис пытается читать информацию из объекта СостояниеПроцессов, а объект Процесс писать. В этом случае время отклика увеличится на время блокировок записи/чтения объекта СостояниеПроцессов, что по отношению к общему времени отклика составляет совершенно незначительный прирост.

Рассмотрим описанное решение на примере упрощенного кода.

СостояниеПроцессов

Представляет собой обычный датасет примерно следующей структуры:

Примечание: Структура датасета взята из реальной задачи по загрузке данных из внешних источников, поэтому некоторые поля достаточно специфичны. Значимые поля - id (идентификатор процесса) и Статус (отображает состояние процесса).

Web-сервис

[WebService(Namespace="http://localhost/Fox2SQL/webservices/", 
	Description="Загрузка данных из таблиц FoxPro в MS SQL Server.")]
public class FoxReader : System.Web.Services.WebService
{
  [WebMethod(Description="Отображает состояние загрузки данных из таблиц
                          FoxPro в таблицы MS SQL Server.")]
  public TasksDataset GetTasks()
  {
    return GlobalData;
  }

  [WebMethod(Description="Запуск загрузки данных из таблиц FoxPro в таблицы
                          MS SQL Server.")]
  public bool StartTasks(int[] taskIds)
  {
    // проверка, что задание не выполняется
    TasksDataset tasks = GlobalData;
    bool isEmptyTasks = true;
    lock (tasks)
    {
      for (int i = 0; i < taskIds.Length; i++)
      {
        TasksDataset.TasksRow row = 
          tasks.Tasks.Rows.Find(taskIds[i]) as TasksDataset.TasksRow;
        if (row != null && 
            !row.IsСтатусNull() && row.Статус == (int)StatusCode.Started)
        {
          taskIds[i] = 0;
        }
        else if (taskIds[i] > 0)
          isEmptyTasks = false;
      }
    }

    // если список заданий для загрузки пустой, возврат false
    if (isEmptyTasks)
      return false;

    // запуск задания
    FoxReaderOperation op = new FoxReaderOperation(taskIds);
    Thread t = new Thread(new ThreadStart(op.Execute));
    t.Start();

    // возврат, не дожидаясь окончания процесса загрузки
    return true;
  }

  /// <summary>
  /// Глобальное состояние процессов
  /// </summary>
  static internal TasksDataset GlobalData 
  {
    get
    {
      if (globalData == null)
      {
        globalData = new TasksDataset();
        // загрузить информацию, сохраненную в базе данных
        LoadGlobalData();
      }
      return globalData;
    }
  }

  /// <summary>
  /// Глобальное состояние процессов
  /// </summary>
  static private TasksDataset globalData = null;
}

Метод GetTasks просто возвращает список как выполняемых, так и закончившихся процессов.

В методе StartTasks на время проверки выполняется блокировка объекта глобальных данных, а затем выполняется запуск в отдельной цепочке метода Execute объекта FoxReaderOperation (непосредственное выполнение процесса).

Примечание: Реализация метода LoadGlobalData не приводится.

Процесс

internal class FoxReaderOperation
{
  public FoxReaderOperation(int[] taskIds)
  {
    this.taskIds = taskIds;
  }

  public void Execute()
  {		
    foreach (int taskId in taskIds)
    {
      if (taskId <= 0)
        continue;
				
      TasksDataset tasks;
      TasksDataset.TasksRow row;
      string source, target;

      // запись информации о начале процесса
      tasks = FoxReader.GlobalData;
      lock (tasks)
      {
        row = tasks.Tasks.Rows.Find(taskId) as TasksDataset.TasksRow;
        if (row == null)
          continue;
        else
        {
          source = row.Источник;
          target = row.Получатель;
          if (source == "" || target == "")
            continue;

          row.BeginEdit();
          row.Статус = (byte)StatusCode.Started;
          row.EndEdit();
        }
      }
      tasks = null;
      row = null;

      // процесс
      StatusCode status = LoadFoxTable(source, target);

      // запись информации об окончании процесса
      tasks = FoxReader.GlobalData;
      lock (tasks)
      {
        row = tasks.Tasks.Rows.Find(taskId) as TasksDataset.TasksRow;
        if (row != null)
        {
          row.BeginEdit();
          row.Статус = (byte)status;
          row.EndEdit();
        }
      }
    }
  }

  /// <summary>
  /// Реальная загрузка данных
  /// </summary>
  /// <param name="source">полный путь к исходной таблице FoxPro</param>
  /// <param name="target">полное имя SQL-таблицы - получатель</param>
  /// <returns>код ошибки</returns>
  private StatusCode LoadFoxTable(string source, string target)
  {
    // отладка в домашних условиях - задержка 60 секунд
    Thread.Sleep(60000);

    return StatusCode.NoError;
  }

  private int[] taskIds;
}

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

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

Клиент

public class _Default : System.Web.UI.Page
{
  private void Page_Load(object sender, System.EventArgs e)
  {
    // построение перечня идентификаторов заданий на загрузку			
    foreach (DataGridItem taskRow in TaskDataGrid.Items)
    {
      CheckBox startCheckBox = FindStartCheckBox(taskRow);
      if (startCheckBox != null && startCheckBox.Visible &&
          startCheckBox.Checked)
      {
        int taskId = int.Parse((taskRow.Cells[0].FindControl("IdLabel")
                                as Label).Text);
        taskIdList.Add(taskId);
      }
    }
	
    // загрузка информации о выполняемых процессах	
    tasksDataset1 = reader.GetTasks();

    DataBind();
  }

  private void StartButton_Click(object sender, System.EventArgs e)
  {
    if (taskIdList.Count != 0)
    {
      int[] taskIds = (int[])taskIdList.ToArray(typeof(int));

      // запуск задания на загрузку
      reader.StartTasks(taskIds);
    }
  }

  /// <summary>
  /// Список заданий загрузки
  /// </summary>
  protected Dorogobuzh.Fox2SQL.Reader.localhost.TasksDataset tasksDataset1;

  /// <summary>
  /// Сервис загрузки таблиц FoxPro
  /// </summary>
  protected FoxReader reader = new FoxReader(); 
}

 

Клиентская aspx-страница получается простой - здесь есть всего два вызова Web-сервиса, причем оба выполняются в синхронном режиме, не приводя к задержке отклика.

 

Таким образом, поставленная задача по управлению длительными процессами решена.

Полные исходные тексты реальной задачи загрузки данных FoxPro на SQL-сервер прилагаются.

Критика и самокритика

  1. Представленное решение можно было бы оформить в виде расширяемой библиотеки классов. Узким местом здесь является, конечно, слишком жесткая привязка структуры глобальных данных к предметной области. Естественно, можно воспользоваться шаблонами проектирования (см., например, http://www.dotsite.spb.ru/solutions/patterns), чтобы обойти эту проблему. Но может быть лучше сначала наработать практический опыт использования этой технологии в разных задачах (хотя бы двух-трех), собрать критические замечания и пожелания, а потом уже заниматься обобщениями?
  2. Можно было бы сделать визард для VS.Net, который бы генерировал основную часть типового проекта (неплохое описание - http://www.c-sharpcorner.com/Code/2002/Oct/CustomWizard.asp). Впрочем, такую работу есть смысл делать также только после наработки опыта и соответствующих обобщений.
  3. Иванов Роман (mailto:rsivanov2@mail.ru) предложил использовать для асинхронного запуска процессов MSMQ. В Сети есть немало информации об использовании MSMQ в .Net (например, http://www.gotdotnet.ru/default.aspx?s=doc&d_no=2710&c_no=4 или http://www.c-sharpcorner.com/2/MessageQueueTut1.asp) и автору статьи этот механизм знаком. Однако предложенное решение, хотя и не обеспечивает такой мощной поддержки транзакций, как MSMQ, выглядит проще.

Одним словом, любые замечания и предложения приветствуются.


Может пригодится:


Автор: sivanov@drg.dol.ru
Прочитано: 4534
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру