#c# #asp.net #asp.net-core #asp.net-core-webapi #fire-and-forget
#c# #asp.net #asp.net-core #asp.net-core-webapi #запустить и забыть
Вопрос:
Я пишу один Webapi, в котором я должен вызывать 2-ю конечную точку HTTP, когда первая конечная точка HTTP завершается сбоем. Однако для второго HTTP-вызова мое приложение не должно блокировать основной поток и должно возвращать неудачный результат. Мой HTTP-клиент уже оснащен политиками повторных попыток Polly, и после повторных попыток я хочу вызвать 2-й URL-адрес молча, не блокируя основной поток. Мой код выглядит следующим образом.
public class Request
{
public string Id { get; set; }
}
public class Result
{
public string Id { get; set; }
public bool IsSuccess { get; set; }
public string Message { get; set; }
}
public class Service
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly Ilogger _appLogger;
private readonly string _url1="www.somedomain.com/api1";
private readonly string _url2="www.somedomain.com/api2";
Service(IHttpClientFactory factory,Ilogger logger)
{
_httpClientFactory = httpClientFactory;
_appLogger = logger;
}
public async Task<Result> Send(Request request)
{
try
{
using var client = _httpClientFactory.CreateClient();
using HttpRequestMessage httpRequest =new HttpRequestMessage(HttpMethod.Post, _url1);
var req = JsonConvert.SerializeObject<Request>(body);
var stringContent = new StringContent(req, Encoding.UTF8, "json");
var result = await client.SendAsync(httpRequest);
var resultContent = await result.Content.ReadAsStringAsync();
return JsonConvert.DeserializeObject<Result>(resultContent));
}
catch (Exception ex)
{
if (ex is OperationCanceledException || ex is TaskCanceledException)
{
//Second call needed but don't want to hold the result
// Task.Run(async ()=> await Log(request)) //Approach 1
// Task.Factory.StartNew(async () => await Log(request)) //Approach 2
// HostingEnvironment.QueueBackgroundWorkItem(ct => Log(request).ConfigureAwait(false)); //Approach 3
// QueuedHostedService defined here [Microsoft][1]
// var jobId = BackgroundJob.Enqueue(() => Log(request).ConfigureAwait(false)); //Approach 4 by hangfire [Hangfire][2]
// Custome singleton suggested here // [CustomImplementation][1]
}
//No 2nd call needed
await _appLogger.LogAsync(new ExceptionLog(ex));
return Result{IsSuccess=false;};
}
}
public async Task Log(Request request)
{
try
{
using var client = _httpClientFactory.CreateClient();
using var httpRequest =new HttpRequestMessage(HttpMethod.Post, _url2);
var req = JsonConvert.SerializeObject<Request>(body);
var stringContent = new StringContent(req, Encoding.UTF8, "json");
var result = await client.SendAsync(httpRequest);
var resultContent = await result.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<Result>(resultContent));
await _appLogger.LogAsync("2nd call successful.",result );
}
catch (Exception ex)
{
await _appLogger.LogAsync("2nd call failed",ex);
}
}
}
Я искал много решений, но теперь путаюсь между ними.
[1]: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-6.0amp;tabs=visual-studio
[2]: https://www.hangfire.io /
[3]: https://anduin.aiursoft.com/post/2020/10/14/fire-and-forget-in-aspnet-core-with-dependency-alive
Ответ №1:
На самом деле, то, что вы ищете, — это fire and forget
механизм, который должен работать независимо с текущей областью.
Мы все знаем, что, как только HttpRequest
завершится их ответ, текущая область, включая все службы, которые мы используем, будет удалена (поведение на уровне области по умолчанию). Итак, давайте представим, что 2-й вызов вернулся позже, после завершения исходного http-запроса, все уже было бы удалено и очищено. Неприятная ошибка…
Взгляните на это и обработайте его в своем блоке catch, и все должно быть хорошо.
Другой подход заключается в использовании фоновой службы, идея заключается в создании очереди, содержащей необходимую информацию, позволяющую выполнять фоновое сканирование и обрабатывать их, скажем, для любого желаемого интервала.
Комментарии:
1. Да, вы правы, однако мне может понадобиться некоторая ограниченная информация, например, logger, для регистрации результата. Для меня это тоже выглядит идеальным кандидатом, но я не уверен в фоновом обслуживании (будет ли это чрезмерным для простой проблемы).
2. Для ведения журнала мы должны что-то записать, прежде чем вызывать 2-й запрос, и передать какую-то уникальную вещь
fire and forget
механизму, напримерCorrelationId
, или что-то еще, что нам нужно для поиска в журнале. Невозможно сохранить весь контекст ведения журнала с помощью механизма забывания, потому что он тесно связан с текущей областью.3. И для
background service
, это вообще не перестройка. Мы можем полностью создать свой собственный, используя менее 100 строк кода для всегоbackground service
и еще несколько строк для очереди информации, где нам нужно их обработать. На самом деле, менее 100 строк кода, похоже, не является инженерной проблемой … верно? — Одна вещь, которую я абсолютно одобряю,Hangfire
должна быть излишней для этого.
Ответ №2:
Я постараюсь представить разбивку ваших возможных подходов:
— Вызов «Запустить и забыть»: он работает, его проще всего реализовать, но он не дает никаких гарантий или видимости того, что код выполнен. Любое исключение внутри метода fire и forget будет проглочено. Если ваше приложение останавливается во время выполнения метода fire и forget, вы не сможете получить повторную попытку. Используйте этот подход, если вызов не является обязательным.
— Hangfire: он делает то, что вы хотите, у него есть несколько опций, таких как определение политик повторных попыток и настройка количества параллельных выполнений, которые вы можете иметь. Он поддерживает несколько различных хранилищ, в том числе в памяти, SQL и NoSQL. Лично я большой поклонник HangFire и считаю, что это самый надежный подход, который прост в реализации. Однако это добавляет зависимость, которую вам, возможно, придется обсудить с остальной частью вашей команды.
— Размещенная служба: я не делаю то, что вы хотите из коробки, поскольку размещенная служба больше похожа на приложение в приложении. Вы можете использовать его для создания пользовательского постоянного выполнения задания, но вам придется в значительной степени заново изобретать подмножество функций Hangfire. Это, безусловно, самый сложный подход, и я бы избегал его, если это возможно.
Комментарии:
1. Да, что касается
HostedService
того, что я нахожу это немного сложным. Что касаетсяHangfire
, я проверил и обнаружил, что доступно одно хранилище в памяти, но упомянул, что не использую его здесь для производства, и я не могу использовать sql server или любой другой серверный сервер.2. @MaheshJadhav использование в хранилище памяти означает, что вы потеряете задания, поставленные в очередь, если ваше приложение завершит работу. Это верно при любом подходе к памяти, который вы используете. Как вы собираетесь продолжать выполнение, если вы не можете использовать хранилище?
3. Хотелось бы, чтобы hangfire предоставлял реализацию IHostedService for.net веб-API, чтобы очистка могла быть эффективно обработана при завершении работы приложения. В моем случае я запускаю задание только в том случае, если получаю исключение, и в любом приложении это происходит очень часто, но не каждый раз.
4. @MaheshJadhav вы можете указать своему приложению ждать зависания при завершении работы. Но это работает только в том случае, если ваше приложение завершает работу чисто. Если ваше приложение неожиданно завершит работу, выполнение кода очистки не гарантируется, даже с помощью IHostedService. Вы согласны с тем, что 2-й вызов не выполняется?
5. Я не думаю, что использование какой-либо зависимой службы, такой как SQL или другое хранилище, разрешено из-за ограничений и соответствия требованиям. Также на этом этапе рекомендовать это и получать одобрение — утомительная задача. Я согласен, что Hangfire сделает это очень простым и гибким. Иногда вам приходится довольствоваться тем, какие варианты у вас остались.