Мне понадобилось запустить фоновый процесс в ASP.NET. Возник вопрос: как лучше это сделать? Немного погуглив в блоге SCOTT HANSELMAN, я нашел запись «How to run Background Tasks in ASP.NET». Статья не очень новая – 2014 года, но вполне актуальная, поэтому я решил перевести ее на русский язык.

Несколько лет назад Phil Haack написал великолепную статью об опасностях выполнения фоновых задач в ASP.NET. Он выделил три основных риска, связанных с запуском фонового процесса:

  1. Необработанное исключение в потоке, несвязанном с запросом, может снять процесс.
  2. Если запустить сайт в веб-ферме, то есть вероятность случайно завершить несколько экземпляров приложения, которое могло запустить несколько экземпляров одной и той же задачи одновременно.
  3. Домен приложения, в котором запущен сайт, по разным причинам может выгрузиться и снять фоновую задачу, запущенную в нем.

Конечно, можно написать собственный менеджер для управления фоновыми задачами. Но, вероятнее всего, Вы сделаете это неправильно. Никто не собирается оспаривать Ваши навыки разработчика. Просто создание подобного менеджера — это достаточно тонкая вещь. Да и зачем Вам это надо?

Есть множество отличных вариантов запуска задач в фоновом режиме. И не просто абстрактных методик, а готовых библиотек.

Какие-то ASP.NET приложения могут работать на Ваших собственных серверах под IIS, какие-то размещаться в Azure.

Для запуска фоновых задач можно выделить несколько вариантов:

  • Основной: Hangfire или аналогичная библиотека, которую можно использовать для написания фоновых задач на Вашем собственном ASP.NET сайте.
  • Облачный: Azure WebJobs веб-задания Azure. Собственный сервис в Azure, который можно использовать для запуска фоновых задач вне Вашего веб-сайта с возможностью масштабирования нагрузки.
  • Расширенный: Веб-роль Azure в облачных службах. Сервис, позволяющий запускать фоновые задачи независимо от Вашего сайта, с возможностью масштабирования нагрузки и контролем выполнения.

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

WEBBACKGROUNDER


Как написано на сайте: «WebBackgrounder — это проверка концепции совместимого с веб-фермами менеджера фоновых задач, который должен работать только с веб-приложениями ASP.NET». Его код не менялся уже много лет, но NuGet пакет был скачан более полумиллиона раз.

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

Это явно библиотека не для всех случаев жизни. Но если во время работы ASP.NET приложения необходимо запускать всего одну задачу, то WebBackgrounder — все что Вам нужно.

using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace WebBackgrounder.DemoWeb
{
    public class SampleJob : Job
    {
        public SampleJob(TimeSpan interval, TimeSpan timeout)
            : base("Sample Job", interval, timeout)
        {
        }
 
        public override Task Execute()
        {
            return new Task(() => Thread.Sleep(3000));
        }
    }
}

Добавленный в .NET 4.5.2 QUEUEBACKGROUNDWORKITEM


Можно сказать, что QueueBackgroundWorkItem был добавлен в .NET 4.5.2 в ответ на появление WebBackgrounder. Это не просто реализация «Task.Run». Это нечто большее:

QBWI управляет по расписанию задачами, которые должны запускаться в фоновом режиме, независимо от любого запроса. Разница с обычным элементом ThreadPool заключается в том, что ASP.NET автоматически отслеживает сколько рабочих элементов, зарегистрированных с помощью API, запущено в настоящий момент, а в случае выключения домена приложения среда выполнения ASP.NET пытается отсрочить его, давая возможность завершить работу запущенным фоновым задачам.

Необходимо учитывать, что среда выполнения ASP.NET может отсрочить выключение домена приложения всего на 90 секунд, давая возможность завершить задачи. Поэтому, если Вы не можете завершить задачи за это время, то вам необходимо использовать другие, более надежные средства.

API очень простое — используется «Func<CancellationToken, Task>». Далее небольшой пример, который запускает фоновый процесс из контроллера MVC:

public ActionResult SendEmail([Bind(Include = "Name,Email")] User user)
{
    if (ModelState.IsValid)
    {
       HostingEnvironment.QueueBackgroundWorkItem(ct => SendMailAsync(user.Email));
       return RedirectToAction("Index", "Home");
    }
 
    return View(user);
}

FLUENTSCHEDULER


FluentScheduler — более продвинутый и сложный менеджер задач с fluent интерфейсом. С его помощью вы действительно можете управлять процессом запуска задачи.

using FluentScheduler;
 
public class MyRegistry : Registry
{
    public MyRegistry()
    {
        // Запуск задачи ITask через определенный интервал времени
        Schedule<MyTask>().ToRunNow().AndEvery(2).Seconds();
 
        // Запуск простой задачив определенный момент времени 
        Schedule(() => Console.WriteLine("Timed Task - Will run every day at 9:15pm: " + DateTime.Now)).ToRunEvery(1).Days().At(21, 15);
 
        // Запуск более сложного действия – запуск задачи немедленно и с месячным интервалом
        Schedule(() =>
        {
            Console.WriteLine("Complex Action Task Starts: " + DateTime.Now);
            Thread.Sleep(1000);
            Console.WriteLine("Complex Action Task Ends: " + DateTime.Now);
        }).ToRunNow().AndEvery(1).Months().OnTheFirst(DayOfWeek.Monday).At(3, 0);
    }
}

Также FluentScheduler поддерживает IoC и может легко подключаться с помощью Вашей любимой Dependency Injection библиотеки, просто реализуя его ITaskFactory интерфейс.

Необходимо отметить, что FluentScheduler может работать с .NET Core.

Для обработки исключений, возникающих в фоновых задачах, можно использовать событие JobException объекта JobManager. Событие позволяет получить информацию о возникшем в задаче исключении.

JobManager.JobException += (info) => Log.Fatal("An error just happened with a scheduled job: " + info.Exception);

QUARTZ.NET


Quartz.NET — это версия популярного фреймворка для Java, реализованного в .NET. Фреймворк активно развивается. Для создания фоновой задачи в Quartz используется интерфейс IJob с единственным методом Execute, который и надо реализовать.

using Quartz;
using Quartz.Impl;
using System;
 
namespace ScheduledTaskExample.ScheduledTasks
{
    public class JobScheduler
    {
        public static void Start()
        {
            IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler();
            scheduler.Start();
 
            IJobDetail job = JobBuilder.Create<MyJob>().Build();
 
            ITrigger trigger = TriggerBuilder.Create()
                .WithDailyTimeIntervalSchedule
                  (s =>
                     s.WithIntervalInHours(24)
                    .OnEveryDay()
                    .StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(0, 0))
                  )
                .Build();
 
            scheduler.ScheduleJob(job, trigger);
        }
    }
}

Для запуска менеджера внутри Application_Start необходимо вызвать JobScheduler.Start(). Для начала работы можно прочитать Действия по расписанию и Quartz.NET (перевод Scheduled Tasks In ASP.NET With Quartz.Net).

Проект имеет довольно приличную документацию, которую стоит прочитать перед началом использования (как минимум, стоит просмотреть Tutorials for Developing with Quartz).

HANGFIRE


И последний в обзоре, но, безусловно, непоследний по функциональности, наиболее продвинутый из всех Hangfire. Это действительно очень продвинутый фреймворк для фоновых задач в ASP.NET. Опционально он может использовать Redis, SQL Server, SQL Azure, MSMQ или RabbitMQ для повышения надежности выполнения задач.

Документация Hangfire действительно превосходна. Хотелось бы, чтобы каждый проект с открытым исходным кодом имел такую документацию.

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

Hangfire позволяет легко определить задачи типа «запустить и забыть», информация о которых будет храниться в базе данных:

BackgroundJob.Enqueue(() => Console.WriteLine("Fire-and-forget"));

Можно отсрочить выполнение задачи:

BackgroundJob.Schedule(() => Console.WriteLine("Delayed"), TimeSpan.FromDays(1));

Или запустить задачу в CRON стиле

RecurringJob.AddOrUpdate(() => Console.Write("Recurring"), Cron.Daily);

Работать с Hangfire очень удобно. Hangfire имеет хорошую документацию и обучающие руководства, основанные на реальных примерах.

Hangfire — это целая экосистема для работы с фоновыми задачами в ASP.NET приложениях.

Библиотеки доступны в виде открытых исходных кодов или Nuget пакетов.

Итоги (лично от себя)


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

Я уже знаю, что мне нужно запускать больше одного процесса, и работать процессы могут долго (ограничение в 90 секунд на завершение в QueueBackgroundWorkItem). FluentScheduler выглядит неплохо, но хотелось большего. Hangfire – отличное решение, но, вроде, сразу требует использования базы данных для хранения очереди задач. Да и не совсем там все бесплатно – есть и платная версия.

В итоге я пока выбрал Quartz.NET: вполне приличная документация, достаточное количество примеров, чтобы быстро начать использовать, а также расширяемый функционал, который можно добавлять по мере возрастания потребностей.

Если вы знаете другие библиотеки для запуска фоновых задач или имеете опыт решения подобных задач – делитесь в комментариях.
Поделиться с друзьями
-->

Комментарии (23)


  1. Gradarius
    09.02.2017 18:01

    Hangfire плох тем, что минимальный интервал минута там, в остальном больше всех понравился. В итоге пользуюсь Quartz.NET, но панель нужно ставить отдельно(CrystalQuartz ), да и она просто отвратительна. К тому же стоит учесть, что с CrystalQuartz нужен первый толчок, так как запускаются все задачи уже иначе.


    1. AlexOleynik
      10.02.2017 11:35

      Да. Панель CrystalQuartz не внушает восхищения. Но это лучше чем ничего. Чтоб добавить панель я установил Nuget пакет для OWIN. Добавил несколько строк в Startup.cs

            public void Configuration(IAppBuilder app)
              {
      ///....
                  ISchedulerFactory schedulerFactory = new StdSchedulerFactory();
                  IScheduler scheduler = schedulerFactory.GetScheduler();
      
                  app.UseCrystalQuartz(scheduler);
              }   
      


      И все заработало — по ссылке /quartz открылась панель, которая показывает запущенные задачи.


    1. emdroof
      10.02.2017 15:37

      с hangfire имел только положительный опыт, в quartz однажды использовал неверное cron выражение, которое записалось в базу и потом джоб начинал одновременно выполняться сразу в нескольких потоках, что приводило к их созданию — непонятно, но результат — неверная работа программы и долгое выяснение причин


  1. Gradarius
    09.02.2017 18:16

    Да, кстати сравнительная таблица, пусть даже добавленная в перевод от себя, смотрелась бы отлично.


  1. OnYourLips
    09.02.2017 19:46
    -1

    А почему нельзя использовать обыкновенные очереди и воркеры?
    Зачем нужно как-то извращаться?


    1. chumakov-ilya
      09.02.2017 20:23
      +1

      Масштабируемость, отказоустойчивость, персистентность?


  1. Nikita_Danilov
    09.02.2017 21:45

    Недавно тоже озадачивался темой запуска фоновых процесс, в контексте организации очереди обработки.
    Возможно я не понимаю как готовить Hangfire или Quartz, но ни один из них не позволяет нормальным образом организовать Circuit Breaker на случай аварий.
    Hangfire не позволяет практически конфигурировать интервалы между попытками.
    Есть Polly, который всё это умеет делать замечательно, но туда никак не вкрутишь адекватно персистентность, он не про это.
    Hangfire не позволяет переопределять названия таблиц в БД, так что использовать придется отдельные БД на каждое приложение, либо разделять их по схеме (dbo и т.д.).
    Quartz творит с БД какое-то непотребство, гора таблиц в 2000-like стиле.
    В итоге — придется писать видимо свой инструмент.


  1. chumakov-ilya
    09.02.2017 21:46
    +1

    Hangfire – отличное решение, но, вроде, сразу требует использования базы данных для хранения очереди задач. Да и не совсем там все бесплатно – есть и платная версия.

    Вроде? Вы их не запускали, что ли, когда сравнивали?


    Чтобы внести ясность: да, Hangfire-у для работы необходима БД на SQL Server. В этой базе хранится очередь, через нее же синхронизируются процессы, выполняющие задачи из очереди. Приятный бонус такого подхода — "из коробки" работает кластеризация, т.е. распараллеливание процесссов-обработчиков. В платной версии хранилищем задач может выступать Redis, и есть ряд синтаксических вкусностей (прекрасно обходились и без них). Код бесплатной версии открыт, и репозиторий уже становится популярнее Квартца, хотя Hangfire на много лет моложе.


    1. AlexOleynik
      10.02.2017 00:13

      В первую очередь я искал то, что поможет мне решить мою конкретную задачу. И сделал перевод статьи. Поэтому не совсем корректно говорить, что я сам делал сравнение. Конечно, я сам попробовал запустить все варианты. Посмотрел исходники, чтоб оценить, что конкретно мне будет полезно. Честно говоря, до этого с Redis не сталкивался, хотя видел его поддержку, например, в Azure.
      Судя по документации

      SQL Server and Redis support
      Hangfire uses persistent storage to store jobs, queues and statistics and let them survive application restarts. The storage subsystem is abstracted enough to support both classic SQL Server and fast Redis.

      SQL Server provides simplified installation together with usual maintenance plans.
      Redis provides awesome speed, especially comparing to SQL Server, but requires additional knowledge.

      поддержка Redis есть в бесплатной версии.


      1. chumakov-ilya
        10.02.2017 14:30

        Судя по http://hangfire.io/pro/#hangfireproredis и состоянию пакета Hangfire.Redis — всё таки платно. Был бы рад ошибиться.


        1. Nikita_Danilov
          10.02.2017 14:42

          Их реализация — платно.
          Есть порт на базе StackExchange.Redis https://github.com/BoltR/Hangfire.Redis.StackExchange, который можно прикрутить к бесплатной версии.


    1. raptor
      10.02.2017 02:03

      Нет не необходима — он спокойно может все хранить в памяти. Конечно персистентность страдает, но для задача, которым это не нужно вполне подходит.


      1. chumakov-ilya
        10.02.2017 14:26

        Ваша правда. Забыл, что к Hangfire все-таки можно прикрутить in-memory storage.


        1. AlexOleynik
          10.02.2017 14:59

          Подскажите, пожалуйста, по поводу in-memory storage.
          Я нашел Hangfire.MemoryStorage.
          Но в описании сказано, что использовать можно только для тестирования. А дальше: "… не может использоваться в production ..."

          It can be useful for testing purpose like check the behaviour and use it in a development environment. Please note that it should not be used in production (no integrity and no thread safe even if many cases are managed).


          1. chumakov-ilya
            10.02.2017 15:10

            Этот пакет я и имел ввиду. Возможно, raptor знает что-то еще?
            Да и в соседней ветке комментариев подсказывают про Hangfire.Redis.StackExchange.


            1. raptor
              11.02.2017 02:22

              Я это хранилище (Hangfire.MemoryStorage) использую для задачи периодического чтения очереди. Персистентость для этой задачи не важна, а какое-то хранилище быть все равно должно.


  1. Szer
    09.02.2017 23:53
    +1

    Я понимаю что это может быть оверкил, но я бы использовал Akka.Net.
    Кинул актору задачу и не переживаешь что он завалится. CircuitBreaker, BackoffStrategy, Supervising из коробки.
    Ну и до кучи персистентность, ремоутинг, кластеринг, и тут Остапа понесло…


    1. Nikita_Danilov
      10.02.2017 10:43

      Инструмент похоже и правда очень мощный, хотя очень забавно смотрится куча вставок в документации http://getakka.net/docs/Fault%20tolerance "!!!TODO: Port sample code" :)


      1. Szer
        10.02.2017 10:48

        Akka.Net — порт одноимённого фреймворка Akka из Scala-мира. Если я правильно понимаю портированием и коммерческой поддержкой занимается Petabridge. Поэтому функционал есть, а документация местами отсутствует. Но отсутствующую документацию по Akka.Net всегда можно найти в документации для Scala http://akka.io/docs/


      1. Szer
        10.02.2017 10:54

        Не ту ссылку дал. По Fault Tolerance документация здесь
        Немного привыкнув к Scala код можно передирать почти 1:1, т.к. фреймворк полностью портирован.


        1. Nikita_Danilov
          10.02.2017 14:06

          Подскажете, пожалуйста, еще ссылку на прозрачное описание реализации персистентности заданий в Akka.Net? Пока насколько разобрался это http://getakka.net/docs/persistence/persistent-actors, но из описания следует как будто надо самому заботиться хранением данных по актору.


          1. Szer
            10.02.2017 16:25

            Прозрачное описание персистентности в Akka лежит в понимании EventSourcing.
            Нужно сделать актора, который реагирует на команды. Изменение состояния происходит через генерацию и сохранении событий в хранилище. При восстановлении состояния читается поток событий и проигрывается заново реакция на них.

            Персистентность руками делать не надо, просто подключаем плагин из готовых и работаем.
            Если надо по-быстрому, то есть плагин для embedded SqLite.


  1. just_dmitry
    11.02.2017 20:08

    Когда я "последний раз" искал что-то для фоновых заданий — брать большие комплексы типа Hangfire не хотелось из-за требования БД, а изощренные (извращенные) расписания для запуска типа "каждый понедельник в 8 утра, во вторник каждые 10 минут" с одной стороны были слишком заумными и нереальными, с другой стороны сразу же возникают опасения "что если при 10 минутном запуске задача будет выполнять 12 минут — запустится ли вторая не дожидаясь конца первой".


    В итоге сделал свой велосипед RecurrentTasks — обычный Task просыпается, делает что надо и снова засыпает на заданный интервал. При этом для "сложных" задач, которые должны работать лишь "трижды в день" — все равно где-то в бизнес-требованиях есть "дата последнего запуска" той или иной функции — в итоге удобно оказалось сделать что таск стартует раз в 20-30 минут, считывает дату из реальной базы и "делает дело" только если надо. Заодно этим решается проблема "если приложение было выключено в заданное время" — при следующем запуске все отработает. Периодом можно управлять прямо внутри основного метод: если очередь пустая то следующий раз читать ее "через минуту", если еще что-то есть — "через 10 секунд".