Pull to refresh

Comments 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 но пока его не хотят исправлять и это печально
Да, Ваш вариант тоже можно добавить в копилку. Отдельное спасибо за ссылку на гитхаб. Собственно, основная мысль, которая побудила меня написать статью, что до тех пор, пока у нас не появится официального async Startup, мы все будем вынуждены придумывать свои более или менее очевидные решения.

Проблема не только в Startup, ещё есть места, где требуется вызов async кода в синхронных методах. И везде в таких случаях использование GetResult может вести к дедлокам

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, и конфигурация читается заново.
Да, но она кэшируется внутри ConfigurationManager, ссылка есть выше
        public async Task<T> GetConfigurationAsync(CancellationToken cancel)
        {
            DateTimeOffset now = DateTimeOffset.UtcNow;
            if (_currentConfiguration != null && _syncAfter > now)
            {
                return _currentConfiguration;
            }
Посмотрел внимательнее. Похоже, Вы правы. Что-то я не дожал этот вопрос. Что ж, я завтра еще раз перепроверю, и если всё так, то выкину этот кусок кода) Спасибо!

Что, надеюсь, не отменяет полезности оставшейся части статьи)
Разумеется нет) Пусть в данном примере и можно избавиться от вызова асинхронной инициализации, но действительно существуют случаи когда это необходимо.
Мы в свое время пришли ко второму варианту.
Бегло прочитал заголовок и думал, когда успел выйти ASP.NET Core 4:)
:D
Вот, скажу нашим сочинителям заголовков, что они должны быть не только завлекательными, но и не вводящими в заблуждение!
Я прочитал статью, но так и не понял, какие задачи может решить асинхронная процедура инициализации сервиса. Ведь до её завершения сервис всё равно не способен обрабатывать запросы клиентов. Так что уменьшить задержку (для чего обычно полезна асинхронность) между запуском и готовностью отвечать на запросы экземпляра сервиса такая процедура всё равно не позволит.
Возможность писать await t вместо t.GetAwaiter().GetResult(), на мой взгляд, имеет малую практическую полезность (ну, кроме сокращения числа нажатий на клавиатуре при написании), потому как семантически это почти (если не учитывать вариант IsCompleted==true) одно и тоже. Видимо именно поэтому Microsoft такая проблема и не напрягает.
Единственная польза, которую я тут смог усмотреть — это борьба с криво написанными/настроенными маршрутизаторами запросов пользователей к серверу (они же, по выполняемой функции — балансировщики нагрузки), которые не способны контролировать доступность экземпляров сервиса и не направлять запросы к недоступным экземплярам, приводящие к большим задержкам ответа клиентам об ошибке. Но почему-то мне кажется, что эту задачу всё равно надо решать как-то по-другому — например, правильным выбором или настройкой балансировщика, — а не перекладывать её на клиента.
Я не прав?
По первому пункту — какой-то принципиальной практической разницы действительно нет, это сугубо вопрос организации кода. Опять же, жили без асинхронного Main(), с ним особо ничего не поменялось, кроме организации кода.
Про балансировщики — признаюсь, я не очень глубоко копал этот вопрос. Но выглядит так, как будто помимо настройки самого балансировщика необходимо будет настроить и сервис, чтобы весь механизм healthcheck/readiness/startup заработал.

Еще юзкейс: метрики приложения отдаются в формате prometheus по http. Если рано запустить вебхост, можно уже начать отдавать метрики и даже отслеживать, сколько времени требуется на старте, и возникают ли какие-то проблемы. Если, конечно, приложение на старте делает что-то тяжелое.
Например: прогрев кэшей, подтягивание каких-то данных, которые нужны для работы, но не хранятся у себя (конфигурация), в экзотических случаях бывает даже кодогенерация на основе рантайм данных, чтоьы потом быстрее что-то работалось...

Это же Startup, он выполняется один раз при старте. Почему не использовать старый добрый Task.Run с асинхронной лямбдой внутри, который выполнит асинхронный метод в отдельном background-потоке, а мы его просто подождём? Это и в msdn описывалось как универсальный подход для запуска асинхронного метода в синхронном, и я не припомню случая, чтобы это вызывало проблемы.


Шаблон:


var value = Task.Run(async () => await GetValueAsync()).Result;

Или


Task.Run(async () => await SomeInitializationAsync()).Wait();
Автору впервую очередь нужен красивый код, а во вторую — рабочее решение.
В точку! Я бы даже обобщил. Это моё сугубо личное мнение, основанное не то чтобы на очень богатом опыте использования, но по факту весь фреймворк ASP.NET — он как раз про организацию кода. А я всегда за то, чтобы код был организован единообразно.
Можете использовать этот подход вполне. А можете один из тех четырех, что описаны в статье. А можете вариант из первого комментария.
Мне вот лично не нравится вызывать асинхронный код в синхронном, если этого можно не делать. Какой-то принципиальной выгоды этот подход не даёт.
async/await разве влияет здесь на что-то?
Интересно, есть ли разница между:
LoadAsync().GetAwaiter().GetResult();
и
LoadAsync().Result
?
Есть.
GetAwaiter().GetResult() возвращает осмысленный Exception, в то время как просто Result возвращает AggregateException.
А ещё LoadAsync().GetAwaiter().GetResult() и LoadAsync().Result вызывают дедлоки и категорически запрещены к использованию, являются жутким говнокодом. Но их используют по старой привычке и потому что ни разу не «прилетело».

Правильно вызывать асинхронный код исключительно через новый поток как написано выше:
var value = Task.Run(async () => await GetValueAsync()).Result;

потому что в зависимости от контекста вызова там куча вариантов (кажется 6), когда поток попадёт 100% в дедлок, когда не 100% и когда не попадёт. Разница для одного и того же кода будет даже если вы вызывали его как часть библиотеки, через IIS или как часть сервиса.
В случае данной статьи вы не правы. Потому что в статье речь идет об ASP.NET Core. А эта среда выполнения отличается от классического ASP.NET в IIS тем, что в ней используется SynchronizationContext по умолчанию — контекст синхронизации пула потоков. В нем задачи завершения (в которые компилятор преобразует части кода после await) могут выполняться параллельно сразу в нескольких потоках. Поэтому блокировка одной из задач в этом контексте на ожидании не приведет к ситуации тупика (deadlock).
Ваше замечание было бы правильным для ASP.NET в IIS. Там использутся контекст синхронизации AspNetSynchronizationContext, который запускает задачи завершения не параллельно, а по одной. И из-за этого событие, которого ожидает блокируемый код, может не произойти, потому что оно должно возникнуть в другой задаче завершения — стоящей в очереди без шансов на выполнение. Поэтому там для надежности в случае ожидания надо переключаться в контекст синхронизации пула потоков, что и делает Task.Run.
В ASP.NET Core необходимости в этом нет.
PS Для ожидания завершения Task.Run() тоже лучше (если вы обрабатываете исключения, конечно) использовать не Result, а GetAwaiter().GetResult(), как делает автор — эта конструкция позволяет не выковыривать исключение для обработки из AggregateException, а получить его сразу.
PPS А еще вместо Task.Run() можно (но осторожно) использовать ConfigureAwait(false).
Недавно я тоже наткнулся на startup probe в k8s 1.16.

На сколько я понял, отличие от 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
Это то, что я пытался описать в решениях 3 и 4. Возможно, не так структурировано, как у Вас. И тут всегда остаётся вопрос, что есть «долго запускающийся» сервис. Получение секретов из внешнего хранилища — это долго или нет? По мнению тех, кто писал получение из Azure KeyVault, недолго, раз можно обойтись без healthcheck в этом случае.
Не важно долго или не долго. Если используется Kubernates — то в каждом сервисе должны быть подняты оконечные точки для liveness & rediness. И до каждого разработчика должно быть донесено, что его сервис не будет запущен на production среде если этого не будет сделано.
В случае если нет HealthCheck с тегом ready, rediness будет всегда выдавать ок. В дальнейшем как только вам понадобится этот функционал у вас появится HostedService который будет соответствующий чекер устанавливать.
Весь каркас занимает дай бог 20 строчек кода.
Асинхронный код для двух вещей:
1. Высокая нагрузка, как следствие много запросов и потоки не должны ждать на Network/IO и прочих операциях и продолжать заниматься полезным делом
2. WPF и графический интерфейс — для удобства
Его не надо пихать везде
Как это противоречит тому, что я написал в выводе?
Я просто не догнал зачем нужен async в инициализации )
Sign up to leave a comment.