Безопасный способ реализовать метод «Fire and Forget» в ASP.NET Core - PullRequest
0 голосов
/ 08 июня 2018

Я пытаюсь реализовать простую библиотеку журналов, которая будет использоваться в нескольких проектах.Задача библиотеки - отправлять HTTP-запросы в ElasticSearch.Суть этой библиотеки в том, что она не должна ждать ответа.Кроме того, меня не волнуют какие-либо ошибки / исключения.Он должен отправить запрос в ElasticSearch и немедленно вернуться.Я не хочу создавать интерфейсы с типом возврата Task, я хочу, чтобы они оставались void.

Ниже приведен мой пример кода.Это правильная и безопасная реализация «Огонь и забудь»?Это нормально, если я использую Task.Run() в библиотеке высокой нагрузки?Или я должен избегать использования Task.Run() в моем случае?Кроме того, если я не использую await с Task.Run(), заблокирую ли я поток?Этот код находится в библиотеке:

public enum LogLevel
{
    Trace = 1,
    Debug = 2,
    Info = 3,
    Warn = 4,
    Error = 5,
    Fatal = 6
}

public interface ILogger
{
    void Info(string action, string message);
}

public class Logger : ILogger
{
    private static readonly HttpClient _httpClient = new HttpClient(new HttpClientHandler { Proxy = null, UseProxy = false });
    private static IConfigurationRoot _configuration;

    public Logger(IConfigurationRoot configuration)
    {
        _configuration = configuration;
    }

    public void Info(string action, string message)
    {
        Task.Run(() => Post(action, message, LogLevel.Info));
        /*Post(action, message, LogLevel.Info);*/ // Or should I just use it like this?
    }

    private async Task Post(string action, string message, LogLevel logLevel)
    {
        // Here I have some logic

        var jsonData = JsonConvert.SerializeObject(log);
        var content = new StringContent(jsonData, Encoding.UTF8, "application/json");

        var response = await _httpClient.PostAsync(_configuration.GetValue<string>("ElasticLogger:Url"), content);
        // No work here, the end of the method
    }
}

Вот так я регистрирую регистратор внутри метода ConfigureServices в классе запуска моего веб-API:

public void ConfigureServices(IServiceCollection services)
{
     // ......

     services.AddSingleton<ILogger, Logger>();

     // .....
}

Этот код находится в методе внутри моеговеб-интерфейс:

public void ExecuteOperation(ExecOperationRequest request)
{
    // Here some business logic

    _logger.Info("ExecuteOperation", "START"); // Log

   // Here also some business logic

    _logger.Info("ExecuteOperation", "END"); // Log
}

1 Ответ

0 голосов
/ 08 июня 2018

Re: Неожиданный вызов асинхронного метода против Task.Run ()

Так как в Post имеется лишь небольшая часть работы с процессором, то есть создание полезной нагрузки json,нет никакой выгоды от другого Task.Run - накладные расходы по планированию новой задачи в пуле потоков перевесят любую выгоду IMO.то есть

Post(action, message, LogLevel.Info);*/ // Or should I just use it like this?

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

Но в соответствии с окончательным ответом Стивена Клири, запустить и забыть в ASP.Net почти никогда не бывает хорошей идеей .Предпочтительнее было бы перенести работу, например, через очередь, в службу Windows, веб-задание Azure и др.

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

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

Re: Кроме того, если я не использую await с Task.Run (), я заблокирую поток?

await не требует потока .await является синтаксическим сахаром, чтобы попросить компилятор переписать ваш код асинхронно.Task.Run() запланирует вторую задачу в ThreadPool, которая выполнит лишь небольшое количество работы, прежде чем достигнет метода PostAsync, поэтому рекомендация не использовать его.

КоличествоИспользование / блокировка потока вызывающего абонента при незапланированном вызове от Info до Post зависит от того, какая работа выполняется перед возвратом Task.В вашем случае работа по сериализации Json будет выполняться в потоке вызывающего (я пометил # 1), однако время выполнения должно быть незначительным по сравнению с продолжительностью HTTP-вызова.Поэтому, хотя метод Info и не ожидается, любой код после HTTP-вызова все равно необходимо запланировать после завершения вызова Http и запланировать в любом доступном потоке (# 2).

public void Info(string action, string message)
{
#pragma warning disable 4014 // Deliberate fire and forget
    Post(action, message, LogLevel.Info); // Unawaited Task, thread #1
#pragma warning restore 4014
}

private async Task Post(string action, string message, LogLevel logLevel)
{
    var jsonData = JsonConvert.SerializeObject(log); // #1
    var content = new StringContent(jsonData, Encoding.UTF8, "application/json"); // #1

    var response = await httpClient.PostAsync(...), content);

    // Work here will be scheduled on any available Thread, after PostAsync completes #2
}

Re: Обработка исключений

try..catch блоки работают с асинхронным кодом - await будет проверять наличие сбоя Task и выдавать исключение:

 public async Task Post()
 {
     try
     {
         // ... other serialization code here ...
         await HttpPostAsync();
     }
     catch (Exception ex)
     {
         // Do you have a logger of last resort?
         Trace.WriteLine(ex.Message);
     }
 }

Хотя вышеперечисленное будет соответствовать критериям для наблюдения исключения, все же будет хорошей идеей зарегистрировать обработчик UnobservedTaskException на глобальном уровне.

Это поможет вам обнаружить и определить, где вы не смогли наблюдать исключение:

TaskScheduler.UnobservedTaskException += (sender, eventArgs) =>
{
    eventArgs.SetObserved();
    ((AggregateException)eventArgs.Exception).Handle(ex =>
    {
        // Arriving here is BAD - means we've forgotten an exception handler around await
        // Or haven't checked for `.IsFaulted` on `.ContinueWith`
        Trace.WriteLine($"Unobserved Exception {ex.Message}");
        return true;
    });
};

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

...