Понимаю вашу боль. Разворот макроса в документации действительно выглядит устрашающе, и дебажить типы, если что-то пошло не так, бывает непросто
Но тут вопрос приоритетов. Если писать вручную возвращаемые типы Pin<Box<dyn Future...>> в каждом методе доменного слоя, это сильно повышает порог входа и зашумляет бизнес логику техническими деталями. async_trait стал стандартом дефакто в веб бэкенде на Rust именно потому, что позволяет писать сигнатуры почти как в синхронном коде, пряча эту сложность от разработчика прикладного уровня
Для обучающего материала мне показалось важнее, чтобы читатель сфокусировался на архитектуре и потоках данных, а не на борьбе с лайфтаймами в боксовых футурах)
Если вы уже пишете на Rust, то порог входа будет низким. В solana смарт контракты пишутся на Rust, де-факто стандарт сейчас фреймворк Anchor, он очень производительный
Насчет "денежная или нет" рынок стал взрослее. Времена шальных денег на простых форках прошли, но спрос на сильных инженеров, способных пилить сложный DeFi, инфраструктуру или DePIN (физические сети), огромный. И рейты там часто выше, чем в классическом вебе
Главный плюс для разработчика даже если хайп спадет или вы решите уйти из Web3, опыт написания строгого, высокопроизводительного кода на Rust и понимание работы state machines останутся с вами. Это отличная инвестиция в hard skills
Выбор между dyn и дженериками обычный вопрос в Rust
Я не храню реализации в одной коллекции. Но я выбрал dyn Trait по нескольким причинам которые являются для меня архитектурными
1)Скорость компиляции. Дженерики приводят к мономорфизации изза чего в больших проектах это может замедлить инкрементальные сборки на минуты.
2) Эргономика и невирусность. Если AppState становится AppState<c: ChainClient> , то все хендлеры, которые его используют тоже должны стать дженериками. А это переусложняет сигнатуры и код веб слоя. dyn Trait позволяет сохранить AppState и хендлеры простыми и чистыми создавая четкую компиляционную границу которую дженерики размывают
3) Гибкость. С dyn Trait я могу собрать один бинарник который при старте читает конфиг или переменную окружения и решает какую реализацию поднять. SolanaChainClient для прода, MockChainClient для тестов или стейджинга. С дженериками это сделать не сложнее, и часто требует перекомпиляции с разными #[cfg] флагами.
Архитектура или код?
Архитектура набор осознанных ограничечий и компромиссов. Жертва наносекундами производительности на vtable вызовах ради ускорения разработки, упрощения кода и гибкости конфигурации
Я написал ручные моки для наглядности. Но для реальных проектов где требуется много моков и сложные проверки то mockall будет must have Спасибо что дополнили!
Ваше различие между ООП интерфейсами и трейтами самое лаконичное объснение сути которое я слышал)
Именно об этом и вся статья, бизнес логике не нужен весь "тяжелый" клиент соланы, ей нужна лишь одна его способность записать хеш. Трейты в Rust позволяют выразить эту потребность без лишнего багажа
Пример с C# библиотеками для Excel точно в точку, эта боль заставляет искать более гибкие подходы
Вы правы на счет схемы она больше про поток данных, чем про строгую UML. Сделал для наглядности
solana_sdk и RPC это не разные вещи а части одного целого. solana_sdk - большой набор крейтов который дает и "инструктор" для сборки транзакций (solana_sdk::transaction) и сам RPC клиент для их отправки (solana_client). Этот клиент как раз работает по JSON-RPC (поверх HTTP). Так что в статье и репе используется один и тот же подход - официальный SDK клиент
глянул dynosaur. хороший подход, если нужно сохранить zero cost для статической диспетчеризации
В моем случае AppState использует только dyn поэтому оверхед async_trait не критичен, а его простота и популярность в экосистеме сыграли в его пользу для туториала Но за альтернативу спасибо, полезно знать
Вы всё ещё не привели ни одного технического замечания. Пока что единственная “экспертиза”, которую вы демонстрируете - это умение заметить слово “LLM” и начать на нём ехать, вместо анализа сути.
Для человека, который позиционирует себя как Go-разработчик, странно видеть попытку уйти от инженерного разговора к гаданию на происхождении текста. Обычно так делают те, кому нечего сказать по сути.
Если вы хотите выглядеть экспертом - предъявите конкретные факты, ошибки, кейсы, сравнения, а не попытку спрятать отсутствие аргументов за фразой “типовой текст”. Пока что ваш вклад в дискуссию выглядит намного слабее даже самого среднего ответа LLM.
Вы затронули самый корень архитектурной дилеммы, и это именно тот вопрос, который мы задавали себе на старте. Давайте разберем предложенный вами флоу, который, по сути, и является паттерном Saga, описанным в статье.
Флоу "Сначала Postgres, потом Solana" (Saga):
Начинаем локальную транзакцию в Postgres, сохраняем диплом со статусом PENDING. Коммитим.
Отправляем транзакцию в Solana.
Теперь рассмотрим точки отказа:
Сценарий 1 (простой): Отправка в Solana не удалась. Что мы делаем? Компенсирующую транзакцию: обновляем статус в Postgres на FAILED. Пока все выглядит неплохо и действительно откатывается.
Сценарий 2 (коварный): Транзакция в Solana прошла успешно, но наш сервис падает до того, как он успел обновить статус в Postgres на CONFIRMED.
И вот здесь мы получаем тот же самый рассинхрон, только в обратную сторону: в блокчейне запись есть, а в нашей базе она навсегда осталась в статусе PENDING. Для исправления этого нам понадобится тот самый Reconciliation Job, который будет сверять записи и исправлять такие "подвисшие" состояния.
Но ключевая проблема в вашем тезисе - «все прекрасно откатывается» - не работает с блокчейном. Если транзакция в Solana прошла (сценарий 2), ее невозможно откатить. Можно только сделать новую транзакцию, которая логически ее аннулирует, но это: а) сложно (нужно хранить состояние, чтобы знать, что аннулировать), б) усложняет модель данных (появляются отмененные записи), в) стоит реальных денег (газ).
Именно поэтому Transactional Outbox оказывается надежнее в этом контексте. Он атомарно гарантирует, что задача "отправить в Solana" будет создана. А отдельный воркер потом надежно, с повторными попытками, выполнит эту задачу, защищая нас от сбоев самого сервиса-отправителя.
Так что проблема не высосана из пальца, а является фундаментальным компромиссом при работе с необратимыми внешними системами.
Забавно, но я воспринимаю это как комплимент качеству структуры и текста. В эпоху, когда LLM становятся все лучше, грань стирается. Главное, чтобы материал был полезен, решал реальную проблему и вызывал дискуссию, не так ли?
Cбой на этапе коммита это реальный риск для любой базы данных.
К счастью, здесь нам на помощь приходят гарантии транзакционной системы самой PostgreSQL.
Если COMMIT не прошел (из-за сбоя сети, падения БД, проблем с диском), база данных сама откатит всю транзакцию целиком. Это означает, что ни запись о дипломе, ни событие в outbox_events не будут сохранены. С точки зрения системы, операция просто не удалась. Пользователь получит ошибку, попробует еще раз, и мы не получим рассинхрона.
Если COMMIT прошел, СУБД гарантирует, что данные записаны на диск (согласно уровню durability). Даже если сервис упадет через миллисекунду после этого, обе записи (диплом и событие в outbox) уже будут в базе.
Таким образом, благодаря атомарности транзакции в PostgreSQL, мы избегаем состояния "частичной записи". У нас либо обе записи успешно сохранены, либо ни одной. Изначальная проблема "запись в блокчейн есть, а в БД нет" решена, так как запись в блокчейн теперь происходит асинхронно и только после успешного коммита в БД.
Это отсылка к известному выражению «усидеть на двух стульях». Оно описывает попытку совместить две разные, порой противоречивые вещи, рискуя в итоге не получить ни одной.
В нашем случае, эти «два стула» - это два наших хранилища данных, каждое со своей, уникальной задачей: Первый стул - блокчейн Solana. Это наш «стул» неизменности и гарантий. Второй стул - PostgreSQL. Это наш «стул» скорости и удобства.
Вся статья по сути о том как опасно сидеть «между» этими стульями. Что делать, если мы успешно «сели» на первый стул (записали транзакцию в Solana), но «пошатнулись» на втором (запись в PostgreSQL сорвалась из-за ошибки)? В этот самый момент и возникает рассинхрон — главная проблема, с которой мы боремся.
Понимаю вашу боль. Разворот макроса в документации действительно выглядит устрашающе, и дебажить типы, если что-то пошло не так, бывает непросто
Но тут вопрос приоритетов. Если писать вручную возвращаемые типы
Pin<Box<dyn Future...>>в каждом методе доменного слоя, это сильно повышает порог входа и зашумляет бизнес логику техническими деталями.async_traitстал стандартом дефакто в веб бэкенде на Rust именно потому, что позволяет писать сигнатуры почти как в синхронном коде, пряча эту сложность от разработчика прикладного уровняДля обучающего материала мне показалось важнее, чтобы читатель сфокусировался на архитектуре и потоках данных, а не на борьбе с лайфтаймами в боксовых футурах)
Если вы уже пишете на Rust, то порог входа будет низким. В solana смарт контракты пишутся на Rust, де-факто стандарт сейчас фреймворк Anchor, он очень производительный
Насчет "денежная или нет" рынок стал взрослее. Времена шальных денег на простых форках прошли, но спрос на сильных инженеров, способных пилить сложный DeFi, инфраструктуру или DePIN (физические сети), огромный. И рейты там часто выше, чем в классическом вебе
Главный плюс для разработчика даже если хайп спадет или вы решите уйти из Web3, опыт написания строгого, высокопроизводительного кода на Rust и понимание работы state machines останутся с вами. Это отличная инвестиция в hard skills
Выбор между
dynи дженериками обычный вопрос в RustЯ не храню реализации в одной коллекции. Но я выбрал
dyn Traitпо нескольким причинам которые являются для меня архитектурными1)Скорость компиляции. Дженерики приводят к мономорфизации изза чего в больших проектах это может замедлить инкрементальные сборки на минуты.
2) Эргономика и невирусность. Если
AppStateстановитсяAppState<c: ChainClient>, то все хендлеры, которые его используют тоже должны стать дженериками. А это переусложняет сигнатуры и код веб слоя.dyn Traitпозволяет сохранитьAppStateи хендлеры простыми и чистыми создавая четкую компиляционную границу которую дженерики размывают3) Гибкость. С
dynTrait я могу собрать один бинарник который при старте читает конфиг или переменную окружения и решает какую реализацию поднять.SolanaChainClientдля прода,MockChainClientдля тестов или стейджинга. С дженериками это сделать не сложнее, и часто требует перекомпиляции с разными#[cfg]флагами.Архитектура или код?
Архитектура набор осознанных ограничечий и компромиссов. Жертва наносекундами производительности на vtable вызовах ради ускорения разработки, упрощения кода и гибкости конфигурации
Я написал ручные моки для наглядности. Но для реальных проектов где требуется много моков и сложные проверки то mockall будет must have
Спасибо что дополнили!
Отличный поинт. Для статической диспетчеризации дженериков это теперь стандарт
Но этот подход к сожелению не object-safe. Его нельзя использовать с
dyn TraitА у меня вся архитектура завязана на
Arc<dyn ChainClient>иAppState, поэтому безasync_trait(который боксит футуры) пока не обойтисьТак что это осознанный выбор именно под
dynВаше различие между ООП интерфейсами и трейтами самое лаконичное объснение сути которое я слышал)
Именно об этом и вся статья, бизнес логике не нужен весь "тяжелый" клиент соланы, ей нужна лишь одна его способность записать хеш. Трейты в Rust позволяют выразить эту потребность без лишнего багажа
Пример с C# библиотеками для Excel точно в точку, эта боль заставляет искать более гибкие подходы
Спасибо за фидбек и отличные вопросы.
Вы правы на счет схемы она больше про поток данных, чем про строгую UML. Сделал для наглядности
solana_sdk и RPC это не разные вещи а части одного целого. solana_sdk - большой набор крейтов который дает и "инструктор" для сборки транзакций (solana_sdk::transaction) и сам RPC клиент для их отправки (solana_client). Этот клиент как раз работает по JSON-RPC (поверх HTTP). Так что в статье и репе используется один и тот же подход - официальный SDK клиент
А как, по-вашему, реализуется Гексагональная архитектура без dependency injection?
глянул dynosaur. хороший подход, если нужно сохранить zero cost для статической диспетчеризации
В моем случае AppState использует только dyn поэтому оверхед async_trait не критичен, а его простота и популярность в экосистеме сыграли в его пользу для туториала
Но за альтернативу спасибо, полезно знать
Вы всё ещё не привели ни одного технического замечания.
Пока что единственная “экспертиза”, которую вы демонстрируете - это умение заметить слово “LLM” и начать на нём ехать, вместо анализа сути.
Для человека, который позиционирует себя как Go-разработчик, странно видеть попытку уйти от инженерного разговора к гаданию на происхождении текста.
Обычно так делают те, кому нечего сказать по сути.
Если вы хотите выглядеть экспертом - предъявите конкретные факты, ошибки, кейсы, сравнения, а не попытку спрятать отсутствие аргументов за фразой “типовой текст”.
Пока что ваш вклад в дискуссию выглядит намного слабее даже самого среднего ответа LLM.
Вы сейчас не содержание текста обсуждаете, а придумываете его происхождение. Когда будут реальные аргументы - приходите.
Забавно, но я воспринимаю это как комплимент качеству структуры и текста. В эпоху, когда LLM становятся все лучше, грань стирается. Главное, чтобы материал был полезен, решал реальную проблему и вызывал дискуссию, не так ли?
Cбой на этапе коммита это реальный риск для любой базы данных.
К счастью, здесь нам на помощь приходят гарантии транзакционной системы самой PostgreSQL.
Если COMMIT не прошел (из-за сбоя сети, падения БД, проблем с диском), база данных сама откатит всю транзакцию целиком. Это означает, что ни запись о дипломе, ни событие в outbox_events не будут сохранены. С точки зрения системы, операция просто не удалась. Пользователь получит ошибку, попробует еще раз, и мы не получим рассинхрона.
Если COMMIT прошел, СУБД гарантирует, что данные записаны на диск (согласно уровню durability). Даже если сервис упадет через миллисекунду после этого, обе записи (диплом и событие в outbox) уже будут в базе.
Таким образом, благодаря атомарности транзакции в PostgreSQL, мы избегаем состояния "частичной записи". У нас либо обе записи успешно сохранены, либо ни одной. Изначальная проблема "запись в блокчейн есть, а в БД нет" решена, так как запись в блокчейн теперь происходит асинхронно и только после успешного коммита в БД.
Это отсылка к известному выражению «усидеть на двух стульях». Оно описывает попытку совместить две разные, порой противоречивые вещи, рискуя в итоге не получить ни одной.
В нашем случае, эти «два стула» - это два наших хранилища данных, каждое со своей, уникальной задачей:
Первый стул - блокчейн Solana. Это наш «стул» неизменности и гарантий.
Второй стул - PostgreSQL. Это наш «стул» скорости и удобства.
Вся статья по сути о том как опасно сидеть «между» этими стульями. Что делать, если мы успешно «сели» на первый стул (записали транзакцию в Solana), но «пошатнулись» на втором (запись в PostgreSQL сорвалась из-за ошибки)? В этот самый момент и возникает рассинхрон — главная проблема, с которой мы боремся.