TGStat
TGStat
Введите текст для поиска
Расширенный поиск каналов
  • flag Russian
    Язык сайта
    flag Russian flag English flag Uzbek
  • Вход на сайт
  • Каталог
    Каталог каналов и чатов Региональные подборки Тематические подборки Платные каналы Поиск каналов
    Добавить канал/чат
  • Рейтинги
    Рейтинг каналов Рейтинг чатов Рейтинг публикаций
    Рейтинги брендов и персон
  • Аналитика
  • Поиск по публикациям
  • Мониторинг Telegram
  • Продвижение
    Реклама через Яндекс Бизнес Реклама в каналах через TGStat Agency Реклама на сайте TGStat.ru
.NET sh blog

12 Dec 2024, 23:27

Открыть в Telegram Поделиться Пожаловаться

Про Result pattern

В C# основной способ обработки ошибок - Exceptions. Под него заточено много инструментария.
Есть удобный stacktrace, конструкции обработки ошибок try catch, exception filters и прочее.
И самый главный плюс - выброшенное исключение невозможно проигнорировать - кто-то его обязан обработать.

Но есть и минусы:

1. Производительность

Исключения жрут много ресурсов. Основная причина - необходимость в stack unwinding, в поисках того кто выше по стеку сможет обработать исключение чтобы войти в catch блок.

Еще из-за stack unwinding мы теряем данные в AsyncLocal, а следовательно всё что реализовано на его основе, например LoggingScope создаваемый с помощью ILogger.BeginScope будут тоже потеряны.

Исключения в асинхронном коде еще кратно дороже из-за множества стейт машин которые то и дело ловят и перевыбрасывают исключения, пока не дойдут до самого верха стека. А если еще сделать асинхронные middlware (чем пронизан современный .NET) , то там всё еще печальней.

К счастью, майкрософт работает над улучшением производительности и в .NET 9 они уже оптимизировали обработку исключений от 2 до 4 раз.

Возможно, еще когда .NET откажется от async/await на уровне языка в пользу реализации её в рантайме (эксперимент async2),
то поведение AsyncLocal починится и перформанс обработки ошибок заметно станет лучше и будет все хорошо.

2. Исключения - неявный контракт.

Ваш метод может выбросить любое исключение, но оно никак не будет отражено в контрактах (сигнатуре) метода.
Максимум что можно сделать, это добавить в документацию метода.
///
/// When was invalid.
///
Но можно ли доверять этому? Метод может вызывать что-нибудь из BCL, который допустим выбросит InvalidOperationException.
Это что, теперь всевозможные виды исключений перечислять в каждом методе? Никогда не знаешь что будет выброшено.

Альтернативой этому подходу является Result pattern, который является основным способом обработки ошибок в таких языках как Rust и Go, и по факту является монадой т.е. одной из основ функциональных ЯП.

Для наглядности я отрефакторил приложение BookLibrary: перевёл юзкейсы и агрегаты на него. Также сохранил всю функциональность кодов ошибок, форматы ответов API в ProblemDetails, логирование и сбор метрик.
А чтобы бенчмарк был актуален - обновил до .NET 9

Стало определенно лучше с точки зрения контракта.
Я теперь точно знаю по сигнатуре метода может ли он вернуть ошибку. В тоже время это меня теперь обязывает обрабатывать результаты вызовов каждого такого метода, но об этом можно легко забыть и занести баг :)
Здесь очень нужна языковая поддержка и статические анализаторы.
В целом сама концепция хороша тем, что заставляет заранее продумать различные ошибочные сценарии.

Теперь что касается перформанса, здесь всё еще интересней:

Я решил сделать нагрузочное тестирование на API, которое возвращает книгу по его идентификатору.
Тестов будет 2:
Лучший кейс - 100% запросов вернут книгу
Худший кейс - 100% запросов не найдут книгу в бд

Логирование в консоль отключено, чтобы не влияло на результаты.
Проект запущен через консоль, сбилжено в релизной конфигурации. Postgres 16ый в докере.

Получились такие результаты.

Result pattern (Худший кейс):

'https://t.me/sh_dotnet/97?comment=279' rel='nofollow'>100 VUs p95 13ms RPS 13703
'https://t.me/sh_dotnet/97?comment=280' rel='nofollow'>150 VUs p95 19.23ms RPS 13644
'https://t.me/sh_dotnet/97?comment=281' rel='nofollow'>200 VUs p95 24.13ms RPS 13323

Result pattern (Лучший кейс):

'https://t.me/sh_dotnet/97?comment=282' rel='nofollow'>100 VUs p95 12.99ms RPS 13667

Exception (Худший кейс):

'https://t.me/sh_dotnet/97?comment=283' rel='nofollow'>100 VUs p95 171.77ms RPS 817
'https://t.me/sh_dotnet/97?comment=284' rel='nofollow'>150 VUs p95 232.87ms RPS 969
'https://t.me/sh_dotnet/97?comment=285' rel='nofollow'>200 VUs p95 353.16ms RPS 880

Exception (Лучший кейс):

'https://t.me/sh_dotnet/97?comment=286' rel='nofollow'>100 VUs p95 14.41ms RPS 13241

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

Если Ваше приложение высоконагруженное и чувствительное к latency и работает в очень нестабильных условиях (много сетевых вызовов) - Result pattern ваш выбор :)
Причем необязательно всё приложение переводить на Result. Достаточно там, где нестабильность максимальная.

В комментарии закинул скриншоты нагрузочных тестов.

518 0 4 16 16
Каталог
Каталог каналов и чатов Подборки каналов Поиск каналов Добавить канал/чат
Рейтинги
Рейтинг каналов Telegram Рейтинг чатов Telegram Рейтинг публикаций Рейтинги брендов и персон
API
API статистики API поиска публикаций API Callback
Наши каналы
@TGStat @TGStat_Chat @telepulse @TGStatAPI
Почитать
Академия TGStat Исследование Telegram 2019 Исследование Telegram 2021 Исследование Telegram 2023
Контакты
Справочный центр Поддержка Почта Вакансии
Всякая всячина
Пользовательское соглашение Политика конфиденциальности Публичная оферта
Наши боты
@TGStat_Bot @SearcheeBot @TGAlertsBot @tg_analytics_bot @TGStatChatBot