Сначала хотел написать комментарий к статье "Я десять лет страдал от ужасных архитектур в C#...", но понял две вещи:
- Слишком много мыслей, которыми хочется поделиться.
- Для такого объёма формат комментария неудобен ни для написания, ни для прочтения.
- Давно читаю Хабр, иногда комментирую, но ни разу не писал статей.
- Я не силён в нумерованных списках.
Disclaimer: я не критикую @pnovikov или его задумку в целом. Текст качественный (чувствуется опытный редактор), часть мыслей разделяю. Архитектур много, но это нормально (да, звучит как название корейского фильма).
Однако давайте по порядку. Сначала моё мнение о том, что влияет на архитектуру, потом про спорные моменты в статье об «исправлении архитектур». Ещё расскажу о том, что у нас хорошо работает — может, пригодится кому-нибудь.
И, конечно, всё сказанное здесь является личным мнением на основе моего опыта и когнитивных искажений.
О моём мнении
Я часто принимаю архитектурные решения. Когда-то — большие, когда-то — маленькие. Изредка придумываю архитектуру с нуля. Ну, как с нуля — наверняка всё придумано до нас, но про что-то мы не знаем, поэтому приходится придумывать. И не из любви к велосипедостроению (скажем так, не только из любви к нему), а потому что для некоторых задач не нашлось готового решения, которое устроило бы по всем параметрам.
Почему я считаю, что совершенно разные архитектуры имеют право на жизнь? Можно порассуждать о том, что программирование — искусство, а не ремесло, но я не буду. Моё мнение: когда-то искусство, когда-то — ремесло. Речь не об этом. Главное, что задачи разные. И люди. Уточню — под задачами подразумеваются требования бизнеса.
Если когда-то мои задачи станут однотипными, я напишу или попрошу кого-то написать нейросеть (а может, хватит и скрипта), которая меня заменит. А сам займусь чем-то менее безрадостным. Пока же мой и, надеюсь, ваш личный апокалипсис не наступил, давайте подумаем, как влияют задачи и прочие условия на разнообразие архитектур. TL&DR; — разнообразно.
Производительность или масштабируемость
Вот это самая правильная, пожалуй, причина поменять архитектуру. Если, конечно, не проще старую архитектуру адаптировать к новым требованиям. Но тут сложно кратко рассказать что-то полезное.
Сроки
Допустим, сроки (дважды убедился, что написал через «о») сильно сжаты. Тогда нам не до того чтобы выбирать, а, тем более, придумывать архитектуру — бери знакомые инструменты и копай. Но есть нюанс — иногда сложные проекты можно сделать вовремя только применив (и, возможно, придумав) что-то принципиально новое. Кто-то может сказать, что пригласить заказчика в баню — старый прием, но я сейчас про архитектуру…
Когда сроки комфортные — часто получается парадоксальная ситуация — вроде и можно придумать что-то новое, но зачем? Правда, многие успешно поддаются соблазну заняться другим более горящим проектом и сводят ситуацию к предыдущей.
В моей практике сроки редко приводят к революциям в архитектуре, но бывает. И это прекрасно.
Скорость и качество разработки
Бывает так — команда (или кто-то из руководства) замечают, что скорость разработки снизилась или за итерацию багов многовато набежало. Нередко виновной в этом признают «неправильную архитектуру». Иногда — заслуженно. Чаще — просто как самого удобного обвиняемого (особенно, если в коллективе нет её «родителя»).
В принципе, в одних случаях всё сводится к фактору сроков. А в других — к поддерживаемости, о ней далее.
Поддерживаемость
Неоднозначная тема. Потому что всё очень субъективно и зависит много от чего. Например — от команды, языка программирования, процессов в компании, количества адаптаций под разных клиентов. Давайте про последний фактор, мне он кажется самым интересным.
Вот вы сделали заказной проект. Успешно, в сроки и бюджет уложились, заказчик всем доволен. Было и у меня такое. Теперь вы смотрите на то, что использовали и думаете — так вот она — золотая жила! Мы сейчас используем все эти наработки, быстро сделаем один B2B-продукт, и… Сначала всё хорошо. Продукт сделали, пару раз продали. Наняли ещё продавцов и разработчиков («нужно больше золота»). Заказчики довольны, платят за сопровождение, случаются новые продажи…
А потом один из заказчиков говорит человеческим голосом — «мне бы вот эту штуковину совсем по-другому сделать — сколько это может стоить?». Ну, подумаешь — несколько if’чиков с другим кодом воткнуть (допустим, некогда было DI прикрутить), что плохого может случиться?
И в первый раз действительно ничего плохого не случится. Я бы даже не советовал в такой ситуации что-то специальное городить. Преждевременное усложнение архитектуры сродни преждевременной оптимизации. Но когда это случается во второй и третий раз — это повод вспомнить про такие штуки как DI, паттерн «стратегия», Feature Toggle и иже с ними. И, на время, это поможет.
А потом наступает день, когда вы смотрите на настройки проекта (всего-то несколько сотен опций) для конкретного тестового стенда… Вспоминаете, как посчитать число сочетаний и думаете — как же, вашу мать, это можно протестировать? Понятно, что в идеальном мире это просто — ведь каждая фича спроектирована и реализована так, что она никак не влияет на другую, а если влияет, то всё это предусмотрено и вообще наши разработчики никогда не ошибались.
Конечно, я сгустил краски — можно выделить какие-то наборы фич, которые используются у реальных заказчиков, написать больше тестов (как и каких — тема отдельного разговора) и немного упростить задачу. Но вдумайтесь — каждый серьезный релиз нужно протестировать для всех заказчиков. Напоминаю, это не B2C, где можно сказать «выкатим фичу для 5% пользователей и соберём фидбек» — для B2B фидбек можно по судам начать собирать…
Решения? Например, разделить продукт на модули с отдельным жизненным циклом (не забывая тестировать их взаимодействие). Это снизит сложность сопровождения, хотя и усложнит разработку. И я сейчас не о благодатной для холиваров теме «монолит vs. микросервисы» — в монолите тоже можно устроить подобное (хотя и сложнее, на мой взгляд).
И, заметьте, с прагматической точки зрения на каждом этапе у нас была неплохая архитектура.
И к чему всё это?
Я не хочу вас (и себя) утомлять перечислением других причин для изменений в архитектуре. Давайте сейчас согласимся, что архитектуры имеют свойство со временем меняться, в зависимости от многих факторов. А значит: идеальная архитектура, решающая «ну вот все проблемы» не существует.
Если я вас в этом еще не убедил — посмотрите на разнообразие языков программирования и фреймворков (только не во фронтенде — не надо вскрывать эту тему). Если кто-то скажет, что это плохо, предлагаю провести мысленный эксперимент — представьте мир, в котором есть один конкретный язык программирования. С одним важным условием — он вам не нравится. Например, потому что вы никогда его не использовали, да и не собирались этого делать.
И, признаюсь, есть еще один веский довод — придумывать что-то новое, оптимизируя несколько параметров, играя компромиссами — это чертовски увлекательно. А теперь, когда мы все (правда?) согласны, что разнообразие архитектур — это нормально…
Обсуждение статьи про «исправление архитектур»
А что IoC?
Про IoC соглашусь, что портянкам место в армии, а модули — это вселенское добро. Но вот всё остальное…
Если, конечно, послушать некоторых апологетов «чистого кода», то можно накодить гору сервисов, в каждом из которых будет в среднем полтора метода, а в методе — две с половиной строки. Но зачем? Вот честно, вы точно хотите соблюдать принципы, которые помогут справиться с маловероятными проблемами в далеком будущем, но размазывают даже несложную логику по десяткам файлов? Или вам, всё-таки, достаточно сейчас написать прилично работающий код?
К слову сказать, сейчас работаю над модулем, который точно будет использоваться в разных продуктах и, скорее всего, будет активно «тюниться». Так и там стараюсь не «мельчить». Не окупается. Хотя вот в нём использую единственные реализации интерфейсов чаще обычного.
Так вот, если у нас есть модули и мы не «мелочны», то откуда взяться проблемам производительности IoC или неподдерживаемых «портянок IoC-конфигураций»? Я не сталкивался.
Правда, уточню наши условия работы:
- Модули у нас не те, которые «предоставляются почти любым IoC-фреймворком» а вот «прям модули» — которые общаются между собой удалённо через API (иногда можно, из соображений производительности, поселить их в одном процессе, но схема работы не поменяется).
- IoC используется максимально простой и максимально просто — зависимости втыкаются в параметры конструктора.
- Да, у нас сейчас микросервисная архитектура, но и здесь стараемся не мельчить.
Совет: интерфейсы можно держать в том же файле, что и класс — удобно (если, конечно, пользуетесь нормальной IDE, а не блокнотом). Исключения делаю, когда интерфейсы (или комментарии к ним) разрастаются. Но это всё вкусовщина, конечно.
А что не так с ORM и зачем прямой доступ к БД?
Да я и сам скажу, что не так — многие из них слишком далеки от SQL. Но не все. Поэтому, вместо того, чтобы «терпеть, пока O/RM удаляет 3000 объектов» или придумывать ещё один, найдите тот, который вас устроит.
Совет: попробуйте LINQ to DB. Он хорошо сбалансирован, есть методы Update/Delete для нескольких строк. Только осторожно — вызывает привыкание. Да, нет каких-то фич EF и немного другая концепция, но мне понравился намного больше EF.
Кстати, приятно, что это разработка наших соотечественников. Игорю Ткачеву — респект (не нашёл его на Хабре).
UPD: RouR отметил в комментариях, что есть расширение для EF Core, позволяющее использовать массовые операции. LINQ to DB всё равно не брошу, потому что он хороший.
А что не так с тестами на БД?
Да они будут медленнее, чем на данных в памяти. Фатально ли это? Да нет, конечно же. Как решать эту проблему? Вот два рецепта, которые лучше применять одновременно.
Рецепт №1. Берёшь крутого разработчика, который любит делать всякие прикольные штуки и обсуждаешь с ним, как красиво решить эту проблему. Мне повезло, потому что force решил проблему быстрее, чем она появилась (даже не помню, обсуждали её или нет). Как? Сделал (за день, вроде) тестовую фабрику для ORM, которая подменяет основное подмножество операций на обращения к массивам.
Для простых юнит-тестов — идеально. Альтернативный вариант — юзать SQLite или что-то подобное вместо «больших» БД.
Комментарий от force: Тут надо сделать пару уточнений. Во-первых, мы стараемся не использовать сырые запросы к базе данных в коде, а максимально используем ORM, если он хороший, то лезть в базу с SQL наголо не требуется. Во-вторых, разница поведения с базой есть, но ведь мы не проверяем вставку в базу, мы проверяем логику, и небольшое различие в поведении тут несущественно, т.к. особо ни на что не влияет. Поддержка корректной тестовой базы гораздо сложнее.
Рецепт №2. Бизнес-сценарии я предпочитаю тестировать на настоящих БД. А если в проекте заявлена возможность поддержки нескольких СУБД, тесты выполняются для нескольких СУБД. Почему? Да всё просто. В утверждении «не хочется тестировать сервер баз данных», увы, происходит подмена понятий. Я, знаете ли, тестирую не то, что join работает или order by.
Я тестирую свой код, работающий с БД. А зная, что даже разные версии одной СУБД могут выдавать разные результаты на одинаковых запросах (пруф), я хочу основные сценарии проверять именно на тех БД, с которыми этот код будет работать.
Обычно подобные тесты у меня выглядят так:
- Для группы тестов (Fixture) с нуля генерируется по метаданным БД. Если нужно — заполняются необходимые справочники.
- Каждый сценарий сам добавляет нужные данные в процессе прохождения (пользователи ведь тоже это делают). В тестах на производительность не так, но это уже совсем другая история…
- После каждого теста лишние данные (кроме справочников) удаляются.
Совет: если такие тесты у вас выполняются объективно долго (а не потому, что пора оптимизировать запросы к базе) — сделайте билд, который будет запускать их реже (категории тестов или отдельный проект в помощь). Иначе разработчики не захотят сами запускать и остальные — быстрые тесты.
Транзакция и email
Просто дополню историю «транзакция в БД по каким-то причинам упала, а e-mail ушёл». А какое веселье будет, когда транзакция подождёт недоступного почтового сервера, поставив колом всю систему из-за какого-нибудь уведомления, которое пользователь потом отправит в корзину, не читая…
Правда, я всегда считал, что письма в транзакции отправляют только джуны
Итоги
В целом, если @pnovikov не имеет планов по захвату мира с помощью единственно верной
Предлагаемый фреймворк вряд ли буду использовать. Причина проста — у нас уже есть идеальная архитектура…
P.S. Если у вас будет желание обсудить что-то в комментариях, буду рад принять в этом участие.
nesterenko-kv
Все конечно так, но если Вам придет письмо с таким содержимым, то я уверен, в штаны наложите знатно:
Привет, $username$! С тебя списали 100500 рублей за то-то, то-то, то-то.
OlegAxenow Автор
Надеюсь, со мной не случится ни того, ни другого :)
Когда я писал статью, в основном думал о том, что отправка письма часто менее важна, чем сохранение данных и, тем более, работоспособность системы для других пользователей.
Впрочем, есть же разумные способы как вообще не встречаться с такими проблемами. Мне они показались очевидными, поэтому не стал писать в статье.
GreyStrannik
Простой способ: завершить транзакцию и отправить письмо. В крайнем случае адресат не получит уведомление.
Правильный способ: завершить транзакцию, в рамках транзакции в БД создаётся событие отправки уведомления. Эти события периодически выгребаются и отправляются в очередь. Может получиться два сообщения, если крашнулось во время транзакции. Из очереди рассыльщик выгребает сообщения, формирует и отправляет письма. Письмо может уйти дважды. Письмо нужно обогащать данными пользователя и самого события. В общем сложно, дорого и опять без гарантий.
Давят сроки. Они всегда давят. На чём будет настаивать архитектор? Что выберет разработчик, его тимлид и начальник тимлида? Что получится в итоге?
OlegAxenow Автор
Лично я, конечно, за правильный способ. А по поводу сроков, начальников и обогащения...
Druu
Вообще не проверять тот факт, что внешний сервис отработал корректно (на уровне бизнес-логики, по крайней мере). Странно, почему этот очевидный ответ для кого-то не очевиден, на самом-то деле.
OlegAxenow Автор
Такой вариант тоже подходит, но когда факт отправки почты вообще не важен. Либо я не совсем понял предложенное решение.
Часто отправка почты важна, но просто это не срочная задача. Обычно в наших проектах (и продуктах) если письмо не ушло, то сервис будет пытаться отправить его через некоторое время повторно (и только уже в редких случаях придётся человеку разбираться в чём проблема).
Druu
Да все просто — факт того, что вам внешний сервис ответил "ок", не означает, что все на самом деле ок. Следовательно, даже в случае такого ответа вам надо предусматривать негативный сценарий.
В 90% случаев (вроде "письма с подтверждением") гарантированной отправки не требуется — тогда и проверять просто ничего не нужно и делать ничего не нужно. Дернули внешний апи — и все, конец, если сервис я ответил "неок", то можно считать, что случился негативный сценарий, который уже обработан.
Если гарантия требуется — то это должно быть реализовано в слое, который непосредственно взаимодействует с внешним апи (там будет и какая-то retry-логика, и логи ошибок взаимодействия, и запись задач в бд и т.д.).
В бизнес-логике в любом случае ничего не будет, будет просто вызов sendMesage либо sendMessageGuaranteed, без каких-либо явных транзакций или проверок.
В таком контексте вопрос "пихать отправку в транзакцию или нет" — он в принципе как бы и не стоит даже.
OlegAxenow Автор
Теперь чуть лучше понял, что имеете в виду. Понятно, что "проценты" у всех разные, как и подходы, впрочем.
Ещё бы уточнить, как обеспечивается целостность без транзакций (когда реально несколько операций с БД и надо обязательно либо всё сделать и отправить письмо, либо ничего не сделать и не отправлять) — саги?
UPD: На всякий случай поясню, я не говорю про то, что всё всегда должно быть в транзакции. Но вот зарегистрировать факт необходимости вызова внешнего сервиса для наших задач очень удобно как раз в транзакции, а потом уже да — и повторы и прочее конечно не дело бизнес-логики — у нас за это отдельный PersistentInvoker отвечает. Конечно, есть подход, когда решение о "персистентности" вызова принимает не вызывающий код (а оркестрируется где-то сбоку) и он тоже имееет право на жизнь, просто у нас не прижился.
nesterenko-kv
У Amazon в SQS есть возможность отхендлить баунсы и комплейны по отправленным письмам. Чтобы гарантировать прочтение письма, можно заюзать инструмент для отслеживания писем. Такой же подход применим и для push-уведомлений.
OlegAxenow Автор
Amazon тоже юзаем для некоторых проектов, а вот SQS или SES — тут больше force в курсе. Так или иначе, всё что нужно — хэндлится :)
Но не все заказчики хотят/могут использовать Amazon, так что делаем по-разному.
force
Прочтение письма никто не гарантирует. Его можно проверять или заголовком, и почтовый клиент сам письмо отправит (обычно отключают все). Или трекинговым кодом. Но загрузка внешних изображений может быть отключена в целях безопасности или проксироваться (как делают всякие гуглы и яндексы). А когда они решат попросить картинку — в целом не очень известно.
Баунсы и комплейнты приходят с аццкой задержкой, которую можно использовать для отключения доставки на этот ящик (пользователь не хочет), но не как гарантию доставки/недоставки.
pnovikov
Чего это вы так сразу. Для меня очевиден :)
В Tecture тестируется тот факт, что вы отправили команду для отправки email-а. А как на это отреагировал email-сервер — не тестируется (ну или может захватиться один раз, при изначальном прогоне с живыми системами).
То есть вы послали команду, проверили что она реально работает — и забетонировали факт отправки команды именно с такими параметрами в тест. Остальное — не вашей бизнес-логики дело.
powerman
На первый взгляд это так, но не совсем.
В простом случае нет гарантии что письмо уйдёт, в правильном есть гарантия что оно уйдёт минимум один раз (если, конечно, у нас SMTP-сервер в принципе работает, хотя бы изредка :)). Иными словами в правильном случае есть гарантия уведомления юзера. Хотя, конечно, у юзера письмо и в спам попасть может, и не заметить его он тоже может… но это всё не наши проблемы, наша задача — чтобы наш сервис отправил письмо по SMTP, и здесь гарантия всё-таки есть.
Другой аспект этой проблемы заключается в том, что — не почтой единой! Особенно в случае микросервисной архитектуры. Я к тому, что если прямо сейчас единственная задача, которую этот сервис должен выполнить, условно говоря, в рамках транзакции в БД, это отправка почты — то через пару месяцев таких задач будет воз и маленькая тележка: отправка события в nats/кафку/кролика/что там у вас, дёргание вебхука стороннего сервиса для синхронизации состояния или уведомления о чём-то и т.п. Поэтому использование "правильного способа" с самого начала, пусть даже пока надо только письма отправлять — в целом верное решение для многих проектов.
Druu
А зачем нам эта гарантия? Задача же не в том, чтобы отправить письмо, а в том, чтобы пользователь его прочитал. Отправлено оно или нет — при этом совершенно не важно. Как и в том случае, если пользователь его не прочитал — не важно совершенно, по каким именно причинам это произошло.
powerman
Затем, что наша область ответственности ограничена тем, что мы контролируем. Юзер может решить не читать письмо даже если оно у него перед глазами — это вне нашего контроля в принципе. Всё, что мы можем — гарантировать что от нас письмо ушло. И если мы не в состоянии это реализовать — это баг в нашем коде (если, конечно, бизнес изначально не ставил задачу "попытаться отправить письмо, но если не отправится, то и фиг с ним").
Druu
Зачем? Кому это нужно и для чего?
Если вы что-то можете, то это не значит, что следует это что-то по факту делать.
powerman
Всё просто. :) Бизнес попросил реализовать отправку писем при определённом событии. Если моя реализация будет в большинстве случаев отправлять письмо, но иногда, при неудачном стечении обстоятельств, не отправлять — это баг (если, конечно, бизнес не включил такое поведение в постановку задачи).
Вы ведь не хотели бы, чтобы бизнес свои обязательства по отношению к Вам выполнял в том же стиле, который подразумевает Ваш комментарий (обычно зарплату выплачивает, но при неудачном стечении обстоятельств может и не выплатить за какой-то месяц вообще)?
Druu
Бизнесу-то это зачем? оО
В таком виде это просто плохо поставленная задача, надо поработать над постановкой :)
Конечно, хотел бы! Меня совершенно не волнует, как и что мне выплатили, меня волнует — что я по итогу получил. И я хочу, чтобы вариант неполучения мной зп, раз уж он возможен в рамках обычного рабочего flow, был корректно обработан в рамках имеющихся бизнес-процессов, а не "мы деньги отослали, идите нахер".
mayorovp
Почему-то мне кажется, что редкий работодатель согласится выплатить зарплату повторно если прошлая выплата "застрянет" в недрах межбанковских переводов по вине банка. И уж точно ни один работодатель не станет так делать автоматически.
Так же и с почтой. Да, было бы неплохо как-то обрабатывать почтовые "отлупы", да и мониторинг инфраструктуры точно лишним не будет — но это всё не является тем, над чем надо задумываться при реализации отправки уведомления. Это задачи совершенно другого уровня.
И да, потенциальная возможность появления всяческих неразрешимых проблем при доставке электронных писем — не повод вообще никогда не отправлять их.
powerman
Ну, на самом деле я об этом задумываюсь при отправке — это выражается в том, чтобы выяснить у бизнеса/админов адрес для envelope и не забывать указывать его при отправке. Это не совсем "обрабатывать отлупы", скорее "сделать обработку отлупов возможной в принципе".
P.S. И делаю я это чисто ради того, чтобы не искать впустую баг в коде отправке из-за "я не получил письмо при регистрации!!111", когда дело в отлупе и это можно легко проверить просто заглянув в ящик, куда эти отлупы сыпятся.
Druu
Вот именно. Так никто не делает.
Вообще-то, это именно то, что при отправке уведомления всегда учитывается. И, поскольку это всегда заведомо учтено — вам не надо думать о гарантиях отправки. Возможность неотправки уже учтена.
Вроде, об этом никто и не говорил. Речь шла о том, что нет никакого абсолютно смысла как-то выделять "письмо не ушло" в отдельную причину по сравнению с кучей других и обрабатывать эту причину неким особым по сравнению с остальными причинами способом. С точки зрения бизнес-процессов не важно совершенно, почему пользователь письмо не получил — почти всегда, вне зависимости от причины, действия надо по результату такого неполучения предпринимать ровно одни и те же.
ApeCoder
Т.е. если оператор опечатался в email или если админ неправильно настроил почтовый сервер или провайдер почтового сервиса потребовал другой протокол работы действия один и те же? Исправляем адрес базе?
VolCh
В общем случае использовать другой канал связи
pnovikov
Вкачусь в дискуссию.
Тут, кмк, надо разделять возникающие ошибки не две категории: логическая (ваш код не маякнул SMTP-серверу отправить письмо) и инфраструктурная (у SMTP-сервера кончились почтовые голуби и он ваше письмо не отправил).
В Tecture я позволяю юниттестить логические ошибки: получать тест-прувен гарантии что бизнес-логика точно затриггерила отправку емейла. Остальное — инфраструктурные ошибки. И чинятся они не в коде (а например в конфиге SMTP-сервера).
Я ставил себе целью как раз отделить логическое от инфраструктурного. И получилось.
mvv-rus
Про правильный способ отправки сообщений по SMTP — мой взгляд со своей колокольни: я последнее время работал, в основном, администратором сервера электронной почты (MS Exchange).
Так вот, постоянная ошибка программистов прикладных систем (и постоянные их претензии по этому поводу) — в том, что они ожидают, что сервер SMTP всегда готов принять их сообщение. На самом деле это не так. И протокол SMTP этого не требует: в нем есть коды ответа (в диапазоне 400-499), означающие временную недоступность сервиса: сервер не может принять сообщение сейчас, но может это сделать позже, поэтому отправку сообщения надо будет повторить. В частности, у MS Exchange переход в такое состояние — это часть штатной реакции на перегрузку (back pressure), а временная перегрузка в почтовой системе бывает и по вполне уважительным причинам: например, служба внутреннего PR послала сообщения о какой-нибудь акции всем сотрудникам, и прямо сейчас сервер занят доставкой тысяч сообщений в п/я, буквально через минуту он это закончит и освободится, но если повторной отправки посланного в этот момент сообщения из программы не будет, то это сообщение будет потеряно.
Поэтому любой сервер и большинство клиентов SMTP и организуют очередь на отправку (у клиента это обычно специальная папка, типа «Исходящие») и делают попытки повторной отправки из очереди, если они получили ответ о временной недоступности сервиса. Но вот некоторые самодельные программы-клиенты SMTP, (почему-то) так не делают: они пытаются отправлять только один раз.
В Windows Server есть, конечно, обходной путь: поднять локальный сервер SMTP (он там входит в состав дистрибутива) и делать отправку через него, лучше — через папку Pickup. Но, вообще-то, за этим сервером SMTP тоже нужно приглядывать — отказы там тоже возможны. То есть это — лишняя точка отказа, которая требует мониторинга.
Поэтому я двумя руками за такой правильный способ, который гарантирует повторную отправку сообщений при временной недоступности сервиса SMTP из самой программы. Иначе способ отправки будет не совсем соответствовать протоколу, и тогда возможны неприятности.
В случае, если необходимость отправки письма возникает в рамках транзакции в БД, разумно, на мой взгляд, организовать очередь отправки в специальной таблице этой БД (а если отправка важна — то и сделать добавление в эту таблицу частью транзакции), а отправку организовать отдельным процессом, читающим эту очередь и формирующим сообщение.
stristan
Ну или, на худой (или не очень) конец, воспользоваться облачным сервисом, который на 99,99% гарантирует отправку сообщений