Комментарии 29
В итоге сделал элементарную обёртку:
public static class RunSyncUtil
{
private static readonly TaskFactory factory = new
TaskFactory(default,
TaskCreationOptions.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
public static void RunSync(Func<Task> task)
=> factory.StartNew(task).Unwrap().GetAwaiter().GetResult();
}
И в нескольких местах проекта, где раньше вызывался GetAwaiter().GetResult() заменил на такой вызов. Таким оразом мы явным образом берём поток из ThreadPool и уже на нём выполняем блокировку GetResult
Делать это пришлось не только из-за эстетических соображений, но ещё потому что у нас зависали интеграционные тесты. Дело в том, что сам по себе asp.net core не устанавливает никакой специальный SynchronizationContext, поэтому в нём можно без последствий вызывать GetAwaiter().GetResult(). Но XUnit применяет свой хитрый контекст для контроля параллельного выполнения теста и тут мы легко приходим к дедлоку.
Кстати, есть баг, который описывает потенциальное решение проблемы github.com/dotnet/aspnetcore/issues/5897 но пока его не хотят исправлять и это печально
new JwtBearerPostConfigureOptions().PostConfigure(string.Empty, jwtBearerOptions);
try
{
var config = jwtBearerOptions.ConfigurationManager
.GetConfigurationAsync(new CancellationTokenSource(options?.AuthorityTimeoutInMs ?? 5000).Token)
.GetAwaiter().GetResult();
var providerSigningKeys = config.SigningKeys;
signingKeys.AddRange(providerSigningKeys);
}
На сколько я знаю стандартная реализация по умолчанию кэширует конфигурацию, получая конфигурацию через ConfigurationManager при запросе (в асинхронном стеке) JwtBearerPostConfigureOptions
JwtBearerHandler
ConfigurationManager
Вся суть в том, что JwtBearerHandler региструется транзиентно, поэтому вот это условие при каждом вызове возвращает true, и конфигурация читается заново.
public async Task<T> GetConfigurationAsync(CancellationToken cancel)
{
DateTimeOffset now = DateTimeOffset.UtcNow;
if (_currentConfiguration != null && _syncAfter > now)
{
return _currentConfiguration;
}
Что, надеюсь, не отменяет полезности оставшейся части статьи)
Возможность писать await t вместо t.GetAwaiter().GetResult(), на мой взгляд, имеет малую практическую полезность (ну, кроме сокращения числа нажатий на клавиатуре при написании), потому как семантически это почти (если не учитывать вариант IsCompleted==true) одно и тоже. Видимо именно поэтому Microsoft такая проблема и не напрягает.
Единственная польза, которую я тут смог усмотреть — это борьба с криво написанными/настроенными маршрутизаторами запросов пользователей к серверу (они же, по выполняемой функции — балансировщики нагрузки), которые не способны контролировать доступность экземпляров сервиса и не направлять запросы к недоступным экземплярам, приводящие к большим задержкам ответа клиентам об ошибке. Но почему-то мне кажется, что эту задачу всё равно надо решать как-то по-другому — например, правильным выбором или настройкой балансировщика, — а не перекладывать её на клиента.
Я не прав?
Про балансировщики — признаюсь, я не очень глубоко копал этот вопрос. Но выглядит так, как будто помимо настройки самого балансировщика необходимо будет настроить и сервис, чтобы весь механизм healthcheck/readiness/startup заработал.
Еще юзкейс: метрики приложения отдаются в формате prometheus по http. Если рано запустить вебхост, можно уже начать отдавать метрики и даже отслеживать, сколько времени требуется на старте, и возникают ли какие-то проблемы. Если, конечно, приложение на старте делает что-то тяжелое.
Например: прогрев кэшей, подтягивание каких-то данных, которые нужны для работы, но не хранятся у себя (конфигурация), в экзотических случаях бывает даже кодогенерация на основе рантайм данных, чтоьы потом быстрее что-то работалось...
Это же Startup, он выполняется один раз при старте. Почему не использовать старый добрый Task.Run с асинхронной лямбдой внутри, который выполнит асинхронный метод в отдельном background-потоке, а мы его просто подождём? Это и в msdn описывалось как универсальный подход для запуска асинхронного метода в синхронном, и я не припомню случая, чтобы это вызывало проблемы.
Шаблон:
var value = Task.Run(async () => await GetValueAsync()).Result;
Или
Task.Run(async () => await SomeInitializationAsync()).Wait();
Мне вот лично не нравится вызывать асинхронный код в синхронном, если этого можно не делать. Какой-то принципиальной выгоды этот подход не даёт.
LoadAsync().GetAwaiter().GetResult();
и
LoadAsync().Result
?
Правильно вызывать асинхронный код исключительно через новый поток как написано выше:
var value = Task.Run(async () => await GetValueAsync()).Result;
потому что в зависимости от контекста вызова там куча вариантов (кажется 6), когда поток попадёт 100% в дедлок, когда не 100% и когда не попадёт. Разница для одного и того же кода будет даже если вы вызывали его как часть библиотеки, через IIS или как часть сервиса.
Ваше замечание было бы правильным для ASP.NET в IIS. Там использутся контекст синхронизации AspNetSynchronizationContext, который запускает задачи завершения не параллельно, а по одной. И из-за этого событие, которого ожидает блокируемый код, может не произойти, потому что оно должно возникнуть в другой задаче завершения — стоящей в очереди без шансов на выполнение. Поэтому там для надежности в случае ожидания надо переключаться в контекст синхронизации пула потоков, что и делает Task.Run.
В ASP.NET Core необходимости в этом нет.
PS Для ожидания завершения Task.Run() тоже лучше (если вы обрабатываете исключения, конечно) использовать не Result, а GetAwaiter().GetResult(), как делает автор — эта конструкция позволяет не выковыривать исключение для обработки из AggregateException, а получить его сразу.
PPS А еще вместо Task.Run() можно (но осторожно) использовать ConfigureAwait(false).
На сколько я понял, отличие от readiness probe заключается в том, что startup probe завершает свою работу после положительного результата (приложение успешно запустилось и готово к работе), в то время как readiness probe работает на протяжении всей жизни приложения и в случае поломки readiness probe k8s перестает давать трафик этому приложению до тех пор, пока readiness probe снова не заработает.
1. Определить liveness healthcheck /healthz — отвечать ok всегда когда поднят web-сервер
2. Определить rediness healthcheck /healthz/ready — отвечать ok когда все зарегистрированные HealthCheck с тегом ready отвечают что всё в порядке
3. Вынести инициализацию в HostedServic'ы, которые по завершению инициализации выставляет флаги готовности, которые уже считывают HealthCheck.
4. Однажды введенный в строй сервис выводить из балансировки только по факту завершения
5. Настроить liveness probe на /healthz, а rediness probe на /healthz/ready
Это стандартный функционал вроде как:
docs.microsoft.com/en-US/aspnet/core/host-and-deploy/health-checks
В случае если нет HealthCheck с тегом ready, rediness будет всегда выдавать ок. В дальнейшем как только вам понадобится этот функционал у вас появится HostedService который будет соответствующий чекер устанавливать.
Весь каркас занимает дай бог 20 строчек кода.
Асинхронный код для двух вещей:
1. Высокая нагрузка, как следствие много запросов и потоки не должны ждать на Network/IO и прочих операциях и продолжать заниматься полезным делом
2. WPF и графический интерфейс — для удобства
Его не надо пихать везде
Асинхронный код в Startup ASP.NET Core: 4 способа обхода GetAwaiter().GetResult()