Постановка задачи
Пользователи должны иметь возможность запуска длительных серверных
процессов (загрузка объемных данных из внешних источников, архивирование
и пр.). Время выполнения процессов может достигать десятков минут.
Процессы могут по желанию пользователя объединяться в цепочки, в которых
отдельные процессы выполняются последовательно. Необходимо предусмотреть
блокировку от одновременного запуска одного и того же процесса разными
пользователями.
Управление запуском и контроль за ходом выполнения процессов должны
осуществляться через Web-интерфейс. При этом отключения от сервера,
например, выключение браузера или переход на произвольный адрес, не
должны прерывать выполняемые процессы, а при повторном подключении к
странице управления должно показываться текущее состояние процессов.
Подгружаемые на клиент модули желательно не использовать.
Непосредственное управление процессами должно быть реализовано в виде
Web-сервиса, т.к. в перспективе возможно построение альтернативных
клиентских интерфейсов (в т.ч. и Windows-программ) или встраивание
описанной функциональности в другие прикладные программы.
Примечание: система безопасности в целях упрощения не
рассматривается.
Решение задачи
Известно, что клиентский прокси-класс, генерируемый для обращения к
Web-сервису, содержит как синхронный вариант вызова методов сервиса, так
и асинхронный. Если, например, Web-сервис имеет метод GetData, то в
прокси будет сгенерирован соответствующий синхронный метод GetData и
пара методов для асинхронного вызова - BeginGetData и EndGetData.
Очевидно, что синхронный вариант для запуска длительных серверных
процессов не подходит, т.к. время реакции системы, исчисляемое десятками
минут, просто неприемлемо.
Посмотрим, помогут ли решению нашей задачи возможности клиентского
прокси-класса по асинхронному вызову методов Web-сервиса.
Предлагаемая разработчиками Microsoft схема асинхронного
использования Web-методов может быть представлена в виде следующей
диаграммы:
Если учесть, что клиентом для Web-сервиса в нашей задаче выступает
aspx-страница (еще точнее - ее серверный код), то становится понятным,
что время отклика опять окажется непозволительно большим.
Разработаем такую архитектуру системы, которая позволила бы запускать
длительные процессы и не дожидаться их завершения, а контролировать ход
выполнения будем по мере необходимости из любого, в том числе и из вновь
созданного клиентского объекта:
Здесь СостояниеПроцессов - некоторый глобальный (в контексте
Web-сервиса) объект, который хранит информацию о выполнении процессов, а
Процесс - объект, непосредственно выполняющий назначенный процесс.
Причем со стороны Web-сервиса объект СостояниеПроцессов доступен только
по чтению, а Процесс имеет возможность записывать в него информацию о
своем выполнении.
В результате работа всей системы распадается на два относительно
независимых сценария.
Сценарий чтения клиентом информации о выполняемых процессах:
- Создается клиентский объект Клиент.
- Клиент обращается к Web-сервису для чтения информации о
выполняемых процессах.
- Web-сервис опрашивает объект СостояниеПроцессов и возвращает
информацию Клиенту.
- Объект Клиента выполняет необходимую обработку полученной
информации и уничтожается.
Сценарий запуска процессов:
- Создается клиентский объект Клиент.
- Клиент передает Web-сервису команду на запуск процесса.
- Web-сервис опрашивает объект СостояниеПроцессов и, если
назначенный процесс не находится в активном состоянии, создает
объект Процесс и запускает его. Управление возвращается клиенту, не
дожидаясь окончания процесса.
- Клиент, обработав полученную информацию о запуске процесса,
уничтожается.
- Процесс по завершении записывает необходимую информацию в объект
СостояниеПроцессов и уничтожается.
Таким образом, время отклика на клиентской стороне становится
минимальным и практически не превышает время активизации 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-сервер
прилагаются.
Критика и самокритика
- Представленное решение можно было бы оформить в виде расширяемой
библиотеки классов. Узким местом здесь является, конечно, слишком
жесткая привязка структуры глобальных данных к предметной области.
Естественно, можно воспользоваться шаблонами проектирования (см.,
например,
http://www.dotsite.spb.ru/solutions/patterns), чтобы обойти эту
проблему. Но может быть лучше сначала наработать практический опыт
использования этой технологии в разных задачах (хотя бы двух-трех),
собрать критические замечания и пожелания, а потом уже заниматься
обобщениями?
- Можно было бы сделать визард для VS.Net, который бы генерировал
основную часть типового проекта (неплохое описание -
http://www.c-sharpcorner.com/Code/2002/Oct/CustomWizard.asp).
Впрочем, такую работу есть смысл делать также только после наработки
опыта и соответствующих обобщений.
- Иванов Роман (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, выглядит
проще.
Одним словом, любые замечания и предложения приветствуются.