День 2816. #Карьера #AI
Станет ли ИИ-Кодинг Концом Программирования для Людей?
Автор оригинала: Pavel Yosifovich
Мое знакомство с компьютерами состоялось в 1983 г. В ZX-Spectrum я мог использовать язык программирования (BASIC), чтобы заставить компьютер выполнять различные задачи, создавать игры и другие интересные программы. Важной частью процесса было именно удовольствие — создание чего-то из ничего; это было увлекательно и бросало вызов, напоминая игру, независимо от того, что я писал. Вскоре у меня появился Commodore 64. Хотя BASIC оставался основным языком, нередко приходилось писать код и на ассемблере. Главной причиной была низкая скорость BASIC (он был интерпретируемым).
Прошли годы — и BASIC ушёл в прошлое. Ассемблер тоже практически не используется. Популярные языки справляются с большинством задач. Даже интерпретируемые языки вроде Python и JavaScript пользуются огромным успехом: пусть они и «медленные», их быстродействия хватает для множества приложений. С появлением языков программирования, предлагающих более высокие уровни абстракции, потребность в ассемблере свелась к минимуму.
Эра ИИ-программирования
Вы наверняка видели возможности ИИ в написании кода и, вероятно, уже пользовались ими. Признаюсь, мне немного грустно от того, что люди будут писать меньше кода вручную, перепоручая эту задачу ИИ-агентам. Возможно, это естественный эволюционный процесс — переход к использованию абстракций более высокого уровня. Однако здесь есть принципиальное различие. Раньше под более высокой абстракцией обычно подразумевался другой язык программирования — возможно, с более совершенными инструментами и средой разработки.
ИИ-кодинг — процесс особый: мы используем естественный язык, который по своей природе допускает неоднозначность и неточность, — а ведь именно для устранения этих проблем в своё время и создавались языки программирования. ИИ активно используется для написания кода, причем степень участия человека варьируется от существенной до отсутствующей; однако ответственность за качество кода (как бы оно ни определялось для конкретной кодовой базы) по-прежнему лежит на человеке — по крайней мере, пока.
Классические споры энтузиастов разных языков программирования — C# или Java? Python или Ruby? C или C++? — похоже, теряют свою актуальность. Так ли необходимо знать о последних новинках в C#? Если код генерирует ИИ, а мы лишь проводим код-ревью (если проводим), важны ли нам тонкости самого процесса написания? Сторонники использования ИИ в программировании, вероятно, видят будущее (причём не столь отдаленное), в котором ИИ будет писать весь код на основе спецификаций, составленных людьми или даже самим ИИ, причём сам код может быть скрыт от человеческих глаз. Но почему бы не пойти ещё дальше? Возможно, ИИ сможет создать более эффективный язык — ведь использование текстовых токенов удобно лишь для людей; быть может, более компактный и производительный бинарный язык позволил бы ИИ писать код ещё быстрее.
Один из аргументов против такой модели заключается в том, что неизбежно будут возникать ошибки, а значит, участие человека всё же необходимо. ИИ не безошибочен — по крайней мере, пока. В коде могут встречаться состояния гонки, проблемы с памятью, логические ошибки или проблемы с производительностью. Именно здесь требуется вмешательство человека: чтобы исправить ошибку, нужно понимать код. С другой стороны, ИИ способен помочь и в устранении ошибок — он уже это делает. Достаточно дать промпт, предоставить логи и т.д., и ИИ с готовностью возьмётся за поиск и исправление багов. Гарантирован ли успех? Пока нет, но однажды ИИ сможет устранять ошибки самостоятельно — возможно, лишь изредка прибегая к помощи человека. Даже в тех случаях, когда требуется вмешательство человека, «видеть» сам код может и не понадобиться. ИИ сам внесёт исправления: если ожидаемое поведение системы обеспечено и целевые показатели производительности достигнуты, то какая разница, как выглядит код? Конечно, возможны исключительные ситуации, когда ошибки не удается устранить таким способом. Но ИИ всегда способен преобразовать бинарный код в вид, понятный человеку, чтобы тот мог детально изучить проблему.
Кому-то подобные перспективы могут показаться чрезмерными. Неужели мы действительно полностью переложим программирование на плечи машин? Возможно, история сможет нас чему-то научить.
До появления карманных калькуляторов учёные прибегали к помощи людей-«вычислителей», которые выполняли рутинную работу, в то время как сам учёный сосредотачивался на «научной» составляющей. Потом роль таких помощников исчезла — мы безоговорочно доверяем технике. Когда мы пишем код на языке высокого уровня, мы используем компилятор для его преобразования в машинный код, понятный физическому процессору. В компиляторах случаются ошибки, но мы всё равно им доверяем. Ошибку в компиляторе рано или поздно исправят. Почему бы не рассматривать написание кода с помощью ИИ в том же ключе? ИИ может ошибаться, как и компиляторы, но он также способен исправить эти ошибки после их обнаружения. Нам не обязательно видеть сам код.
Конец эпохи ручного программирования?
Означает ли это, что людям больше не нужно писать код вручную? Полагаю, мы движемся именно в этом направлении. Кажется, что участие человека в написании кода больше не требуется. Неудивительно, что многие программисты всерьез опасаются, что ИИ «отнимет» у них работу. Что же ждет программиста-человека в будущем?
Недавно я в прямом эфире демонстрировал процесс написания драйвера ядра Windows с нуля — задачи, требующей учёта множества нюансов. Один из участников спросил, обязательно ли знать все эти детали: ведь ИИ наверняка может написать код вместо меня. Возможно, ИИ справится с написанием кода даже лучше меня. Однако я делал упор не на деталях реализации, а на архитектуре и концепциях, лежащих в основе кода: как работает драйвер ядра? Как он загружается? Как управляет ресурсами? Как взаимодействует с ядром? И т.д. Суть в том, что детали реализации кода можно доверить ИИ, но фундаментальные концепции — нет. Во-первых, как человек может провести проверку кода, написанного ИИ? Даже если предположить, что ИИ пишет код без ошибок, как быть с общей архитектурой и замыслом проекта? ИИ может не видеть всей картины целиком. И даже поручив написание всего кода ИИ, всё равно придётся объяснить ему, что именно нужно создать. Какова роль драйвера в этом проекте? Как он должен взаимодействовать с пользователем? Какие логи использовать? Здесь возможны разные варианты, и именно поэтому человеку необходимо глубокое понимание предметной области, чтобы составить качественное техническое задание для ИИ. Человек становится проектировщиком и архитектором, делегируя рутинную работу и технические детали ИИ.
Этот постепенный переход — длительный процесс. Он не произойдет за месяц, но и десятилетия на это не уйдут. Темпы развития ИИ трудно предсказать, поскольку они, похоже, носят экспоненциальный характер, а человеческая природа не приспособлена к восприятию таких процессов — мы мыслим линейно.
Итого
В глубине души мне грустно от такого положения дел. Мне по-прежнему нравится писать код вручную, создавая то, что я могу назвать своим творением. Все мои знания тонкостей тех или иных языков программирования со временем устареют. Стоит ли изучать новейшие версии языков программирования? Пока да. Но полагаю, что в недалёком будущем это изменится.
Если код моих приложений пишет ИИ, можно ли считать этот код моим? А может, это неверная постановка вопроса. «Моим» является замысел приложения, его архитектура, функционал, возможности и дизайн. Сам же код — это лишь деталь реализации, с чем лучше справляются машины.
Источник: https://scorpiosoftware.net/2026/09/21/is-ai-coding-the-end-of-coding/
Станет ли ИИ-Кодинг Концом Программирования для Людей?
Автор оригинала: Pavel Yosifovich
Мое знакомство с компьютерами состоялось в 1983 г. В ZX-Spectrum я мог использовать язык программирования (BASIC), чтобы заставить компьютер выполнять различные задачи, создавать игры и другие интересные программы. Важной частью процесса было именно удовольствие — создание чего-то из ничего; это было увлекательно и бросало вызов, напоминая игру, независимо от того, что я писал. Вскоре у меня появился Commodore 64. Хотя BASIC оставался основным языком, нередко приходилось писать код и на ассемблере. Главной причиной была низкая скорость BASIC (он был интерпретируемым).
Прошли годы — и BASIC ушёл в прошлое. Ассемблер тоже практически не используется. Популярные языки справляются с большинством задач. Даже интерпретируемые языки вроде Python и JavaScript пользуются огромным успехом: пусть они и «медленные», их быстродействия хватает для множества приложений. С появлением языков программирования, предлагающих более высокие уровни абстракции, потребность в ассемблере свелась к минимуму.
Эра ИИ-программирования
Вы наверняка видели возможности ИИ в написании кода и, вероятно, уже пользовались ими. Признаюсь, мне немного грустно от того, что люди будут писать меньше кода вручную, перепоручая эту задачу ИИ-агентам. Возможно, это естественный эволюционный процесс — переход к использованию абстракций более высокого уровня. Однако здесь есть принципиальное различие. Раньше под более высокой абстракцией обычно подразумевался другой язык программирования — возможно, с более совершенными инструментами и средой разработки.
ИИ-кодинг — процесс особый: мы используем естественный язык, который по своей природе допускает неоднозначность и неточность, — а ведь именно для устранения этих проблем в своё время и создавались языки программирования. ИИ активно используется для написания кода, причем степень участия человека варьируется от существенной до отсутствующей; однако ответственность за качество кода (как бы оно ни определялось для конкретной кодовой базы) по-прежнему лежит на человеке — по крайней мере, пока.
Классические споры энтузиастов разных языков программирования — C# или Java? Python или Ruby? C или C++? — похоже, теряют свою актуальность. Так ли необходимо знать о последних новинках в C#? Если код генерирует ИИ, а мы лишь проводим код-ревью (если проводим), важны ли нам тонкости самого процесса написания? Сторонники использования ИИ в программировании, вероятно, видят будущее (причём не столь отдаленное), в котором ИИ будет писать весь код на основе спецификаций, составленных людьми или даже самим ИИ, причём сам код может быть скрыт от человеческих глаз. Но почему бы не пойти ещё дальше? Возможно, ИИ сможет создать более эффективный язык — ведь использование текстовых токенов удобно лишь для людей; быть может, более компактный и производительный бинарный язык позволил бы ИИ писать код ещё быстрее.
Один из аргументов против такой модели заключается в том, что неизбежно будут возникать ошибки, а значит, участие человека всё же необходимо. ИИ не безошибочен — по крайней мере, пока. В коде могут встречаться состояния гонки, проблемы с памятью, логические ошибки или проблемы с производительностью. Именно здесь требуется вмешательство человека: чтобы исправить ошибку, нужно понимать код. С другой стороны, ИИ способен помочь и в устранении ошибок — он уже это делает. Достаточно дать промпт, предоставить логи и т.д., и ИИ с готовностью возьмётся за поиск и исправление багов. Гарантирован ли успех? Пока нет, но однажды ИИ сможет устранять ошибки самостоятельно — возможно, лишь изредка прибегая к помощи человека. Даже в тех случаях, когда требуется вмешательство человека, «видеть» сам код может и не понадобиться. ИИ сам внесёт исправления: если ожидаемое поведение системы обеспечено и целевые показатели производительности достигнуты, то какая разница, как выглядит код? Конечно, возможны исключительные ситуации, когда ошибки не удается устранить таким способом. Но ИИ всегда способен преобразовать бинарный код в вид, понятный человеку, чтобы тот мог детально изучить проблему.
Кому-то подобные перспективы могут показаться чрезмерными. Неужели мы действительно полностью переложим программирование на плечи машин? Возможно, история сможет нас чему-то научить.
До появления карманных калькуляторов учёные прибегали к помощи людей-«вычислителей», которые выполняли рутинную работу, в то время как сам учёный сосредотачивался на «научной» составляющей. Потом роль таких помощников исчезла — мы безоговорочно доверяем технике. Когда мы пишем код на языке высокого уровня, мы используем компилятор для его преобразования в машинный код, понятный физическому процессору. В компиляторах случаются ошибки, но мы всё равно им доверяем. Ошибку в компиляторе рано или поздно исправят. Почему бы не рассматривать написание кода с помощью ИИ в том же ключе? ИИ может ошибаться, как и компиляторы, но он также способен исправить эти ошибки после их обнаружения. Нам не обязательно видеть сам код.
Конец эпохи ручного программирования?
Означает ли это, что людям больше не нужно писать код вручную? Полагаю, мы движемся именно в этом направлении. Кажется, что участие человека в написании кода больше не требуется. Неудивительно, что многие программисты всерьез опасаются, что ИИ «отнимет» у них работу. Что же ждет программиста-человека в будущем?
Недавно я в прямом эфире демонстрировал процесс написания драйвера ядра Windows с нуля — задачи, требующей учёта множества нюансов. Один из участников спросил, обязательно ли знать все эти детали: ведь ИИ наверняка может написать код вместо меня. Возможно, ИИ справится с написанием кода даже лучше меня. Однако я делал упор не на деталях реализации, а на архитектуре и концепциях, лежащих в основе кода: как работает драйвер ядра? Как он загружается? Как управляет ресурсами? Как взаимодействует с ядром? И т.д. Суть в том, что детали реализации кода можно доверить ИИ, но фундаментальные концепции — нет. Во-первых, как человек может провести проверку кода, написанного ИИ? Даже если предположить, что ИИ пишет код без ошибок, как быть с общей архитектурой и замыслом проекта? ИИ может не видеть всей картины целиком. И даже поручив написание всего кода ИИ, всё равно придётся объяснить ему, что именно нужно создать. Какова роль драйвера в этом проекте? Как он должен взаимодействовать с пользователем? Какие логи использовать? Здесь возможны разные варианты, и именно поэтому человеку необходимо глубокое понимание предметной области, чтобы составить качественное техническое задание для ИИ. Человек становится проектировщиком и архитектором, делегируя рутинную работу и технические детали ИИ.
Этот постепенный переход — длительный процесс. Он не произойдет за месяц, но и десятилетия на это не уйдут. Темпы развития ИИ трудно предсказать, поскольку они, похоже, носят экспоненциальный характер, а человеческая природа не приспособлена к восприятию таких процессов — мы мыслим линейно.
Итого
В глубине души мне грустно от такого положения дел. Мне по-прежнему нравится писать код вручную, создавая то, что я могу назвать своим творением. Все мои знания тонкостей тех или иных языков программирования со временем устареют. Стоит ли изучать новейшие версии языков программирования? Пока да. Но полагаю, что в недалёком будущем это изменится.
Если код моих приложений пишет ИИ, можно ли считать этот код моим? А может, это неверная постановка вопроса. «Моим» является замысел приложения, его архитектура, функционал, возможности и дизайн. Сам же код — это лишь деталь реализации, с чем лучше справляются машины.
Источник: https://scorpiosoftware.net/2026/09/21/is-ai-coding-the-end-of-coding/
👍7👎2
День 2817. #ЧтоНовенького #VS
Прекращаем Сборку После Первой Ошибки
Мэдс Кристенсен продолжает поставлять небольшие и на первый взгляд очевидные улучшения процесса разработки в Visual Studio.
Если один из проектов в решении не удается собрать, процесс сборки всего решения обычно продолжается. У вас уже есть ошибка, которую нужно исправить, но остальные проекты продолжают собираться, несмотря на то что сборка решения всё равно закончится неудачей.
В VS2026 (версии 18.11) появилась опция, позволяющая автоматически останавливать сборку решения сразу после того, как в одном из проектов произошел сбой. Если ваш следующий шаг — исправление этой ошибки, то такая настройка поможет сэкономить время, прекращая дальнейший процесс сборки.
Настройка остановки сборки
Перейдите в раздел Tools > Options > Projects and Solutions > Build and Run (Сервис > Параметры > Проекты и решения > Сборка и запуск) и включите параметр Stop solution build after first failure (Останавливать сборку решения после первой ошибки).
При возникновении ошибки в проекте Visual Studio перестанет запускать задачи для проектов, стоящих в очереди, и безопасно остановит те, что уже находятся в процессе сборки. При этом результат сборки помечается как «сбой» (failed), а не «отмена» (canceled). Вам больше не нужно следить за появлением первой ошибки и вручную прерывать выполнение остальных задач.
Эта функция останавливает сборку всего решения, а не только тех проектов, которые зависят от проблемного компонента. Это важное отличие: вы быстрее переходите к устранению первопричины, но при этом не получаете результаты сборки остальных проектов, входящих в решение.
Если вы предпочитаете видеть результаты по всем проектам, прежде чем приступать к исправлению ошибок, оставьте эту настройку отключенной — в таком случае сохранится привычное поведение системы. При необходимости функцию всегда можно отключить на той же странице параметров.
Итоговая экономия времени зависит от структуры вашего решения и того, на каком этапе произошёл сбой. В решениях с длинными цепочками зависимостей или большим объёмом параллельных задач экономия времени будет более заметной, чем в небольших проектах, сборка которых и так завершается практически мгновенно.
Данная настройка определяет, когда следует остановить процесс сборки в случае возникновения ошибки. Она не влияет на то, какие именно проекты будут включены в сборку. Существующие настройки Build and Run (Сборка и запуск) уже позволяют задать, например, сборку только запускаемых проектов и их зависимостей при выполнении или отладке.
Попробуйте использовать функцию остановки при сбое для решения, в котором вы обычно отменяете сборку после появления ошибки. Сравните время ожидания после возникновения сбоя и выберите тот подход, который лучше всего соответствует вашему процессу поиска и устранения неполадок.
Источник: https://devblogs.microsoft.com/visualstudio/stop-waiting-on-a-failed-build/
Прекращаем Сборку После Первой Ошибки
Мэдс Кристенсен продолжает поставлять небольшие и на первый взгляд очевидные улучшения процесса разработки в Visual Studio.
Если один из проектов в решении не удается собрать, процесс сборки всего решения обычно продолжается. У вас уже есть ошибка, которую нужно исправить, но остальные проекты продолжают собираться, несмотря на то что сборка решения всё равно закончится неудачей.
В VS2026 (версии 18.11) появилась опция, позволяющая автоматически останавливать сборку решения сразу после того, как в одном из проектов произошел сбой. Если ваш следующий шаг — исправление этой ошибки, то такая настройка поможет сэкономить время, прекращая дальнейший процесс сборки.
Настройка остановки сборки
Перейдите в раздел Tools > Options > Projects and Solutions > Build and Run (Сервис > Параметры > Проекты и решения > Сборка и запуск) и включите параметр Stop solution build after first failure (Останавливать сборку решения после первой ошибки).
При возникновении ошибки в проекте Visual Studio перестанет запускать задачи для проектов, стоящих в очереди, и безопасно остановит те, что уже находятся в процессе сборки. При этом результат сборки помечается как «сбой» (failed), а не «отмена» (canceled). Вам больше не нужно следить за появлением первой ошибки и вручную прерывать выполнение остальных задач.
Эта функция останавливает сборку всего решения, а не только тех проектов, которые зависят от проблемного компонента. Это важное отличие: вы быстрее переходите к устранению первопричины, но при этом не получаете результаты сборки остальных проектов, входящих в решение.
Если вы предпочитаете видеть результаты по всем проектам, прежде чем приступать к исправлению ошибок, оставьте эту настройку отключенной — в таком случае сохранится привычное поведение системы. При необходимости функцию всегда можно отключить на той же странице параметров.
Итоговая экономия времени зависит от структуры вашего решения и того, на каком этапе произошёл сбой. В решениях с длинными цепочками зависимостей или большим объёмом параллельных задач экономия времени будет более заметной, чем в небольших проектах, сборка которых и так завершается практически мгновенно.
Данная настройка определяет, когда следует остановить процесс сборки в случае возникновения ошибки. Она не влияет на то, какие именно проекты будут включены в сборку. Существующие настройки Build and Run (Сборка и запуск) уже позволяют задать, например, сборку только запускаемых проектов и их зависимостей при выполнении или отладке.
Попробуйте использовать функцию остановки при сбое для решения, в котором вы обычно отменяете сборку после появления ошибки. Сравните время ожидания после возникновения сбоя и выберите тот подход, который лучше всего соответствует вашему процессу поиска и устранения неполадок.
Источник: https://devblogs.microsoft.com/visualstudio/stop-waiting-on-a-failed-build/
👍1👎1
День 2818. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
50. Фреймворки для создания тестовых заглушек
«Расскажите, как использовать тестовые заглушки для модульного тестирования? Приведите примеры создания заглушек для зависимостей и проверки того, что код ведёт себя ожидаемым образом».
Хороший ответ
Использование фреймворков, вроде Moq или NSubstitute в .NET-приложениях для модульного тестирования предполагает создание заглушек для зависимостей, которые могут быть у класса. Это позволяет всесторонне протестировать класс в изоляции.
Тестовые заглушки – общий термин, описывающий все разновидности фиктивных зависимостей в тестах. Среди них можно выделить два основных:
- Моки (mock) – позволяют эмулировать и проверять выходные взаимодействия, т.е. вызовы, совершаемые тестируемой системой к её зависимостям для изменения их состояния. Например, моком можно заменить почтовый сервер, на который отправляется письмо.
- Стабы (stub) – помогают эмулировать входные взаимодействия, т.е. вызовы к зависимостям для получения входных данных. Например, стабом можно заменить базу, из которой запрашиваются данные. Стабы настраиваются для возвращения разных значений для разных сценариев.
Вот пример использования фреймворка NSubstitute:
NSubstitute позволяет проверять, что методы мок-объекта были вызваны с ожидаемыми аргументами. Это важно для того, чтобы убедиться, что тестируемый класс правильно взаимодействует со своими зависимостями. Для этого в NSubstitute используется метод
Рекомендации
1. Проверка поведения: важно всегда проверять поведение, а не детали реализации. Т.е. фокусироваться на том, что возвращает мок или его влияние на результат, а не на том, как именно он это делает.
Замечание: не стоит проверять взаимодействие со стабами. В примере выше ICalculator по сути является стабом. Он предоставляет входные значения для сервиса. Проверка
При этом заглушка может быть моком и стабом одновременно, когда в тесте задаётся поведение одного метода зависимости, а проверяется взаимодействие с другим методом. Например, при тестировании сервиса бронирования такой заглушкой может быть репозиторий, где задаётся поведение метода проверки наличия свободных мест, а проверяется, что сервис бронирования вызывает метод резервации места в репозитории.
2. Один мок на тест: в идеале для каждого теста следует создавать отдельный мок-объект. Это помогает избежать взаимного влияния тестов друг на друга и делает их более понятными.
3. Чёткая настройка взаимодействий: важно явно задавать взаимодействия с моками и проверять их — это обеспечит ясность тестов и упростит их дальнейшее сопровождение.
Распространённые ошибки
- Чрезмерное использование заглушек: делает тесты хрупкими и излишне сложными. Заменять стоит только те зависимости, которые являются внешними для тестируемого класса. Использование заглушек для приватных зависимостей, простых структур данных или сущностей обычно приводит к тому, что тесты ломаются при малейшем изменении структуры данных.
И даже внешние зависимости не всегда стоит заменять заглушками. Например, база данных, которая используется только вашим приложением, и к которой не имеют доступа внешние системы, полностью скрыта от клиентов вашей системы. Её даже можно заменить другой базой, и никто не заметит. Здесь вместо замены базы заглушкой в тестах можно рассмотреть тестирование с использованием БД в памяти.
- Игнорирование реальных реализаций: заменяя слишком много объектов — особенно те, что содержат бизнес-логику, которую тоже следовало бы протестировать, — вы рискуете пропустить серьёзные проблемы интеграции или логические ошибки в реальном коде.
- Мокирование конкретных классов: Moq позволят задать поведение отдельных методов зависимости, при этом вызывая методы реального объекта, поведение которых не было задано. Например:
Настройка
Это один из антипаттернов тестирования. Если вам приходится так делать, это часто означает, что класс зависимости объединяет две ответственности, и стоит его разделить.
Источники:
- https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
- «Принципы юнит-тестирования» (Владимир Хориков — СПб.: Питер, 2022).
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
50. Фреймворки для создания тестовых заглушек
«Расскажите, как использовать тестовые заглушки для модульного тестирования? Приведите примеры создания заглушек для зависимостей и проверки того, что код ведёт себя ожидаемым образом».
Хороший ответ
Использование фреймворков, вроде Moq или NSubstitute в .NET-приложениях для модульного тестирования предполагает создание заглушек для зависимостей, которые могут быть у класса. Это позволяет всесторонне протестировать класс в изоляции.
Тестовые заглушки – общий термин, описывающий все разновидности фиктивных зависимостей в тестах. Среди них можно выделить два основных:
- Моки (mock) – позволяют эмулировать и проверять выходные взаимодействия, т.е. вызовы, совершаемые тестируемой системой к её зависимостям для изменения их состояния. Например, моком можно заменить почтовый сервер, на который отправляется письмо.
- Стабы (stub) – помогают эмулировать входные взаимодействия, т.е. вызовы к зависимостям для получения входных данных. Например, стабом можно заменить базу, из которой запрашиваются данные. Стабы настраиваются для возвращения разных значений для разных сценариев.
Вот пример использования фреймворка NSubstitute:
using NSubstitute;
using Xunit;
public class ServiceTests
{
private IService _sut;
private ICalculator _calc;
public ServiceTests()
{
// Создаём заглушку для зависимости ICalculator
_calc = Substitute.For<ICalculator>();
_sut = new Service(_calc);
}
[Fact]
public void AddNumbers_ShouldReturnCorrectSum()
{
// Задаём поведение метода Add
_calc.Add(5, 10).Returns(15);
var result = _sut.AddNumbers(5, 10);
Assert.Equal(15, result);
// Проверяем, что Add был вызван
_calc.Received(1).Add(5, 10);
}
}
NSubstitute позволяет проверять, что методы мок-объекта были вызваны с ожидаемыми аргументами. Это важно для того, чтобы убедиться, что тестируемый класс правильно взаимодействует со своими зависимостями. Для этого в NSubstitute используется метод
Received(), как показано в примере выше.Рекомендации
1. Проверка поведения: важно всегда проверять поведение, а не детали реализации. Т.е. фокусироваться на том, что возвращает мок или его влияние на результат, а не на том, как именно он это делает.
Замечание: не стоит проверять взаимодействие со стабами. В примере выше ICalculator по сути является стабом. Он предоставляет входные значения для сервиса. Проверка
Received() показана для примера, но она излишняя и делает тест хрупким. Тест не должен интересоваться, как тестируемая система получает результат, если результат верный.При этом заглушка может быть моком и стабом одновременно, когда в тесте задаётся поведение одного метода зависимости, а проверяется взаимодействие с другим методом. Например, при тестировании сервиса бронирования такой заглушкой может быть репозиторий, где задаётся поведение метода проверки наличия свободных мест, а проверяется, что сервис бронирования вызывает метод резервации места в репозитории.
2. Один мок на тест: в идеале для каждого теста следует создавать отдельный мок-объект. Это помогает избежать взаимного влияния тестов друг на друга и делает их более понятными.
3. Чёткая настройка взаимодействий: важно явно задавать взаимодействия с моками и проверять их — это обеспечит ясность тестов и упростит их дальнейшее сопровождение.
Распространённые ошибки
- Чрезмерное использование заглушек: делает тесты хрупкими и излишне сложными. Заменять стоит только те зависимости, которые являются внешними для тестируемого класса. Использование заглушек для приватных зависимостей, простых структур данных или сущностей обычно приводит к тому, что тесты ломаются при малейшем изменении структуры данных.
И даже внешние зависимости не всегда стоит заменять заглушками. Например, база данных, которая используется только вашим приложением, и к которой не имеют доступа внешние системы, полностью скрыта от клиентов вашей системы. Её даже можно заменить другой базой, и никто не заметит. Здесь вместо замены базы заглушкой в тестах можно рассмотреть тестирование с использованием БД в памяти.
- Игнорирование реальных реализаций: заменяя слишком много объектов — особенно те, что содержат бизнес-логику, которую тоже следовало бы протестировать, — вы рискуете пропустить серьёзные проблемы интеграции или логические ошибки в реальном коде.
- Мокирование конкретных классов: Moq позволят задать поведение отдельных методов зависимости, при этом вызывая методы реального объекта, поведение которых не было задано. Например:
var moq = new Mock<Calculator> {CallBase = true };
// метод Add должен быть виртуальным
moq.Setup(x => x.Add(5, 10)).Returns(15);Настройка
CallBase = true позволяет моку сохранить реальное поведение базового класса, если оно не было переопределено. Таким образом вызов moq.Add(5, 10) в тесте будет использовать заданное поведение, а вызов, например, moq.Multiply(5, 10) будет использовать метод реального объекта Calculator. Так можно, к примеру, задать заглушки только для методов класса, меняющих состояние системы (пишущих в базу), при этом вызывая реальные методы, не меняющие состояния (читающие из базы).Это один из антипаттернов тестирования. Если вам приходится так делать, это часто означает, что класс зависимости объединяет две ответственности, и стоит его разделить.
Источники:
- https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
- «Принципы юнит-тестирования» (Владимир Хориков — СПб.: Питер, 2022).
👎2
День 2819. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 4
Паттерн awaitable: создание объектов, поддерживающих ожидание
Если спросить большинство C#-разработчиков, что можно использовать с оператором
Понимание этой структуры поможет разобраться в конечном автомате, который генерируется компилятором (о нём позже). Кроме того, это позволит создавать собственные «ожидаемые» объекты.
Структура
Чтобы выражение
Вот и весь контракт. Компилятор выполняет привязку этих членов по их именам, что имеет два важных следствия:
1. Метод
2. Объект ожидания может быть структурой. TaskAwaiter, ValueTaskAwaiter и YieldAwaitable.YieldAwaiter являются структурами; поэтому при ожидании уже завершённой задачи не происходит выделения памяти под объект ожидания.
Как используются эти три члена
Для каждого вызова
- Свойство
- Метод
- Метод
То, что
ICriticalNotifyCompletion
Существует еще один интерфейс —
Слово «Unsafe» звучит тревожно, но на деле всё не так страшно: создаваемый компилятором конечный автомат уже захватывает
Реализуйте оба интерфейса. В
Полезный объект, поддерживающий ожидание
Особенности:
- Тип сам является своим собственным «ожидателем».
- Свойство
- Это readonly-структура. Для операции ожидания не требуется выделение памяти в куче.
Примечание: Тот же механизм, только наоборот, позволяет в UI-фреймворках использовать конструкцию
Когда стоит создавать свой объект ожидания
Довольно редко. В большинстве случаев правильным выбором будут
1. Вы хотите, чтобы
2. Вы работаете с механизмом завершения операций, который существенно отличается от стандартного, и хотите избежать выделения памяти под объект
3. Вы хотите сделать существующий тип «ожидаемым», не меняя его исходный код — здесь поможет прием с методами расширения.
Важно помнить: собственный объект ожидания не поддерживает композицию. Он не будет работать с
Источник: https://www.meziantou.net/the-awaitable-pattern-writing-your-own-awaitable.htm
Ещё Раз Про Async/Await в C#. Часть 4
Паттерн awaitable: создание объектов, поддерживающих ожидание
Если спросить большинство C#-разработчиков, что можно использовать с оператором
await, они ответят: «Task». Это распространённый, но далеко не единственный случай. Механизм await работает так же, как и foreach, на основе структуры. Компилятор ищет определенную «форму» (набор элементов), и Task как раз ей соответствует.Понимание этой структуры поможет разобраться в конечном автомате, который генерируется компилятором (о нём позже). Кроме того, это позволит создавать собственные «ожидаемые» объекты.
Структура
Чтобы выражение
await expr успешно скомпилировалось, у объекта expr должен быть метод GetAwaiter(). Возвращаемый им тип — так называемый «awaiter» (объект ожидания) — должен содержать три элемента и реализовывать один интерфейс:public readonly struct MyAwaiter : INotifyCompletion
{
// завершилась ли уже операция?
public bool IsCompleted { get; }
// значение, либо повторный выброс исключения
public TResult GetResult();
// вызывается при завершении
public void OnCompleted(Action continuation);
}
Вот и весь контракт. Компилятор выполняет привязку этих членов по их именам, что имеет два важных следствия:
1. Метод
GetAwaiter может быть методом расширения. Это позволяет сделать тип «ожидаемым», даже если вы не являетесь его владельцем:public static class
TimeSpanAwaitableExtensions
{
public static TaskAwaiter
GetAwaiter(this TimeSpan timeSpan) =>
Task.Delay(timeSpan).GetAwaiter();
}
…
await TimeSpan.FromMilliseconds(200);
2. Объект ожидания может быть структурой. TaskAwaiter, ValueTaskAwaiter и YieldAwaitable.YieldAwaiter являются структурами; поэтому при ожидании уже завершённой задачи не происходит выделения памяти под объект ожидания.
Как используются эти три члена
Для каждого вызова
await компилятор генерирует примерно следующий код:var awaiter = expr.GetAwaiter();
if (!awaiter.IsCompleted)
{
// сохранение состояния
// регистрация продолжения
// возврат к вызывающему методу
awaiter.OnCompleted(MoveNext);
return;
}
var result = awaiter.GetResult();
- Свойство
IsCompleted определяет, будет ли выполнение метода приостановлено. Если оно возвращает true, выполнение продолжается без прерываний, в том же потоке, без планирования задачи и выделения дополнительной памяти. Это «быстрый путь», ради которого и существует ValueTask.- Метод
OnCompleted вызывается только в том случае, если IsCompleted вернуло false. Он получает продолжение и отвечает за его последующий запуск. Где именно будет выполнено это продолжение — решать вам.- Метод
GetResult вызывается в обоих случаях. Он возвращает результат операции и, что важно, именно здесь повторно выбрасываются исключения. Если операция завершилась неудачей, ожидающий объект сообщает об этом, выбрасывая исключение из GetResult, а не через какой-то отдельный канал для передачи ошибок.То, что
GetResult может возвращать void, нормально: именно так ведёт себя await при работе с непараметризованным Task.ICriticalNotifyCompletion
Существует еще один интерфейс —
ICriticalNotifyCompletion, который добавляет метод UnsafeOnCompleted. Практически все реальные реализации ожидающих объектов поддерживают его. Разница заключается в работе с ExecutionContext. Ожидается, что OnCompleted захватит текущий ExecutionContext и восстановит его перед выполнением продолжения. UnsafeOnCompleted этого делать не обязан.Слово «Unsafe» звучит тревожно, но на деле всё не так страшно: создаваемый компилятором конечный автомат уже захватывает
ExecutionContext при запуске метода и восстанавливает его при каждом вызове MoveNext. Повторный захват контекста для каждого await был бы излишней работой. Поэтому конечный автомат вызывает UnsafeOnCompleted, если ожидающий объект его предоставляет, а в противном случае использует «безопасную» версию (OnCompleted).Реализуйте оба интерфейса. В
UnsafeOnCompleted должна выполняться основная логика, а OnCompleted может просто вызывать UnsafeOnCompleted — при условии, что у вас нет собственного контекстного состояния, которое нужно передавать дальше.Полезный объект, поддерживающий ожидание
Task.Yield() возвращает управление в текущий SynchronizationContext (например, в UI-приложении это означает возврат в UI-поток). Иногда же требуется обратное: безусловно покинуть текущий поток и перейти в поток из пула потоков.public readonly struct
ThreadPoolAwaitable : ICriticalNotifyCompletion
{
public ThreadPoolAwaitable GetAwaiter() => this;
public bool IsCompleted => false;
public void GetResult()
{
}
public void OnCompleted(Action continuation)
=> UnsafeOnCompleted(continuation);
public void UnsafeOnCompleted(Action continuation)
=> ThreadPool.UnsafeQueueUserWorkItem(
static state => ((Action)state!)(), continuation);
}
…
await default(ThreadPoolAwaitable);
// дальнейший код выполняется в потоке из пула
Особенности:
- Тип сам является своим собственным «ожидателем».
GetAwaiter() возвращает this. Это распространённый прием, который позволяет избежать лишнего выделения памяти и дополнительного уровня косвенности; YieldAwaitable использует такой же подход.- Свойство
IsCompleted всегда возвращает false. Это сделано намеренно: если бы оно возвращало true, продолжение выполнялось бы синхронно в текущем потоке, а именно этого мы и стараемся избежать.- Это readonly-структура. Для операции ожидания не требуется выделение памяти в куче.
Примечание: Тот же механизм, только наоборот, позволяет в UI-фреймворках использовать конструкцию
await someControl.Dispatcher; для возврата в UI-поток. Метод OnCompleted объекта ожидания в данном случае помещает продолжение в диспетчер, а не в пул потоков.Когда стоит создавать свой объект ожидания
Довольно редко. В большинстве случаев правильным выбором будут
Task, ValueTask или TaskCompletionSource. Создание собственного объекта ожидания оправдано в следующих ситуациях:1. Вы хотите, чтобы
await означал «смену контекста» (как в случае с Task.Yield() или ThreadPoolAwaitable). При этом не нужно передавать какой-либо результат — важен только факт планирования выполнения.2. Вы работаете с механизмом завершения операций, который существенно отличается от стандартного, и хотите избежать выделения памяти под объект
Task для каждой операции. Для этого существует интерфейс IValueTaskSource (про него позже в этой серии).3. Вы хотите сделать существующий тип «ожидаемым», не меняя его исходный код — здесь поможет прием с методами расширения.
Важно помнить: собственный объект ожидания не поддерживает композицию. Он не будет работать с
Task.WhenAll, WaitAsync или любыми другими механизмами, ожидающими объект Task. Если вам нужны такие возможности, просто возвращайте Task.Источник: https://www.meziantou.net/the-awaitable-pattern-writing-your-own-awaitable.htm
👍9
День 2820. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 5
Как компилятор C# переписывает асинхронный метод. Начало
Вплоть до версии .NET 10,
Метод содержит все элементы, с которыми приходится работать компилятору: параметр, локальные переменные, значения которых должны сохраняться после
Чтобы увидеть, что с методом делает компилятор, можно либо декомпилировать его с помощью ILSpy, либо использовать онлайн инструмент sharplab.io.
Что осталось от исходного метода
По порядку:
1. Создаётся экземпляр конечного автомата.
2. Для него создаётся построитель —
3. Параметры копируются в конечный автомат. Теперь они становятся его полями, поскольку код, который их использует, находится в другом месте.
4. Состояние устанавливается в значение -1 (не запущено, не приостановлено).
5. Вызывается метод
Сгенерированные имена с угловыми скобками не являются допустимыми идентификаторами в языке C#. Это сделано намеренно: они не могут конфликтовать с именами, которые вы задаёте в своём коде, и вы не сможете случайно сослаться на них.
Примечание: Атрибут
Важное следствие: вызов асинхронного метода приводит к выполнению его тела до первого ожидания (
Сгенерированный тип
Ниже представлены поля конечного автомата:
-
-
-
-
-
-
Важно:
1. Локальная переменная становится полем класса только в том случае, если она необходима после оператора
2. Слоты для объектов ожидания привязаны к их типу, а не к конкретному оператору
Окончание следует…
Источник: https://www.meziantou.net/how-the-csharp-compiler-rewrites-an-async-method.htm
Ещё Раз Про Async/Await в C#. Часть 5
Как компилятор C# переписывает асинхронный метод. Начало
Вплоть до версии .NET 10,
async и await не являются встроенными возможностями среды выполнения. Компилятор переписывает код метода с модификатором async. Он извлекает тело метода, переносит его в автоматически созданный тип, а исходный метод заменяет небольшим фрагментом кода, который инициализирует этот тип и запускает его метод. Всё остальное — планирование выполнения, обработка исключений, формирование значения, которое в итоге попадает в возвращаемый объект Task, — представляет собой обычный код, который вы могли бы написать самостоятельно. Сегодня рассмотрим небольшой асинхронный метод и проанализируем, во что его превращает компилятор.Метод содержит все элементы, с которыми приходится работать компилятору: параметр, локальные переменные, значения которых должны сохраняться после
await, два await разных типов, цикл, а также блок try/finally:public static async Task<int>
SumPageSizesAsync(string[] pages)
{
var total = 0;
var sw = Stopwatch.StartNew();
try
{
foreach (var page in pages)
{
var content = await LoadAsync(page);
total += content.Length;
}
await Task.Yield();
return total;
}
finally
{
Console.WriteLine($"Loaded {pages.Length} pages in {sw.ElapsedMilliseconds}ms");
}
}
private static async Task<string>
LoadAsync(string page)
{
await Task.Delay(1);
return page;
}
Чтобы увидеть, что с методом делает компилятор, можно либо декомпилировать его с помощью ILSpy, либо использовать онлайн инструмент sharplab.io.
Что осталось от исходного метода
[AsyncStateMachine(typeof(<SumPageSizesAsync>d__0))]
public static Task<int> SumPageSizesAsync(string[] pages)
{
<SumPageSizesAsync>d__0 stateMachine = default(<SumPageSizesAsync>d__0);
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.pages = pages;
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
По порядку:
1. Создаётся экземпляр конечного автомата.
2. Для него создаётся построитель —
AsyncTaskMethodBuilder<int>. Он управляет объектом Task, который будут ожидать вызывающие стороны; именно он отвечает за завершение.3. Параметры копируются в конечный автомат. Теперь они становятся его полями, поскольку код, который их использует, находится в другом месте.
4. Состояние устанавливается в значение -1 (не запущено, не приостановлено).
5. Вызывается метод
Start, который запускает выполнение тела метода, после чего возвращается объект Task.Сгенерированные имена с угловыми скобками не являются допустимыми идентификаторами в языке C#. Это сделано намеренно: они не могут конфликтовать с именами, которые вы задаёте в своём коде, и вы не сможете случайно сослаться на них.
Примечание: Атрибут
[AsyncStateMachine] используют отладчики, профилировщики и анализаторы, чтобы связать метод с типом, который фактически содержит его логику.Важное следствие: вызов асинхронного метода приводит к выполнению его тела до первого ожидания (
await) операции, которая ещё не завершилась. Только после этого управление возвращается вызывающей стороне.Сгенерированный тип
Ниже представлены поля конечного автомата:
private struct <SumPageSizesAsync>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder<int> <>t__builder;
public string[] pages;
private int <total>5__2;
private Stopwatch <stopwatch>5__3;
private string[] <>7__wrap3;
private int <>7__wrap4;
private TaskAwaiter<string> <>u__1;
private YieldAwaitable.YieldAwaiter <>u__2;
}
-
<>1__state- указывает точку возобновления выполнения: -1 — выполняется, -2 — завершено; значения 0 и выше соответствуют отдельным операторам await, на которых выполнение может быть приостановлено.-
<>t__builder- объект-построитель, который управляет возвращаемым объектом Task.-
pages - параметр метода; сохраняет своё исходное имя.-
<total>5__2, <sw>5__3 - локальные переменные, которые требуются после выполнения оператора await; отчасти сохраняют свои исходные имена.-
<>7__wrap3, <>7__wrap4 - состояние цикла foreach: массив и текущий индекс. Компилятору они необходимы, чтобы сохранить контекст при выполнении await внутри цикла.-
<>u__1, <>u__2 - по одному слоту на каждый тип объектов ожидания; удерживают их, пока метод находится в приостановленном состоянии.Важно:
1. Локальная переменная становится полем класса только в том случае, если она необходима после оператора
await. Если же переменная используется исключительно в промежутке между двумя операторами await, она остаётся обычной локальной переменной. Поэтому асинхронный метод, сохраняющий большое количество данных, оказывается более «дорогим», чем метод, который этого не делает. Кроме того, перенос части логики в отдельный, синхронный метод позволяет уменьшить размер конечного автомата.2. Слоты для объектов ожидания привязаны к их типу, а не к конкретному оператору
await. Метод LoadAsync возвращает Task<string>, поэтому его «ожидатель» типа TaskAwaiter<string>. А Task.Yield() возвращает YieldAwaitable и его «ожидатель» другого типа. Сколько типов, столько и полей.Окончание следует…
Источник: https://www.meziantou.net/how-the-csharp-compiler-rewrites-an-async-method.htm
👍8
День 2822. #ЗаметкиНаПолях
Таймаут, Который Никогда не Срабатывал
У нас был такой код:
Отправка запроса выполняется с помощью
Выглядит логично. Сможете найти здесь ошибку? Напишите в комментариях, если нашли, не читая дальше.
На самом деле их тут целых две :)
Ошибка 1. Тайм-аут никогда не срабатывает
Конструкция
Небольшой пример для воспроизведения:
Тайм-аут составляет 1 секунду, однако задержка длится все 3 секунды.
Таким образом, в изначальном коде единственным действующим тайм-аутом остается
Ошибка 2. При повторной попытке используется тот же токен
Токен отмены
При выполнении повторной попытки через Polly условие
Из этого также следует, что простого переноса конструкции
Исправление:
Теперь CancellationTokenSource существует на протяжении всей попытки, а каждая попытка получает собственный локальный токен, что исключает перезапись захваченного параметра.
Источник: https://josef.codes/the-timeout-that-never-fired/
Таймаут, Который Никогда не Срабатывал
У нас был такой код:
public async Task<Result<TData>> SendAsync<TData>(
Func<HttpRequestMessage> requestFactory,
RequestOptions? opts = null,
CancellationToken ct = default)
{
opts ??= new RequestOptions();
var policy = opts.Policy ?? DefaultPollyPolicy;
var result = await
policy.ExecuteAndCaptureAsync(async () =>
{
using var request = requestFactory();
if (ct == CancellationToken.None)
{
// Если таймаут не задан, ставим 5 сек
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
ct = cts.Token;
}
using var response =
await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, ct);
// …
});
// …
}
Отправка запроса выполняется с помощью
HttpClient, который обёрнут в политику Polly (по умолчанию настроенную на одну повторную попытку). Если вызывающий код не передаёт токен отмены, мы создаём свой токен с тайм-аутом 5 секунд.Выглядит логично. Сможете найти здесь ошибку? Напишите в комментариях, если нашли, не читая дальше.
На самом деле их тут целых две :)
Ошибка 1. Тайм-аут никогда не срабатывает
Конструкция
using var освобождает ресурс по завершении области видимости, в которой она объявлена. В данном случае такой областью является блок if, а не лямбда-выражение. В результате CancellationTokenSource уничтожается (вызывается метод Dispose()) в момент закрывающей фигурной скобки — ещё до того, как запрос будет отправлен. При вызове Dispose() на CancellationTokenSource его внутренний таймер также уничтожается. Токен, полученный из этого объекта, формально продолжает «работать» (исключения не возникает), но он никогда не перейдёт в отменённое состояние.Небольшой пример для воспроизведения:
var ct = CancellationToken.None;
var sw = Stopwatch.StartNew();
if (ct == CancellationToken.None)
{
using var cts =
new CancellationTokenSource(TimeSpan.FromSeconds(1));
ct = cts.Token;
}
try
{
await Task.Delay(TimeSpan.FromSeconds(3), ct);
Console.WriteLine($"Завершено спустя
{sw.Elapsed.TotalSeconds:F1}сек");
}
catch (Exception e)
{
Console.WriteLine($"{e.GetType().Name} спустя
{sw.Elapsed.TotalSeconds:F1}сек");
}
// Вывод
Завершено спустя 3.0сек
Тайм-аут составляет 1 секунду, однако задержка длится все 3 секунды.
Таким образом, в изначальном коде единственным действующим тайм-аутом остается
HttpClient.Timeout, значение которого по умолчанию составляет 100 секунд.Ошибка 2. При повторной попытке используется тот же токен
Токен отмены
ct является параметром метода, и лямбда-выражение захватывает его. Следовательно, когда лямбда-выражение присваивает ему значение, меняется не локальная копия, а одна и та же переменная при каждом вызове лямбды.При выполнении повторной попытки через Polly условие
ct == CancellationToken.None перестаёт быть истинным. В результате блок if пропускается, и для повторной попытки используется токен из первой попытки — тот самый, который относится к уже уничтоженному объекту CancellationTokenSource.Из этого также следует, что простого переноса конструкции
using var за пределы блока if недостаточно. Это решило бы проблему для первой попытки, но повторная попытка все равно выполнялась бы без тайм-аута.Исправление:
using var timeoutCts = ct == CancellationToken.None
// таймаут 5 сек
? new CancellationTokenSource(TimeSpan.FromSeconds(5))
: null;
var token = timeoutCts?.Token ?? ct;
using var response =
await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token);
Теперь CancellationTokenSource существует на протяжении всей попытки, а каждая попытка получает собственный локальный токен, что исключает перезапись захваченного параметра.
Источник: https://josef.codes/the-timeout-that-never-fired/
👍10👎5
День 2823. #СтатьяДня #AltenarCollab
Heartbeat: как мы создали систему управления системой вместо обычных сигналов
Обычно никто не обращает особого внимания на вспомогательные сервисы вроде мониторинга здоровья (HeartBeat). Они обслуживающие, клиент не видит ошибок в них, да и вообще не имеет к ним доступа. Поэтому (если только у команды не было негативного опыта и – больше того – извлечённых из этого опыта правильных уроков), заботятся об их правильной работе или отказоустойчивости вроде как в последнюю очередь. Есть? Хорошо. Что-то там мониторят? Отлично. А правильно мониторят? Могут своевременно сообщить о сбое до того, как вам начнут звонить разгневанные клиенты? Этот ваш HeartBeat, он вообще, вот прямо сейчас, работает?
А что, если система представляет собой высоконагруженный поток данных в реальном времени, неполадки в одном из сервисов обнаруживаются клиентами также в реальном времени, и устранить их желательно не завтра, после утреннего кофе, а также в реальном времени?
Александр Илларионов из Altenar в своей статье на Хабре рассказывает, как они разрабатывали такую систему, которая могла не просто мониторить real-time сервисы, но и своевременно поднимать тревогу и даже предпринимать некоторые действия, вроде отключения сбоящего сервиса.
Статье немного не хватает последовательности, и автор упускает из повествования некоторые детали. Очевидно, он «в контексте» и понимает, что, зачем и почему. Мне этого понимания в некоторых местах не хватало. Например:
При этом ранее не упоминалось, что значит заблокировать трафик и чем это отличается от отключения.
Но в принципе тема довольно интересная. Хотелось бы, чтобы Александр её развил и рассказал поподробнее. Возможно, в серии статей. Какие конкретно сервисы мониторинга и управления были созданы, как распределялись между ними обязанности и, возможно, детали их реализации.
Heartbeat: как мы создали систему управления системой вместо обычных сигналов
Обычно никто не обращает особого внимания на вспомогательные сервисы вроде мониторинга здоровья (HeartBeat). Они обслуживающие, клиент не видит ошибок в них, да и вообще не имеет к ним доступа. Поэтому (если только у команды не было негативного опыта и – больше того – извлечённых из этого опыта правильных уроков), заботятся об их правильной работе или отказоустойчивости вроде как в последнюю очередь. Есть? Хорошо. Что-то там мониторят? Отлично. А правильно мониторят? Могут своевременно сообщить о сбое до того, как вам начнут звонить разгневанные клиенты? Этот ваш HeartBeat, он вообще, вот прямо сейчас, работает?
А что, если система представляет собой высоконагруженный поток данных в реальном времени, неполадки в одном из сервисов обнаруживаются клиентами также в реальном времени, и устранить их желательно не завтра, после утреннего кофе, а также в реальном времени?
Александр Илларионов из Altenar в своей статье на Хабре рассказывает, как они разрабатывали такую систему, которая могла не просто мониторить real-time сервисы, но и своевременно поднимать тревогу и даже предпринимать некоторые действия, вроде отключения сбоящего сервиса.
Статье немного не хватает последовательности, и автор упускает из повествования некоторые детали. Очевидно, он «в контексте» и понимает, что, зачем и почему. Мне этого понимания в некоторых местах не хватало. Например:
Именно поэтому для нас принципиально важна последовательность в этих действиях: сначала заблокировать трафик, а потом уже отключить его и сообщить клиенту о проблеме.
При этом ранее не упоминалось, что значит заблокировать трафик и чем это отличается от отключения.
Но в принципе тема довольно интересная. Хотелось бы, чтобы Александр её развил и рассказал поподробнее. Возможно, в серии статей. Какие конкретно сервисы мониторинга и управления были созданы, как распределялись между ними обязанности и, возможно, детали их реализации.
👍3
День 2824. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
51. Стратегии интеграционного тестирования
«Опишите ваш подход к интеграционному тестированию .NET-приложений. Расскажите, как вы планируете, реализуете и автоматизируете такие тесты, а также какие инструменты или фреймворки при этом используете».
Хороший ответ
Интеграционное тестирование в .NET-приложениях предполагает объединение модулей и их проверку как единого целого, чтобы убедиться, что взаимодействие между ними происходит ожидаемым образом. Мой подход к интеграционному тестированию включает следующие этапы:
1. Определение сценариев тестирования: выявление критических точек интеграции в приложении, где происходит взаимодействие между различными модулями, сервисами или внешними системами.
2. Подготовка тестовых данных: планирование и создание данных, необходимых для каждой точки интеграции, чтобы имитировать реальные сценарии использования.
3. Настройка тестовой среды: создание отдельной среды, максимально приближённой к проду, что позволяет избежать расхождений между результатами тестов и реальным поведением системы.
Для написания тестов можно использовать xUnit, а также пакет Microsoft.AspNetCore.TestHost, который предоставляет тестовый сервер для запуска приложения. Пример организации теста:
Автоматизация интеграционного тестирования
- Непрерывная интеграция: выполнение интеграционных тестов запускаются в рамках CI-конвейера, чтобы гарантировать их запуск при каждом значимом изменении кодовой базы.
- Отчётность: подробное протоколирование и формирование отчётов о результатах в CI-конвейере позволит оперативно выявлять сбои и устранять их причины.
Лучшие практики
- Изоляция сценариев тестирования: несмотря на то что интеграционные тесты фокусируются на взаимодействии между компонентами, важно изолировать сценарии тестирования, привязывая их к конкретным точкам интеграции. Это упрощает поиск неисправностей и повышает наглядность тестов.
- Использование заглушек для внешних зависимостей, чтобы имитировать внешние системы (см. подробнее о заглушках). Это уменьшает количество зависимостей, а также повышает надёжность и скорость выполнения тестов.
- Мониторинг производительности: важно следить за тем, сколько времени занимают интеграционные тесты, чтобы они не стали «узким местом» в процессе разработки.
Такой подход гарантирует, что интеграционные тесты будут всесторонними и эффективными, а также внесут реальный вклад в общее качество приложения.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
51. Стратегии интеграционного тестирования
«Опишите ваш подход к интеграционному тестированию .NET-приложений. Расскажите, как вы планируете, реализуете и автоматизируете такие тесты, а также какие инструменты или фреймворки при этом используете».
Хороший ответ
Интеграционное тестирование в .NET-приложениях предполагает объединение модулей и их проверку как единого целого, чтобы убедиться, что взаимодействие между ними происходит ожидаемым образом. Мой подход к интеграционному тестированию включает следующие этапы:
1. Определение сценариев тестирования: выявление критических точек интеграции в приложении, где происходит взаимодействие между различными модулями, сервисами или внешними системами.
2. Подготовка тестовых данных: планирование и создание данных, необходимых для каждой точки интеграции, чтобы имитировать реальные сценарии использования.
3. Настройка тестовой среды: создание отдельной среды, максимально приближённой к проду, что позволяет избежать расхождений между результатами тестов и реальным поведением системы.
Для написания тестов можно использовать xUnit, а также пакет Microsoft.AspNetCore.TestHost, который предоставляет тестовый сервер для запуска приложения. Пример организации теста:
public class IntegrationTests : IDisposable
{
private readonly TestServer _server;
private readonly HttpClient _client;
public IntegrationTests()
{
var builder = WebApplication.CreateBuilder();
// … Добавляем сервисы …
builder.Services.AddControllers();
_server = new TestServer(
new WebHostBuilder().UseStartup<Startup>());
_client = _server.CreateClient();
}
[Fact]
public async Task TestEndpointReturnsSuccess()
{
var response = await
_client.GetAsync("/api/values");
response.EnsureSuccessStatusCode();
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
public void Dispose()
{
_client.Dispose();
_server.Dispose();
}
}
Автоматизация интеграционного тестирования
- Непрерывная интеграция: выполнение интеграционных тестов запускаются в рамках CI-конвейера, чтобы гарантировать их запуск при каждом значимом изменении кодовой базы.
- Отчётность: подробное протоколирование и формирование отчётов о результатах в CI-конвейере позволит оперативно выявлять сбои и устранять их причины.
Лучшие практики
- Изоляция сценариев тестирования: несмотря на то что интеграционные тесты фокусируются на взаимодействии между компонентами, важно изолировать сценарии тестирования, привязывая их к конкретным точкам интеграции. Это упрощает поиск неисправностей и повышает наглядность тестов.
- Использование заглушек для внешних зависимостей, чтобы имитировать внешние системы (см. подробнее о заглушках). Это уменьшает количество зависимостей, а также повышает надёжность и скорость выполнения тестов.
- Мониторинг производительности: важно следить за тем, сколько времени занимают интеграционные тесты, чтобы они не стали «узким местом» в процессе разработки.
Такой подход гарантирует, что интеграционные тесты будут всесторонними и эффективными, а также внесут реальный вклад в общее качество приложения.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍4👎2
День 2828. #ЗаметкиНаПолях #Logging
Настраиваем Логирование в ASP.NET. Часть 1
Настройка стандартных провайдеров логирования в ASP.NET Core через конфигурацию
После развёртывания нового приложения в рабочей среде мы обнаружили, что в журнал событий (EventLog) записывается множество лишних сообщений. Поскольку мы не настраивали провайдер логирования для EventLog в нашем приложении, это стало для нас неожиданностью. Оказалось, что при создании нового приложения ASP.NET Core с помощью
Возникает вопрос: какие именно провайдеры включены и как отключить ненужные?
Какие провайдеры включены по умолчанию?
Стандартный построитель (builder) регистрирует следующие провайдеры:
- Console (консоль),
- Debug (отладка),
- EventSource,
- EventLog (только для Windows).
Вы можете убедиться в этом самостоятельно, проверив зарегистрированные службы
Подход на основе кода
Очевидный путь — начать с чистого листа и добавить обратно только то, что нужно:
Это работает, но настройки «зашиты» в код. Если вы хотите изменять поведение системы в зависимости от окружения без выполнения нового развёртывания, потребуется другое решение.
Подход на основе конфигурации
Конфигурация логирования считывается из раздела «Logging» в файле
Что мы здесь сделали?
1. Для Debug и EventSource задано значение None, поэтому они больше ничего не записывают в лог.
2. Console продолжает работать, но фиксирует только сообщения уровня Warning и выше.
3. Для всех остальных провайдеров действуют общие настройки из раздела LogLevel.
Примечание: установка уровня None не удаляет провайдер. Он остаётся зарегистрированным, но отфильтровывает все сообщения. Если вы хотите полностью удалить провайдер, используйте метод ClearProviders().
Если для провайдера задаётся свой раздел
Изменение без правки кода
Поскольку это обычная конфигурация, её также можно переопределить с помощью переменных окружения. В качестве разделителя используйте двойное подчёркивание:
Это удобно при использовании Azure Container Apps или Kubernetes: можно изменить уровень ведения журнала для отдельного провайдера, не пересобирая образ.
Совет: названия провайдеров нечувствительны к регистру, но должны совпадать с их псевдонимами: Console, Debug, EventSource и EventLog. Опечатки игнорируются без вывода сообщений об ошибках, поэтому, если ничего не меняется, в первую очередь проверьте правильность написания.
Источник: https://bartwullems.blogspot.com/2026/10/tuning-default-logging-providers-in.html
Настраиваем Логирование в ASP.NET. Часть 1
Настройка стандартных провайдеров логирования в ASP.NET Core через конфигурацию
После развёртывания нового приложения в рабочей среде мы обнаружили, что в журнал событий (EventLog) записывается множество лишних сообщений. Поскольку мы не настраивали провайдер логирования для EventLog в нашем приложении, это стало для нас неожиданностью. Оказалось, что при создании нового приложения ASP.NET Core с помощью
WebApplication.CreateBuilder активируется целый набор провайдеров логирования.Возникает вопрос: какие именно провайдеры включены и как отключить ненужные?
Какие провайдеры включены по умолчанию?
Стандартный построитель (builder) регистрирует следующие провайдеры:
- Console (консоль),
- Debug (отладка),
- EventSource,
- EventLog (только для Windows).
Вы можете убедиться в этом самостоятельно, проверив зарегистрированные службы
ILoggerProvider:var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var providers = app.Services.GetServices<ILoggerProvider>();
foreach (var provider in providers)
Console.WriteLine(provider.GetType().Name);
// Вывод в Windows:
// ConsoleLoggerProvider
// DebugLoggerProvider
// EventSourceLoggerProvider
// EventLogLoggerProvider
Подход на основе кода
Очевидный путь — начать с чистого листа и добавить обратно только то, что нужно:
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
Это работает, но настройки «зашиты» в код. Если вы хотите изменять поведение системы в зависимости от окружения без выполнения нового развёртывания, потребуется другое решение.
Подход на основе конфигурации
Конфигурация логирования считывается из раздела «Logging» в файле
appsettings.json. Помимо общего раздела «LogLevel», можно добавить отдельный раздел для каждого провайдера, используя его имя в качестве ключа:{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
},
"Debug": {
"LogLevel": {
"Default": "None"
}
},
"EventSource": {
"LogLevel": {
"Default": "None"
}
},
"Console": {
"LogLevel": {
"Default": "Warning"
}
}
}
}Что мы здесь сделали?
1. Для Debug и EventSource задано значение None, поэтому они больше ничего не записывают в лог.
2. Console продолжает работать, но фиксирует только сообщения уровня Warning и выше.
3. Для всех остальных провайдеров действуют общие настройки из раздела LogLevel.
Примечание: установка уровня None не удаляет провайдер. Он остаётся зарегистрированным, но отфильтровывает все сообщения. Если вы хотите полностью удалить провайдер, используйте метод ClearProviders().
Если для провайдера задаётся свой раздел
LogLevel, общие правила из секции Logging:LogLevel перестают к нему применяться. В приведённом выше примере правило Microsoft.AspNetCore: Warning никак не влияет на провайдер Console. Если вы хотите, чтобы оно действовало и для него, добавьте его повторно:"Console": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
}Изменение без правки кода
Поскольку это обычная конфигурация, её также можно переопределить с помощью переменных окружения. В качестве разделителя используйте двойное подчёркивание:
Logging__Debug__LogLevel__Default=None
Logging__Console__LogLevel__Default=Warning
Это удобно при использовании Azure Container Apps или Kubernetes: можно изменить уровень ведения журнала для отдельного провайдера, не пересобирая образ.
Совет: названия провайдеров нечувствительны к регистру, но должны совпадать с их псевдонимами: Console, Debug, EventSource и EventLog. Опечатки игнорируются без вывода сообщений об ошибках, поэтому, если ничего не меняется, в первую очередь проверьте правильность написания.
Источник: https://bartwullems.blogspot.com/2026/10/tuning-default-logging-providers-in.html
👍4
День 2829. #ЗаметкиНаПолях #Logging
Настраиваем Логирование в ASP.NET. Часть 2
Очищаем старые лог-файлы в Serilog
Мы получили оповещение от команды эксплуатации: на одном из серверов заканчивается место на диске. Причиной стал лог-файл, который рос настолько быстро, что занял всё доступное пространство. Команда разработки не настроила механизм очистки, поэтому файл продолжал бесконтрольно увеличиваться. В приложении уже использовался Serilog. Давайте разберёмся, какие существуют способы ограничить объём логов.
Конфигурация по умолчанию
Самый простой способ записи логов в файл с помощью Serilog выглядит так:
Это работает, но все данные записываются в один файл. По умолчанию ротация файлов не выполняется, и ничего не удаляется.
Примечание: это не совсем так. Для записи в файл по умолчанию установлен лимит размера файла (
Ротация файлов
Первый шаг — обеспечить создание нового файла через определённые промежутки времени. Это делается с помощью параметра
Примечание: в более старых версиях Serilog существовал отдельный приёмник — RollingFile. Теперь же этот функционал полностью встроен непосредственно в приёмник File.
Serilog добавляет дату в имя файла, в результате чего создаются файлы вида
Доступны следующие интервалы:
- Infinite (бесконечный): ротация не выполняется (по умолчанию),
- Year (год),
- Month (месяц),
- Day (день),
- Hour (час),
- Minute (минута).
Сама по себе ротация не удаляет старые файлы — вместо одного большого файла у вас просто появляется множество отдельных. Для управления этим процессом предусмотрены параметры хранения.
Ограничение количества файлов
Параметр
При ежедневной ротации сохраняются логи примерно за две недели. По умолчанию это значение составляет 31. Передайте
Ограничение срока давности файлов
В новых версиях
Если задать оба параметра одновременно, будут действовать оба правила.
Что выбрать? Ограничение по количеству файлов позволяет предсказать объём занимаемого дискового пространства, а ограничение по времени — глубину хранения истории. Часто имеет смысл использовать оба параметра.
Ограничение размера отдельного файла
Именно здесь кроется причина возникшего у нас инцидента. Ежедневная ротация не помогает, если за одни сутки генерируется 40ГБ логов. Поэтому необходимо также ограничить размер каждого отдельного файла:
-
-
При создании нового файла из-за превышения лимита размера Serilog добавляет к имени порядковый номер:
Теперь можно рассчитать максимальный объём дискового пространства, который может быть занят:
Примечание 1: параметр
Примечание 2: старые файлы удаляются только в момент создания нового файла. Если ротация файлов в вашем приложении не происходит, то очистка старых файлов также выполняться не будет.
Настройки в appsettings.json
Конечно, можно задать эти параметры непосредственно в коде C#, но, скорее всего, вы захотите иметь возможность изменять их без повторного развёртывания приложения. При использовании пакета
И в Program.cs:
Другие опции
Стоит также обратить внимание на некоторые другие параметры записи в файл:
-
-
-
-
-
-
Совет: Ротация и политики хранения данных лишь защищают ваш диск, но не устраняют первопричину проблемы. В нашем случае основной объём логов создавался компонентами из пространства имен
Источник: https://bartwullems.blogspot.com/2026/10/cleaning-up-old-log-files-with-serilog.html
Настраиваем Логирование в ASP.NET. Часть 2
Очищаем старые лог-файлы в Serilog
Мы получили оповещение от команды эксплуатации: на одном из серверов заканчивается место на диске. Причиной стал лог-файл, который рос настолько быстро, что занял всё доступное пространство. Команда разработки не настроила механизм очистки, поэтому файл продолжал бесконтрольно увеличиваться. В приложении уже использовался Serilog. Давайте разберёмся, какие существуют способы ограничить объём логов.
Конфигурация по умолчанию
Самый простой способ записи логов в файл с помощью Serilog выглядит так:
Log.Logger = new LoggerConfiguration()
.WriteTo.File("logs/log.txt")
.CreateLogger();
Это работает, но все данные записываются в один файл. По умолчанию ротация файлов не выполняется, и ничего не удаляется.
Примечание: это не совсем так. Для записи в файл по умолчанию установлен лимит размера файла (
fileSizeLimitBytes), равный 1ГБ. Когда файл достигает этого предела, Serilog просто перестает записывать события лога — без каких-либо уведомлений. В итоге диск не переполняется, но вы теряете логи. Что, впрочем, не лучше.Ротация файлов
Первый шаг — обеспечить создание нового файла через определённые промежутки времени. Это делается с помощью параметра
rollingInterval:Log.Logger = new LoggerConfiguration()
.WriteTo.File("logs/log-.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
Примечание: в более старых версиях Serilog существовал отдельный приёмник — RollingFile. Теперь же этот функционал полностью встроен непосредственно в приёмник File.
Serilog добавляет дату в имя файла, в результате чего создаются файлы вида
log-20261029.txt.Доступны следующие интервалы:
- Infinite (бесконечный): ротация не выполняется (по умолчанию),
- Year (год),
- Month (месяц),
- Day (день),
- Hour (час),
- Minute (минута).
Сама по себе ротация не удаляет старые файлы — вместо одного большого файла у вас просто появляется множество отдельных. Для управления этим процессом предусмотрены параметры хранения.
Ограничение количества файлов
Параметр
retainedFileCountLimit определяет, сколько файлов логов будет сохранено (включая текущий):….WriteTo.File(
"logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 14)
При ежедневной ротации сохраняются логи примерно за две недели. По умолчанию это значение составляет 31. Передайте
null, если хотите сохранить всё (хотя вряд ли вам это нужно).Ограничение срока давности файлов
В новых версиях
Serilog.Sinks.File также предусмотрен параметр retainedFileTimeLimit. Вместо подсчёта количества файлов вы определяете, как долго файл может храниться:….WriteTo.File(
"logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileTimeLimit: TimeSpan.FromDays(7))
Если задать оба параметра одновременно, будут действовать оба правила.
Что выбрать? Ограничение по количеству файлов позволяет предсказать объём занимаемого дискового пространства, а ограничение по времени — глубину хранения истории. Часто имеет смысл использовать оба параметра.
Ограничение размера отдельного файла
Именно здесь кроется причина возникшего у нас инцидента. Ежедневная ротация не помогает, если за одни сутки генерируется 40ГБ логов. Поэтому необходимо также ограничить размер каждого отдельного файла:
….WriteTo.File(
"logs/log-.txt",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 100 * 1024 * 1024,
rollOnFileSizeLimit: true,
retainedFileCountLimit: 14)
-
fileSizeLimitBytes: максимальный размер одного файла (в данном случае — 100 МБ);-
rollOnFileSizeLimit: если установлено true, то при достижении лимита размера Serilog создаёт новый файл, а не отбрасывает события.При создании нового файла из-за превышения лимита размера Serilog добавляет к имени порядковый номер:
log-20261029.txt, log-20261020_001.txt, log-20261029_002.txt и т.д.Теперь можно рассчитать максимальный объём дискового пространства, который может быть занят:
fileSizeLimitBytes × retainedFileCountLimit. В нашем примере это 100 МБ × 14 = 1,4 ГБ.Примечание 1: параметр
retainedFileCountLimit задаёт количество файлов, а не дней. При ротации по размеру в периоды высокой активности может создаваться большое количество файлов, из-за чего старые файлы будут удаляться быстрее, чем вы ожидаете. Если вам необходимо гарантированное хранение логов за определённый период, используйте retainedFileTimeLimit.Примечание 2: старые файлы удаляются только в момент создания нового файла. Если ротация файлов в вашем приложении не происходит, то очистка старых файлов также выполняться не будет.
Настройки в appsettings.json
Конечно, можно задать эти параметры непосредственно в коде C#, но, скорее всего, вы захотите иметь возможность изменять их без повторного развёртывания приложения. При использовании пакета
Serilog.Settings.Configuration та же конфигурация будет выглядеть следующим образом:{
"Serilog": {
"Using": [ "Serilog.Sinks.File" ],
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "File",
"Args": {
"path": "logs/log-.txt",
"rollingInterval": "Day",
"fileSizeLimitBytes": 104857600,
"rollOnFileSizeLimit": true,
"retainedFileCountLimit": 14,
"retainedFileTimeLimit": "7.00:00:00"
}
}
]
}
}И в Program.cs:
builder.Host.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration));
Другие опции
Стоит также обратить внимание на некоторые другие параметры записи в файл:
-
shared: позволяет нескольким процессам записывать данные в один и тот же файл. Полезно в определённых сценариях хостинга, но снижает производительность.-
buffered: выполняет буферизацию записей в оперативной памяти, а не записывает каждое событие на диск немедленно. Это работает быстрее, но в случае сбоя процесса можно потерять часть событий.-
flushToDiskInterval: определяет, как часто буферизованные данные сбрасываются на диск.-
restrictedToMinimumLevel: позволяет записывать в файл только те события, уровень важности которых соответствует заданному или превышает его.-
outputTemplate: позволяет настроить формат каждой строки. Более короткий шаблон означает меньший размер файлов.-
hooks: реализация интерфейса FileLifecycleHooks, позволяющая выполнять пользовательскую логику при открытии или закрытии файла (например, для сжатия ротированных файлов с помощью GZip).Совет: Ротация и политики хранения данных лишь защищают ваш диск, но не устраняют первопричину проблемы. В нашем случае основной объём логов создавался компонентами из пространства имен
Microsoft.*. Повышение минимального уровня логирования для этих пространств имен (см. часть 1) позволило сократить размер логов гораздо эффективнее, чем любые настройки хранения.Источник: https://bartwullems.blogspot.com/2026/10/cleaning-up-old-log-files-with-serilog.html
👍2
День 2830. #ЗаметкиНаПолях #Logging
Настраиваем Логирование в ASP.NET. Часть 3
Использование FileLifecycleHooks в Serilog
В предыдущей части мы рассмотрели, как настроить ротацию и политику хранения логов, чтобы предотвратить переполнение диска бесконтрольно растущими файлами. Это решает проблему, но возникает вопрос: а что, если вы хотите выполнить какие-либо действия с файлами до того, как они будут удалены?
Возможно, вы хотите сжать файлы, чтобы сэкономить место, или же требования нормативных актов обязывают вас архивировать их. В
Что это?
Механизм «хуков жизненного цикла» позволяет выполнять ваш код в определённые моменты существования лог-файла. Для этого нужно создать класс, наследующий от абстрактного класса
-
-
У обоих методов есть реализация по умолчанию, которая ничего не делает, поэтому вы можете переопределять только то, что вам нужно. Затем передайте свой «хук» в приёмник через параметр
Примечание: в более новых версиях
Пример 1: сжатие логов
Поскольку логи представляют собой текстовые данные, они отлично поддаются сжатию, что позволяет существенно сократить объём занимаемого ими дискового пространства.
Этот хук оборачивает поток, в который пишет Serilog, в GZipStream. Всё, что записывается в файл, сжимается «на лету». Использование:
Обратите внимание на расширение .gz в пути. Без него вы получите сжатые файлы, которые будут выглядеть как обычный текст.
Примечание 1: Поток gzip, который не был корректно закрыт, считается неполным. В случае сбоя процесса можно потерять последние события, записывавшиеся в текущий файл. Кроме того, не используйте этот параметр вместе с
Примечание 2: ограничение по размеру проверяется по количеству байтов, записываемых в файл, то есть по размеру уже сжатых данных. Таким образом, лимит в 100МБ означает 100МБ данных в формате gzip, что соответствует гораздо большему числу событий в логах. Учитывайте это при расчёте дискового пространства.
Пример 2: архивирование файлов перед их удалением
Иногда удаление файла через 14 дней допустимо для локального хранилища, но при этом возникает необходимость сохранить логи где-то ещё на более длительный срок. Для этого предназначен параметр
В этом примере файл копируется в общую сетевую папку. В реальных условиях это также может быть загрузка в Azure Blob Storage:
Примечание: хук выполняется синхронно в рамках конвейера логирования — в тот момент, когда приёмник переключается на новый файл. Позаботьтесь о том, чтобы он работал быстро: медленная операция загрузки заблокирует процесс логирования. В случае возникновения ошибки исключение в самом приложении выброшено не будет. Чтобы узнать, что именно пошло не так, включите SelfLog:
Пример 3: добавление заголовка в начало каждого файла
Это удобно, если файл в дальнейшем будет обрабатываться другим инструментом — например, в случае с CSV-файлом, содержащим названия столбцов:
Проверка
Не обязательно писать всё самостоятельно
Для решения описанных выше задач существуют готовые пакеты, созданные сообществом — например,
Когда использовать?
- вы храните лог-файлы на сервере, а дисковое пространство ограничено;
- требования регуляторов или правила аудита обязывают вас хранить логи дольше, чем предусмотрено вашей стандартной политикой хранения;
- другие инструменты используют эти файлы и ожидают определённый формат данных;
- необходимо шифровать лог-файлы, хранящиеся на диске.
Если же логи уже передаются в централизованную систему (например, Seq, Elasticsearch или Application Insights), использование хуков, скорее всего, будет излишним. В такой ситуации локальный файл служит лишь резервным хранилищем, и для него достаточно настроить ротацию с ограничением срока хранения.
Источник: https://bartwullems.blogspot.com/2026/10/using-filelifecyclehooks-in-serilog.html
Настраиваем Логирование в ASP.NET. Часть 3
Использование FileLifecycleHooks в Serilog
В предыдущей части мы рассмотрели, как настроить ротацию и политику хранения логов, чтобы предотвратить переполнение диска бесконтрольно растущими файлами. Это решает проблему, но возникает вопрос: а что, если вы хотите выполнить какие-либо действия с файлами до того, как они будут удалены?
Возможно, вы хотите сжать файлы, чтобы сэкономить место, или же требования нормативных актов обязывают вас архивировать их. В
Serilog.Sinks.File для этого предусмотрена точка расширения — FileLifecycleHooks.Что это?
Механизм «хуков жизненного цикла» позволяет выполнять ваш код в определённые моменты существования лог-файла. Для этого нужно создать класс, наследующий от абстрактного класса
FileLifecycleHooks, и переопределить один или оба из следующих методов:-
OnFileOpened: вызывается, когда приемник открывает новый лог-файл. Вы получаете доступ к базовому потоку и можете вернуть свой поток, который будет его «оборачивать».-
OnFileDeleting: вызывается непосредственно перед тем, как файл будет удалён в соответствии с политикой хранения.У обоих методов есть реализация по умолчанию, которая ничего не делает, поэтому вы можете переопределять только то, что вам нужно. Затем передайте свой «хук» в приёмник через параметр
hooks.Примечание: в более новых версиях
Serilog.Sinks.File появилась перегрузка метода OnFileOpened, предоставляющая путь к файлу. Хук OnFileDeleting был добавлен одновременно с параметром retainedFileTimeLimit, поэтому убедитесь, что вы используете актуальную версию библиотеки.Пример 1: сжатие логов
Поскольку логи представляют собой текстовые данные, они отлично поддаются сжатию, что позволяет существенно сократить объём занимаемого ими дискового пространства.
using System.IO.Compression;
using System.Text;
using Serilog.Sinks.File;
public class GZipHooks : FileLifecycleHooks
{
public override Stream OnFileOpened(
Stream stream,
Encoding enc)
=> new GZipStream(
stream,
CompressionLevel.Fastest);
}
Этот хук оборачивает поток, в который пишет Serilog, в GZipStream. Всё, что записывается в файл, сжимается «на лету». Использование:
Log.Logger = new LoggerConfiguration()
.WriteTo.File(
"logs/log-.gz",
rollingInterval: RollingInterval.Day,
fileSizeLimitBytes: 100 * 1024 * 1024,
rollOnFileSizeLimit: true,
retainedFileCountLimit: 14,
hooks: new GZipHooks())
.CreateLogger();
Обратите внимание на расширение .gz в пути. Без него вы получите сжатые файлы, которые будут выглядеть как обычный текст.
Примечание 1: Поток gzip, который не был корректно закрыт, считается неполным. В случае сбоя процесса можно потерять последние события, записывавшиеся в текущий файл. Кроме того, не используйте этот параметр вместе с
shared: true. Попытка записи нескольких процессов в один и тот же сжатый поток ни к чему хорошему не приведёт.Примечание 2: ограничение по размеру проверяется по количеству байтов, записываемых в файл, то есть по размеру уже сжатых данных. Таким образом, лимит в 100МБ означает 100МБ данных в формате gzip, что соответствует гораздо большему числу событий в логах. Учитывайте это при расчёте дискового пространства.
Пример 2: архивирование файлов перед их удалением
Иногда удаление файла через 14 дней допустимо для локального хранилища, но при этом возникает необходимость сохранить логи где-то ещё на более длительный срок. Для этого предназначен параметр
OnFileDeleting:public class ArchiveBeforeDeleteHooks
: FileLifecycleHooks
{
private string _archFolder;
public ArchiveBeforeDeleteHooks(string archFolder)
{
_archFolder = archFolder;
Directory.CreateDirectory(_archFolder);
}
public override void OnFileDeleting(string path)
{
var target = Path.Combine(
_archFolder, Path.GetFileName(path));
File.Copy(path, target, overwrite: true);
}
}
В этом примере файл копируется в общую сетевую папку. В реальных условиях это также может быть загрузка в Azure Blob Storage:
….WriteTo.File(
"logs/log-.txt",
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 14,
hooks: new ArchiveBeforeDeleteHooks(@"\\fileserver\archive\myapp"))
Примечание: хук выполняется синхронно в рамках конвейера логирования — в тот момент, когда приёмник переключается на новый файл. Позаботьтесь о том, чтобы он работал быстро: медленная операция загрузки заблокирует процесс логирования. В случае возникновения ошибки исключение в самом приложении выброшено не будет. Чтобы узнать, что именно пошло не так, включите SelfLog:
Serilog.Debugging.SelfLog.Enable(Console.Error);
Пример 3: добавление заголовка в начало каждого файла
Это удобно, если файл в дальнейшем будет обрабатываться другим инструментом — например, в случае с CSV-файлом, содержащим названия столбцов:
public class HeaderHooks : FileLifecycleHooks
{
private readonly string _header;
public HeaderHooks(string header)
=> _header = header;
public override Stream OnFileOpened(
Stream stream,
Encoding enc)
{
if (stream.Length == 0)
{
var bytes = enc.GetBytes(_header + Environment.NewLine);
stream.Write(bytes, 0, bytes.Length);
}
return base.OnFileOpened(stream, enc);
}
}
Проверка
Length == 0 важна. Один и тот же файл может быть открыт повторно после перезапуска приложения, и вы не хотите, чтобы в середине файла появился ещё один заголовок.Не обязательно писать всё самостоятельно
Для решения описанных выше задач существуют готовые пакеты, созданные сообществом — например,
Serilog.Sinks.File.Archive и Serilog.Sinks.File.Header. Прежде чем приступать к написанию собственного решения, проверьте, нет ли уже готового инструмента, который выполняет нужные вам функции.Когда использовать?
- вы храните лог-файлы на сервере, а дисковое пространство ограничено;
- требования регуляторов или правила аудита обязывают вас хранить логи дольше, чем предусмотрено вашей стандартной политикой хранения;
- другие инструменты используют эти файлы и ожидают определённый формат данных;
- необходимо шифровать лог-файлы, хранящиеся на диске.
Если же логи уже передаются в централизованную систему (например, Seq, Elasticsearch или Application Insights), использование хуков, скорее всего, будет излишним. В такой ситуации локальный файл служит лишь резервным хранилищем, и для него достаточно настроить ротацию с ограничением срока хранения.
Источник: https://bartwullems.blogspot.com/2026/10/using-filelifecyclehooks-in-serilog.html
👍3
День 2831. #ЗаметкиНаПолях
Первичные Конструкторы — Не Просто Синтаксический Сахар
Знаете ли вы, что переход от «обычного» конструктора к первичному может изменить, момент, когда инициализируются поля? Разберёмся, почему это важно.
Проблема
Представьте, что у вас есть базовый класс, который выполняет определённые действия в своем конструкторе, причем эти действия включают вызов виртуального члена класса:
Да-да, вызов виртуальных членов внутри конструктора — это код «с душком». Но в реальной жизни всё не всегда так однозначно. В bUnit базовый конструктор класса WaitForHelper запускает таймер, а по истечении заданного времени таймер обращается к виртуальному свойству TimeoutErrorMessage. Проблема та же, просто она «спрятана» за таймером и другим потоком выполнения.
А теперь создадим производный класс с обычным конструктором:
И такой же с первичным конструктором:
Они выглядят одинаково. Выполним код:
Что делает компилятор
Если взглянуть на преобразованный код (можно на sharplab.io), разница становится очевидной. Вот класс Regular:
Лямбда-выражение захватывает параметр в замыкание (вспомогательный класс DisplayClass), поэтому метод
А вот класс
Поскольку переменная
В контексте bUnit это означает следующее: при использовании «классического» конструктора, если таймер срабатывал до выполнения тела конструктора производного класса, возникала ошибка с сообщением «did not result in any matching element(s)» (не найдено никаких подходящих элементов) вместо «did not result in exactly matching element(s)» (не найдено точно подходящих элементов). При использовании первичного конструктора значение доступно сразу.
Небольшой нюанс
Сгенерированное поле
Источник: https://steven-giesel.com/blogPost/a3c741d3-f261-4bda-8da6-bffbbbd7aa5a/primary-constructors-are-more-than-syntactic-sugar
Первичные Конструкторы — Не Просто Синтаксический Сахар
Знаете ли вы, что переход от «обычного» конструктора к первичному может изменить, момент, когда инициализируются поля? Разберёмся, почему это важно.
Проблема
Представьте, что у вас есть базовый класс, который выполняет определённые действия в своем конструкторе, причем эти действия включают вызов виртуального члена класса:
abstract class Base
{
protected Base(Func<int?> check)
{
Console.WriteLine(
$"{GetType().Name}: check()={check()},Message:{Message}");
}
protected abstract string Message { get; }
}
Да-да, вызов виртуальных членов внутри конструктора — это код «с душком». Но в реальной жизни всё не всегда так однозначно. В bUnit базовый конструктор класса WaitForHelper запускает таймер, а по истечении заданного времени таймер обращается к виртуальному свойству TimeoutErrorMessage. Проблема та же, просто она «спрятана» за таймером и другим потоком выполнения.
А теперь создадим производный класс с обычным конструктором:
class Regular : Base
{
private readonly int? count;
public Regular(int? count)
: base(() => count)
{
this.count = count;
}
protected override string Message
=> $"count={count?.ToString() ?? "null"}";
}
И такой же с первичным конструктором:
class Primary(int? count) : Base(() => count)
{
protected override string Message
=> $"count={count?.ToString() ?? "null"}";
}
Они выглядят одинаково. Выполним код:
new Regular(3);
new Primary(3);
// Вывод:
Regular: check()=3,Message:count=null
Primary: check()=3,Message:count=3
Что делает компилятор
Если взглянуть на преобразованный код (можно на sharplab.io), разница становится очевидной. Вот класс Regular:
public Regular(Nullable<int> count)
{
var obj = new <>c__DisplayClass1_0();
obj.count = count;
base..ctor(new Func<int?>(obj.<.ctor>b__0));
this.count = obj.count;
}
Лямбда-выражение захватывает параметр в замыкание (вспомогательный класс DisplayClass), поэтому метод
check() видит корректное значение. Однако полю this.count присваивается значение только после того, как завершит работу конструктор базового класса. Всё, что происходит в конструкторе базового класса до этого момента, «видит» лишь null.А вот класс
Primary:public Primary(Nullable<int> count)
{
<count>P = count;
base..ctor(new Func<int?>(<.ctor>b__0_0));
}
Поскольку переменная
count используется внутри члена класса (в данном случае — в свойстве Message), компилятор сохраняет её значение в скрытом поле <count>P. Это поле инициализируется до вызова конструктора базового класса. Такой порядок соответствует работе инициализаторов полей, которые в C# также выполняются до вызова base(…).В контексте bUnit это означает следующее: при использовании «классического» конструктора, если таймер срабатывал до выполнения тела конструктора производного класса, возникала ошибка с сообщением «did not result in any matching element(s)» (не найдено никаких подходящих элементов) вместо «did not result in exactly matching element(s)» (не найдено точно подходящих элементов). При использовании первичного конструктора значение доступно сразу.
Небольшой нюанс
Сгенерированное поле
<count>P не является readonly. Вы можете изменить значение count внутри класса, и компилятор не будет препятствовать этому. Если вам необходима гарантия неизменяемости, следует объявить поле самостоятельно — инициализатор поля (например, private readonly int? count = count;) также сработает до вызова конструктора базового класса. Однако избегайте одновременного использования параметра и в членах класса, и в инициализаторе поля: в этом случае создаются две копии одного и того же значения, о чём компилятор предупредит вас ошибкой CS9124.Источник: https://steven-giesel.com/blogPost/a3c741d3-f261-4bda-8da6-bffbbbd7aa5a/primary-constructors-are-more-than-syntactic-sugar
👍4👎1
День 2832. #ВопросыНаСобеседовании
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
52. Тестирование производительности
Опишите ваш подход к тестированию производительности .NET приложений. Расскажите об этапах работы: от планирования и выбора инструментов до проведения тестирования и анализа полученных результатов.
Хороший ответ
Тестирование производительности в .NET-приложениях предполагает системный подход, направленный на выявление потенциальных «узких мест» и обеспечение эффективной работы приложения при ожидаемой нагрузке. Здесь можно выделить несколько важных моментов.
1. Определение критериев производительности: установление того, что именно считается «хорошей производительностью» для конкретного приложения — включая допустимое время отклика, пропускную способность и уровень потребления ресурсов.
2. Разработка сценариев тестирования: создание тестов, имитирующих различные режимы нагрузки: штатный, пиковый и стрессовый.
3. Выбор подходящих инструментов: подбор инструментов, способных реалистично имитировать пользовательскую активность и эффективно измерять показатели производительности. Для .NET-приложений часто используется BenchmarkDotNet для тестирования отдельных блоков кода и NBomber для стресс-теста всей системы.
4. Настройка тестовой среды: подготовка контролируемой среды, максимально приближённой к рабочей. Это включает использование аналогичного аппаратного обеспечения, программного стека, сетевых настроек и конфигураций баз данных.
5. Проведение тестирования: запуск подготовленных сценариев с помощью выбранных инструментов. Мониторинг работы приложения: отслеживание времени отклика, пропускной способности и использования ресурсов.
6. Автоматизация: по возможности — автоматизация запуска тестов и сбора метрик для их интеграции в процесс CI/CD.
7. Анализ данных: сбор и изучение результатов тестирования для выявления «узких мест» или областей, где показатели не соответствуют заданным критериям.
8. Оптимизация: внесение изменений в код, конфигурацию или инфраструктуру на основе полученных данных. Это может включать оптимизацию SQL-запросов, улучшение стратегий кэширования или масштабирование аппаратных ресурсов.
9. Итеративный подход: тестирование производительности — это циклический процесс. Повторное проведение тестов позволяет проверить эффективность внесённых изменений и убедиться, что приложение по-прежнему соответствует установленным критериям.
Основные принципы
- Всесторонний охват: тесты должны затрагивать все критически важные функциональные возможности и сценарии взаимодействия с пользователем.
- Реалистичность сценариев: максимально точная имитация поведения реальных пользователей и объёмов данных.
- Документирование: ведение подробных записей об условиях тестирования, конфигурациях и результатах необходимо для дальнейшего анализа и проведения регрессионного тестирования.
Это позволяет гарантировать, что .NET-приложение будет стабильно работать при ожидаемых нагрузках и обеспечит качественный пользовательский опыт.
Распространенный, но неудачный подход:
«Мы нагружаем основные конечные точки системы перед релизом. Если система не «падает» — значит, всё в порядке. После следим за «длинными ответами» в проде».
Почему это неверно:
- Отсутствие подготовки и планирования: такой подход свидетельствует об отсутствии должного планирования и учёта специфических требований к производительности приложения. Нагрузочное тестирование должно быть неотъемлемой частью жизненного цикла разработки, а не второстепенной задачей, о которой вспоминают в последний момент.
- Недостаточно качественное тестирование: проведение поверхностных тестов без анализа конкретных требований или использования структурированной методологии, скорее всего, приведёт к тому, что критические проблемы с производительностью останутся незамеченными.
- Неверное понимание критериев производительности: убеждение в том, что отсутствие сбоев — единственный показатель приемлемой работы системы, говорит о непонимании целей нагрузочного тестирования. Критерии производительности должны включать такие показатели, как время отклика, пропускная способность, потребление ресурсов и другие метрики.
Подобная ошибка обычно возникает из-за отсутствия опыта в проведении нагрузочного тестирования или недооценки его значимости для успешной работы и масштабируемости приложения. Кроме того, это может указывать на корпоративную культуру, в которой недостаточно внимания уделяется тщательному тестированию.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
Марк Прайс предложил свой набор из 60 вопросов (как технических, так и на софт-скилы), которые могут задать на собеседовании.
52. Тестирование производительности
Опишите ваш подход к тестированию производительности .NET приложений. Расскажите об этапах работы: от планирования и выбора инструментов до проведения тестирования и анализа полученных результатов.
Хороший ответ
Тестирование производительности в .NET-приложениях предполагает системный подход, направленный на выявление потенциальных «узких мест» и обеспечение эффективной работы приложения при ожидаемой нагрузке. Здесь можно выделить несколько важных моментов.
1. Определение критериев производительности: установление того, что именно считается «хорошей производительностью» для конкретного приложения — включая допустимое время отклика, пропускную способность и уровень потребления ресурсов.
2. Разработка сценариев тестирования: создание тестов, имитирующих различные режимы нагрузки: штатный, пиковый и стрессовый.
3. Выбор подходящих инструментов: подбор инструментов, способных реалистично имитировать пользовательскую активность и эффективно измерять показатели производительности. Для .NET-приложений часто используется BenchmarkDotNet для тестирования отдельных блоков кода и NBomber для стресс-теста всей системы.
4. Настройка тестовой среды: подготовка контролируемой среды, максимально приближённой к рабочей. Это включает использование аналогичного аппаратного обеспечения, программного стека, сетевых настроек и конфигураций баз данных.
5. Проведение тестирования: запуск подготовленных сценариев с помощью выбранных инструментов. Мониторинг работы приложения: отслеживание времени отклика, пропускной способности и использования ресурсов.
6. Автоматизация: по возможности — автоматизация запуска тестов и сбора метрик для их интеграции в процесс CI/CD.
7. Анализ данных: сбор и изучение результатов тестирования для выявления «узких мест» или областей, где показатели не соответствуют заданным критериям.
8. Оптимизация: внесение изменений в код, конфигурацию или инфраструктуру на основе полученных данных. Это может включать оптимизацию SQL-запросов, улучшение стратегий кэширования или масштабирование аппаратных ресурсов.
9. Итеративный подход: тестирование производительности — это циклический процесс. Повторное проведение тестов позволяет проверить эффективность внесённых изменений и убедиться, что приложение по-прежнему соответствует установленным критериям.
Основные принципы
- Всесторонний охват: тесты должны затрагивать все критически важные функциональные возможности и сценарии взаимодействия с пользователем.
- Реалистичность сценариев: максимально точная имитация поведения реальных пользователей и объёмов данных.
- Документирование: ведение подробных записей об условиях тестирования, конфигурациях и результатах необходимо для дальнейшего анализа и проведения регрессионного тестирования.
Это позволяет гарантировать, что .NET-приложение будет стабильно работать при ожидаемых нагрузках и обеспечит качественный пользовательский опыт.
Распространенный, но неудачный подход:
«Мы нагружаем основные конечные точки системы перед релизом. Если система не «падает» — значит, всё в порядке. После следим за «длинными ответами» в проде».
Почему это неверно:
- Отсутствие подготовки и планирования: такой подход свидетельствует об отсутствии должного планирования и учёта специфических требований к производительности приложения. Нагрузочное тестирование должно быть неотъемлемой частью жизненного цикла разработки, а не второстепенной задачей, о которой вспоминают в последний момент.
- Недостаточно качественное тестирование: проведение поверхностных тестов без анализа конкретных требований или использования структурированной методологии, скорее всего, приведёт к тому, что критические проблемы с производительностью останутся незамеченными.
- Неверное понимание критериев производительности: убеждение в том, что отсутствие сбоев — единственный показатель приемлемой работы системы, говорит о непонимании целей нагрузочного тестирования. Критерии производительности должны включать такие показатели, как время отклика, пропускная способность, потребление ресурсов и другие метрики.
Подобная ошибка обычно возникает из-за отсутствия опыта в проведении нагрузочного тестирования или недооценки его значимости для успешной работы и масштабируемости приложения. Кроме того, это может указывать на корпоративную культуру, в которой недостаточно внимания уделяется тщательному тестированию.
Источник: https://github.com/markjprice/tools-skills-net8/blob/main/docs/interview-qa/readme.md
👍2
День 2833. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 7
Сразу прошу прощения за оченьмногокода. Но раз уж взялся за эту серию, выложу её целиком (хоть и по кусочкам).
Пишем свой токен отмены с нуля. Начало
В предыдущей части мы создали класс
Самым очевидным API был бы тот, которого на самом деле не существует:
Поэтому отмена в .NET — «скоординированная» (cooperative): операции лишь сообщают, что пора завершиться, а она сама выбирает подходящий для этого момент. Механизм отмены передаёт флаг и, при необходимости, уведомляет заинтересованные стороны о его изменении.
Почему два типа?
-
-
Метод, принимающий
Токен – структура со ссылкой на источник:
Следствия:
1.
2. Свойство
Состояние источника (source) определяется одним полем:
volatile здесь не для красоты. Допустим, у вас реализована отмена через пулинг в цикле:
Внутри этого цикла ничего не меняет флаг, и ничто, что видит JIT-компилятор, его не меняет. Без volatile JIT читает его один раз, кэширует в регистре и переписывает цикл в while (true). Это допустимая оптимизация однопоточной программы, и это привело бы к тому, что отмена никогда бы не срабатывала. volatile заставляет читать значение на каждой итерации.
Флаг является односторонним. Отмена источника не может откатиться назад в неотменённый статус, т.е. операция, которая обнаружила отмену, никогда не должна гадать, изменится ли статус отмены позже.
ThrowIfCancellationRequested
Исключение содержит в себе токен. Это важно в месте перехвата (блоке
Продолжение следует…
Источник: https://www.meziantou.net/writing-your-own-cancellationtoken-from-scratch.htm
Ещё Раз Про Async/Await в C#. Часть 7
Сразу прошу прощения за оченьмногокода. Но раз уж взялся за эту серию, выложу её целиком (хоть и по кусочкам).
Пишем свой токен отмены с нуля. Начало
В предыдущей части мы создали класс
MyTask, который может запускать выполнение задачи и сообщать о её завершении. Однако он не умеет отменять выполнение. Этот не недоработка. Отмена в .NET – отдельная задача, реализованная с помощью двух типов. В этой части мы создадим MyCancellationTokenSource и MyCancellationToken, а затем интегрируем в MyTask. Реализация поможет ответить на распространённые вопросы о том, как устроен токен отмены.Самым очевидным API был бы тот, которого на самом деле не существует:
task.Kill(). В .NET когда-то было нечто подобное — Thread.Abort(). Он прерывал выполнение в произвольный момент. В результате система оказывалась в состоянии, к которому код не был готов, и понять, какие операции успели выполниться, а какие — нет, становилось невозможно. В .NET Core Thread.Abort нет, и не существует способа принудительно остановить код, который «не хочет» останавливаться.Поэтому отмена в .NET — «скоординированная» (cooperative): операции лишь сообщают, что пора завершиться, а она сама выбирает подходящий для этого момент. Механизм отмены передаёт флаг и, при необходимости, уведомляет заинтересованные стороны о его изменении.
Почему два типа?
-
MyCancellationTokenSource инициирует отмену (метод Cancel());-
MyCancellationToken лишь наблюдает за состоянием — он не может ничего отменить.Метод, принимающий
CancellationToken, получает право *узнать* о запросе на отмену, но не право *принять решение* об отмене. Иначе любой метод, которому вы передали токен, мог бы отменить всю операцию целиком — даже те её части, о которых он ничего не знает.Токен – структура со ссылкой на источник:
// MyCancellationToken.cs
public readonly struct MyCancellationToken(
MyCancellationTokenSource? source)
{
public static MyCancellationToken None
=> default;
public bool IsCancellationRequested
=> source is { IsCancellationRequested: true };
public bool CanBeCanceled
=> source is not null;
}
Следствия:
1.
default — допустимый токен, который никогда не переходит в состояние «отменён». Поэтому запись CancellationToken token = default - способ задать необязательный параметр.2. Свойство
CanBeCanceled стоит проверять. Для default оно возвращает false, что позволяет в «горячих» путях кода избежать создания механизмов регистрации отмены, которые всё равно никогда не сработают.Состояние источника (source) определяется одним полем:
// MyCancellationTokenSource.cs
public sealed class MyCancellationTokenSource
: IDisposable
{
private bool _disposed;
private volatile bool _cancellationRequested;
public bool IsCancellationRequested
=> _cancellationRequested;
public MyCancellationToken Token
{
get
{
ObjectDisposedException.ThrowIf(_disposed, this);
return new MyCancellationToken(this);
}
}
}
volatile здесь не для красоты. Допустим, у вас реализована отмена через пулинг в цикле:
while (!token.IsCancellationRequested)
DoOneChunkOfWork();
Внутри этого цикла ничего не меняет флаг, и ничто, что видит JIT-компилятор, его не меняет. Без volatile JIT читает его один раз, кэширует в регистре и переписывает цикл в while (true). Это допустимая оптимизация однопоточной программы, и это привело бы к тому, что отмена никогда бы не срабатывала. volatile заставляет читать значение на каждой итерации.
Флаг является односторонним. Отмена источника не может откатиться назад в неотменённый статус, т.е. операция, которая обнаружила отмену, никогда не должна гадать, изменится ли статус отмены позже.
ThrowIfCancellationRequested
//MyCancellationToken.cs
public void ThrowIfCancellationRequested()
{
if (IsCancellationRequested)
throw new MyOperationCanceledException(this);
}
// MyOperationCanceledException.cs
public sealed class MyOperationCanceledException(
MyCancellationToken token)
: OperationCanceledException("The operation was canceled.")
{
public MyCancellationToken Token { get; } = token;
}
Исключение содержит в себе токен. Это важно в месте перехвата (блоке
catch): в противном случае ситуации «была отменена именно та операция, которую я просил» и «была отменена какая-то другая операция, а её исключение просто перехвачено тут» выглядели бы одинаково. Свойство CancellationToken в OperationCanceledException нужно для того, чтобы их различать — благодаря ему задача может решить, перейти ли ей в состояние «отменена» (Canceled) или в состояние «завершена с ошибкой» (Faulted).Продолжение следует…
Источник: https://www.meziantou.net/writing-your-own-cancellationtoken-from-scratch.htm
👍13
День 2834. #ЗаметкиНаПолях #Async
Ещё Раз Про Async/Await в C#. Часть 7
Пишем свой токен отмены с нуля. Продолжение
Начало
Обратные вызовы и проблема «гонки»
Пулинг (периодический опрос) работает, только когда операция выполняет какой-либо код. Если же это, например, чтение из сокета приостановлено в ожидании данных, пулинг не сработает — а ведь такие операции чаще всего и требуется отменять. В подобных случаях отмена должна быть реализована через механизм событий:
Конец метода – то же состояние гонки, что было в MyTask.
Регистрация реализована в виде структуры со ссылкой на источник:
Предупреждение:
В методе Cancel выполняются зарегистрированные обратные вызовы и собираются исключения:
Важные моменты:
1. Отмена уже отменённого источника ничего не делает. Она идемпотентна, а не является ошибкой.
2. Обратные вызовы выполняются вне блокировки. Обратный вызов, который принимает другую блокировку или регистрирует другой обратный вызов, не должен приводить к взаимоблокировке с собой.
3. Сбой в одном обратном вызове не останавливает остальные. Это несвязанные части процесса очистки; исключения собираются и выбрасываются вместе.
Регистрацию можно отменить, потому что обратный вызов, который существует дольше операции, которая его требовала, является утечкой:
Окончание следует…
Источник: https://www.meziantou.net/writing-your-own-cancellationtoken-from-scratch.htm
Ещё Раз Про Async/Await в C#. Часть 7
Пишем свой токен отмены с нуля. Продолжение
Начало
Обратные вызовы и проблема «гонки»
Пулинг (периодический опрос) работает, только когда операция выполняет какой-либо код. Если же это, например, чтение из сокета приостановлено в ожидании данных, пулинг не сработает — а ведь такие операции чаще всего и требуется отменять. В подобных случаях отмена должна быть реализована через механизм событий:
// MyCancellationTokenSource.cs
private readonly Lock _lock = new();
private List<Registration>? _registrations = [];
internal MyCancellationRegistration
Register(Action callback)
{
lock (_lock)
{
if (!_cancellationRequested)
{
var registration = new Registration(callback);
_registrations!.Add(registration);
return new MyCancellationRegistration(this, registration);
}
}
// Register на отменённом источнике
// выполняет коллбек сразу вне блокировки
callback();
return default;
}
internal sealed class Registration(Action callback)
{
public Action Callback { get; } = callback;
}
Конец метода – то же состояние гонки, что было в MyTask.
Cancel() и Register() могут быть вызваны одновременно в разных потоках, при этом обратный вызов должен сработать ровно один раз: либо потому, что Cancel обнаружил его в списке, либо потому, что Register увидел, что флаг отмены уже установлен. Решение принимается внутри защищённой блокировкой области, а само действие выполняется уже вне её.Регистрация реализована в виде структуры со ссылкой на источник:
// MyCancellationRegistration.cs
public readonly struct MyCancellationRegistration : IDisposable
{
private MyCancellationTokenSource? _source;
private MyCancellationTokenSource.Registration? _registration;
internal MyCancellationRegistration(
MyCancellationTokenSource source,
MyCancellationTokenSource.Registration registration)
{
_source = source;
_registration = registration;
}
public void Dispose()
{
if (_source is not null
&& _registration is not null)
_source.Unregister(_registration);
}
}
Предупреждение:
MyCancellationRegistration.Dispose() завершает выполнение, в то время как обратный вызов всё ещё может выполняться в другом потоке. Это упрощение в нашей реализации. Стандартный метод CancellationTokenRegistration.Dispose() блокирует поток до тех пор, пока обратный вызов не завершится.В методе Cancel выполняются зарегистрированные обратные вызовы и собираются исключения:
// MyCancellationTokenSource.cs
public void Cancel()
{
List<Registration>? registrations;
lock (_lock)
{
if (_cancellationRequested)
return;
_cancellationRequested = true;
registrations = _registrations;
_registrations = null;
}
if (registrations is null)
return;
List<Exception>? exceptions = null;
foreach (var reg in registrations)
{
try
{
reg.Callback();
}
catch (Exception ex)
{
exceptions ??= [];
exceptions.Add(ex);
}
}
if (exceptions is not null)
throw new AggregateException(exceptions);
}
Важные моменты:
1. Отмена уже отменённого источника ничего не делает. Она идемпотентна, а не является ошибкой.
2. Обратные вызовы выполняются вне блокировки. Обратный вызов, который принимает другую блокировку или регистрирует другой обратный вызов, не должен приводить к взаимоблокировке с собой.
3. Сбой в одном обратном вызове не останавливает остальные. Это несвязанные части процесса очистки; исключения собираются и выбрасываются вместе.
Регистрацию можно отменить, потому что обратный вызов, который существует дольше операции, которая его требовала, является утечкой:
// MyCancellationTokenSource.cs
private bool _disposed;
internal void Unregister(Registration registration)
{
lock (_lock)
{
_registrations?.Remove(registration);
}
}
public void Dispose()
{
lock (_lock)
{
if (_disposed)
return;
_disposed = true;
_registrations = null;
}
}
Окончание следует…
Источник: https://www.meziantou.net/writing-your-own-cancellationtoken-from-scratch.htm
👍4
День 2836. #ЧтоНовенького #NuGet
Настройка Минимального Возраста NuGet-пакета
В .NET11 Release Candidate 2 в NuGet добавлена новая опция командной строки, позволяющая разработчикам задавать минимальный «возраст» (часов с момента публикации) для версий пакетов.
Это поможет избежать «слишком ранних» обновлений, чтобы избежать случаев, когда в опубликованном пакете обнаружится какая-нибудь ошибка (обычно сообщество находит их довольно быстро, за 1-2 дня).
Опция
Команды добавления и обновления пакетов учитывают это ограничение при выборе версий. Если более новая версия исключается из-за того, что она «слишком свежая», система сообщит об этом при выполнении обновления.
Эта настройка также влияет на проекты, в которых используются «плавающие» версии пакетов. Если после применения ограничения не остается ни одной подходящей версии, процесс восстановления (restore) завершается с ошибкой
В Visual Studio 2026 параметр можно настроить в меню Tools > Options… > NuGet Package Manager > Sources (Инструменты > Параметры… > Диспетчер пакетов NuGet > Источники). Там нажать Edit (Изменить) у выбранного источника и задать значение поля Minimum publish age (hours) (Минимальный возраст публикации (часы)).
Остаётся под вопросом, что делать со срочными критическими обновлениями. Пока система не предполагает наличия исключений из правила возраста.
Источник: https://visualstudiomagazine.com/articles/2026/10/14/net-11-rc2-adds-post-quantum-crypto-maui-csharp-ui-and-nuget-controls.aspx
Настройка Минимального Возраста NuGet-пакета
В .NET11 Release Candidate 2 в NuGet добавлена новая опция командной строки, позволяющая разработчикам задавать минимальный «возраст» (часов с момента публикации) для версий пакетов.
Это поможет избежать «слишком ранних» обновлений, чтобы избежать случаев, когда в опубликованном пакете обнаружится какая-нибудь ошибка (обычно сообщество находит их довольно быстро, за 1-2 дня).
Опция
--min-publish-age-hours доступна при использовании команд dotnet nuget add source и dotnet nuget update source. Она позволяет настроить параметр minPublishAgeHours в файле nuget.config без необходимости редактировать его вручную.Команды добавления и обновления пакетов учитывают это ограничение при выборе версий. Если более новая версия исключается из-за того, что она «слишком свежая», система сообщит об этом при выполнении обновления.
Эта настройка также влияет на проекты, в которых используются «плавающие» версии пакетов. Если после применения ограничения не остается ни одной подходящей версии, процесс восстановления (restore) завершается с ошибкой
NU1020 — вместо того чтобы просто продолжиться без установки нужного пакета.В Visual Studio 2026 параметр можно настроить в меню Tools > Options… > NuGet Package Manager > Sources (Инструменты > Параметры… > Диспетчер пакетов NuGet > Источники). Там нажать Edit (Изменить) у выбранного источника и задать значение поля Minimum publish age (hours) (Минимальный возраст публикации (часы)).
Остаётся под вопросом, что делать со срочными критическими обновлениями. Пока система не предполагает наличия исключений из правила возраста.
Источник: https://visualstudiomagazine.com/articles/2026/10/14/net-11-rc2-adds-post-quantum-crypto-maui-csharp-ui-and-nuget-controls.aspx
👍5