Постановка завдання, ASP.NET, ASP, статті

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

Управління запуском і контроль за ходом виконання процесів повинні здійснюватися через 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> / / /  повний шлях до вихідної таблиці FoxPro  / / /  повне ім'я SQL-таблиці - одержувач  / / /  Код помилки 
  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. Представлене рішення можна було б оформити у вигляді розширюваної бібліотеки класів. Вузьким місцем тут є, звичайно, занадто жорстка прив’язка структури глобальних даних до предметної області. Природно, можна скористатися шаблонами проектування (див., наприклад,
    www.dotsite.spb.ru/solutions/patterns), Щоб обійти цю проблему. Але може бути краще спочатку напрацювати практичний досвід використання цієї технології в різних завданнях (хоча б двох-трьох), зібрати критичні зауваження і побажання, а потім вже займатися узагальненнями?
  2. Можна було б зробити візард для VS.Net, який би генерував основну частину типового проекту (непогане опис –

    www.c-sharpcorner.com/Code/2002/Oct/CustomWizard.asp). Втім, таку роботу є сенс робити також лише після напрацювання досвіду і відповідних узагальнень.
  3. Іванов Роман (mailto:rsivanov2@mail.ru) запропонував використовувати для асинхронного запуску процесів MSMQ. В Мережі є чимало інформації про використання MSMQ в. Net (наприклад,

    www.gotdotnet.ru/default.aspx?s=doc&d_no=2710&c_no=4 або

    www.c-sharpcorner.com/2/MessageQueueTut1.asp) І автору статті цей механізм знаком. Однак запропоноване рішення, хоча і не забезпечує такої потужної підтримки транзакцій, як MSMQ, виглядає простіше.

Одним словом, будь-які зауваження та пропозиції вітаються.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*