Одновременный запуск нескольких длительно работающих фоновых служб

Я экспериментирую с IHostedService в dotnet core 2.2. Моя задача - создать 2 фоновые длительные задачи.

  • Первый из них - управлять Selenium сеансом браузера (открывать / закрывать вкладки, анализировать DOM) и помещать сообщения электронной почты в очередь внутри ConcurrentBag.
  • Второй фоновый рабочий процесс рассылает уведомления по электронной почте один раз в 10 минут для сообщений, которые существуют в ConcurrentBag (которое добавлено первой задачей). Он также группирует их вместе, так что отправляется только 1 сообщение.

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

Я неправильно использую IHostedService? Если да, то какой архитектурный подход лучше всего подходит для выполнения моей задачи?

Вот код, который я сейчас использую (пытаюсь закончить):

using System;
// ..

namespace WebPageMonitor
{
    class Program
    {
        public static ConcurrentBag<string> Messages = new ConcurrentBag<string>();

        static void Main(string[] args)
        {
            BuildWebHost(args)
                .Run();

            Console.ReadKey();
        }

        private static IHost BuildWebHost(string[] args)
        {
            var hostBuilder = new HostBuilder()
                .ConfigureHostConfiguration(config =>
                {
                    config.AddJsonFile("emailSettings.json", optional: true);
                    config.AddEnvironmentVariables();
                })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddOptions();

                    var bindConfig = new EmailSettings();
                    hostContext.Configuration.GetSection("EmailSettings").Bind(bindConfig);
                    services.AddSingleton<EmailSettings>(bindConfig);

                    services.AddTransient<EmailSender>();

                    services.AddHostedService<BrowserWorkerHostedService>();
                    services.AddHostedService<EmailWorkerHostedService>();
                });

            return hostBuilder.Build();
        }

    }
}

BrowserWorkerHostedService

public class BrowserWorkerHostedService : BackgroundService
{
    private static IWebDriver _driver;

    public BrowserWorkerHostedService()
    {
        InitializeDriver();
    }

    private void InitializeDriver()
    {
        try
        {
            ChromeOptions options = new ChromeOptions();
            options.AddArgument("start-maximized");
            options.AddArgument("--disable-infobars");
            options.AddArgument("no-sandbox");

            _driver = new ChromeDriver(options);
        }
        catch (Exception ex)
        {
            Program.Messages.Add("Exception: " + ex.ToString());

            Console.WriteLine($" Exception:{ex.ToString()}");
            throw ex;
        }
    }

    protected override async Task ExecuteAsync(CancellationToken stopToken)
    {
        while (!stopToken.IsCancellationRequested)
        {
            try
            {
                _driver.Navigate().GoToUrl("https://www.google.com");
                Program.Messages.Add("Successfully opened a website!");
                // rest of the processing here

                Thread.Sleep(60_000);
            }
            catch (Exception ex)
            {
                Program.Messages.Add("Exception: " + ex.ToString());

                Console.WriteLine(ex.ToString());
                Thread.Sleep(120_000);
            }
        }

        _driver?.Quit();
        _driver?.Dispose();
    }
}

EmailWorkerHostedService

public class EmailWorkerHostedService : BackgroundService
{
    private readonly EmailSender _emailSender;
    private readonly IHostingEnvironment _env;

    public EmailWorkerHostedService(
        EmailSender emailSender,
        IHostingEnvironment env)
    {
        _emailSender = emailSender;
        _env = env;
    }

    protected override async Task ExecuteAsync(CancellationToken stopToken)
    {
        while (!stopToken.IsCancellationRequested)
        {
            var builder = new StringBuilder();

            List<string> exceptionMessages = new List<string>();
            string exceptionMessage;
            while (Program.Messages.TryTake(out exceptionMessage))
                exceptionMessages.Add(exceptionMessage);

            if (exceptionMessages.Any())
            {
                foreach (var message in exceptionMessages)
                {
                    builder.AppendLine(new string(message.Take(200).ToArray()));
                    builder.AppendLine();
                }

                string messageToSend = builder.ToString();
                await _emailSender.SendEmailAsync(messageToSend);
            }

            Thread.Sleep(10000);
        }
    }
}

РЕДАКТИРОВАТЬ: после применения изменений, предложенных в ответе, вот текущая версия кода, который работает. Добавление await помогло.


person Alex    schedule 06.07.2019    source источник


Ответы (1)


Во-первых, НИКОГДА НЕ используйте Thread.Sleep() в контексте async, поскольку это блокирующее действие. Вместо этого используйте Task.Delay(). И я считаю, что это твоя проблема. Посмотрите на BackgroundService.StartAsync реализация:

    public virtual Task StartAsync(CancellationToken cancellationToken)
    {
        // Store the task we're executing
        _executingTask = ExecuteAsync(_stoppingCts.Token);

        // If the task is completed then return it, this will bubble cancellation and failure to the caller
        if (_executingTask.IsCompleted)
        {
            return _executingTask;
        }

        // Otherwise it's running
        return Task.CompletedTask;
    }

Когда асинхронный метод вызывается, он выполняется синхронно до первой истинной асинхронной операции. Ваша истинная асинхронная операция

ждать _emailSender.SendEmailAsync (messageToSend);

но он будет вызываться только при выполнении условия

если (exceptionMessages.Any ())

Это означает, что ваш ExecuteAsync метод никогда не вернется, и поэтому StartAsync. Task.Delay также является истинным асинхронным методом (Thread.Sleep - нет), поэтому после его нажатия StartAsync продолжит работу и завершит работу, и у вашей второй службы будет возможность запуститься.

person Artur    schedule 06.07.2019
comment
Большой! Просто обновил свой код и провел пару тестов. Вроде ведет себя так, как надо. Я чувствую, что мне нужно немного углубиться в асинхронную обработку, чтобы лучше понять внутреннюю логику. Спасибо за помощь @Artur! - person Alex; 06.07.2019
comment
Я бы также посоветовал переопределить StarAsync и вызвать там InitializeDriver, а затем вызвать базовую реализацию. Таким образом вы отделите создание объекта от инициализации, и отладка будет проще. - person Artur; 06.07.2019
comment
не возражаете, если я задам дополнительный вопрос? Каков был бы подход, чтобы изящно завершить все запущенные размещенные процессы одновременно, когда соблюдается определенное условие в почтовой службе? - person Alex; 08.07.2019
comment
Ваш ExecuteAsync получает CancelationToken - этот токен отменяется при остановке службы. Чтобы вызвать остановку службы, вы можете использовать `IApplicationLifetime.StopApplication () '. Но имейте в виду, что это остановит всю веб-службу. - person Artur; 08.07.2019
comment
в моем сценарии это предпочтительный исход :) Спасибо! Я пробовал использовать Environment.Exit(0), но приложение зависло .. Возможно, из-за его вызова из неосновного потока .. С IApplicationLifetime.StopApplication() приложение завершает работу, как ожидалось. - person Alex; 08.07.2019
comment
у вас есть логическое объяснение, почему Task.Delay() может привести к установке lifetime.ApplicationStarted.IsCancellationRequested на true? То есть даже без StopApplication() вызова метода. Я помещу текущий код в описание. В примере - приложение будет прервано после первого Task.Delay() вызова. - person Alex; 09.07.2019
comment
Пожалуйста, откройте новый вопрос и разместите ссылку здесь в комментарии. - person Artur; 09.07.2019
comment
done stackoverflow.com/questions/ 56955587 / - person Alex; 09.07.2019