zero2prod (Rust)

от автора

Лет так много назад, если верить слухам того времени, питон был не зыбко популярен, flask был где-то в узких кругах, а за django продавцам нужно было замолвить слово. Все, конечно, понимали — за django будущее, и не только потому, что java всем поднадоела, но потому что было удобно и для бизнеса, и для кодинга. Что кривить, читая книгу zero2prod невольно вспоминаешь удовольствие от изучения django, удивления — «а что, так можно было», и пожалуй, глубину проработки деталей, которые обычный разработчик осилил бы самостоятельно, но обычно было лень.

Rust при всей своей скромности по скорости весьма удобен для day to day разработки, и книга (которая в тайтле) раскрывает детали этой парадоксальной особенности.

Хотелось бы имееть какую-никакую логику в построении приложения, но, к сожaлению или к счастью, в наше время при наличии широкого набора инструментов любой компонент будущей структуры выглядит наиболее подходящим на место первого к выполнению. Сходимость проекта, в свою очередь, будет определяться всем попало, и не последнюю роль в ней будет играть фантазия авторов и усидчивость коллег.

Начнем, пожалуй, с файлов конфигурации. В свое время Степанов говорил, что единственное применение inheritance есть в наследовании полей. У нас есть файлик базовой конфигурации base.yaml и много-много файлов конфигураций для различных сред. В коде это выглядит примерно так:

Config::builder()     .add_source(File::new("configuration/base.yaml", FileFormat::Yaml))     .add_source(File::new(&env_config, FileFormat::Yaml))     .build() 

На всякий случай, yaml формат — это такой удобный KV с отступами.

Миграции для базы данных, почему бы и нет. Ну то есть у нас еще нет базы данных, но есть знание, что персистентный стейт нужен, и каким-то образом нужно будет создать таблички или менять колонки. Для миграций важна очередность выполнения (к именам файлов иногда добавляют timestamp) и инструмент (command line tool). Автор zero2prod юзает миграции в баш-скрипте. Как-то так:

... export DATABASE_URL=... sqlx database create sqlx migrate run 

Лирическое отступление: забавно, что как киллер-фича наряду с async/await рассматривалась и rust cli. То есть как бы люди думали в 2018 куда приложить ресурсы и рассматривалось четыре направления: Embedded systems, WebAssembly, Command-line interfaces, Network services. И тут автор книги о расте на голубом глазу юзает баш.

Докер. Наверно, не очень распространенное, но очень удобное решение — two stage build. На первом шаге билдим код, на втором копируем и запускаем бинарник. Автор zero2prod придает большое значение размеру образа — там прям увлекательное чтиво. Далее автор деплоит приложение на облако, попутно описывая файл конфигурации для деплоя — где живет база, где application code, нужен ли load balancer и т.д. В целом это отдельная история, но как бы неплохо бы знать, как ваш код будет исполняться.

Пару слов об sqlx, который согласно докам «..is not an ORM, but compile-time checked queries». То есть ребята во время компиляции коннектятся к базе и проверяют структуру ваших sql запросов. Кажется, в какое-то время всех это задолбало, и ребята выпустили command line утилиту для генерации query oффлайн. Далее в книге идет не очень популярная, но весьма интересная аргументация о движке запросов. Если мне не изменяет память, суть ORM в достаточно легкой замене базы данных без изменения кода приложения (см. например django). Так вот, автор zero2prod предлагает юзать чистый sql, аргументируя это тем, что язык приложения может смениться, а sql запросы останутся эскуальными. Напомню, книга о языке программирования rust.

Лирическое отступление: популярный вопрос на интервью — какие либы вы юзали. Автор zero2prod сравнивает sqlx c двумя orm, и кажется, самый притязательный читатель найдет что-нибудь интересное для себя.

Итак, мы хотим уже написать код, но как? Автор книги предлагает через тесты, а сам проект разбить на клиентский код и библиотеку. Клиентский код — эт так, чтоб потыкаться курлом, ну или через браузер, — а библиотеку нужно покрыть тестами чуть более чем полностью — red green development, все дела. Тут, как говорится, есть нюанс. Пофиксить свои тесты это всегда ок. Пофиксить чужие тесты, если test case не больше десяти строк, тоже ок, а вот пофиксить абстракцию в тестах (потому что много тестов и нужны абстракции) — это, скорее, skip test, чем фикс. Спорить о тестах также бесполезно, как и о языках программирования (хотя все и так знают, что rust лучший 🙂

Все вебовские приложения в конце концов юзают «экстримли фаст» web framework, не исключая и zero2prod. Автор книги юзает actix-web с весьма интересной фичей powerful request routing. Тут проще кодом:

App::new()     .route("/health_check", web::get().to(routes::health_check))     .route("/subscriptions", web::post().to(routes::subscribe))     

Pучка routes::subscribe может иметь почти любую сигнатуру. По словам автора actix-web, это происходит благодаря системе типов раста, а не за счет магии макросов. Например,

pub async fn health_check() ... pub async fn subscribe(form: web::Form<Email>, pool: web::Data<SqlitePool>) ...  // in the same time we can swap args if we want in subscribe(...) pub async fn subscribe(pool: web::Data<SqlitePool>, form: web::Form<Email>) ... 

Кажется, в динамических языках это сделать не очень тривиально.

Интересная структура кода у автора zero2prod — в одном файле и имплементация route, и запрос к базе данных. Кажется, все свалено в кучу, c другой стороны код получается очень компактным, поэтому и не хочется разделять на файлы, например:

pub async fn subscribe(...) -> Result<HttpResponse, SubscribeError> {     let new_subscriber = form.0.try_into()?;     let mut transaction = pool.begin().await?;     insert_subscriber(&new_subscriber, &mut transaction).await?;     transaction.commit().await?;     Ok(HttpResponse::Ok().finish()) } 

Вот эти знаки вопроса в коде — это про удобство (или эргономику) раста. Каждый такой знак на самом деле раскрывается в что-то типа такого:

if insert_subscriber(&new_subscriber, &mut transaction).await.is_err() {     return HttpResponse::InternalServerError().finish(); } 

Внимательный читатель тут же поинтересуется, каким образом SubscribeError связан с http ответом (ResponseError) — тут работает нативная фича раста — Trait (они же протоколы), как-то так:

impl From<sqlx::Error> for SubscribeError {     fn from(e: sqlx::Error) -> Self {         Self::DatabaseError(e)     } }  impl From<String> for SubscribeError {     fn from(e: String) -> Self {         Self::ValidationError(e)     } }  impl ResponseError for SubscribeError {     fn status_code(&self) -> StatusCode {         match self {             SubscribeError::ValidationError(_) => StatusCode::BAD_REQUEST,             SubscribeError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,         }     } }  

Немного многословно, можно сделать короче с помощью библиотек обработки ошибок: anyhow и thiserror. Тут идея, как мне кажется, в том, что ошибки лежат где-то в одном месте и не мешают чтению кода, основной логики программы (только ok path — класс!).

Валидации инпута. Автор книги предлагает считать String, они же подписчики «грязными» данными, а структурку SubscriberName(String) чистыми данными, соответственно переход между состояниями строго в одном месте:

impl SubscriberName {     pub fn parse(s: String) -> Result<SubscriberName, String> { ... } } 

В идеальном мире, наверно, сработало бы, а так кажется маловероятным, что любые String инпута будут обернуты в структурки. Опять же есть трэйт AsRef — возможно и прокатит:

impl AsRef<str> for SubscriberName {     fn as_ref(&self) -> &str {         &self.0     } } // e.g. we can use our SubscriberName here  fn somewhere_inside_codebase(x: impl AsRef<str>) { ... } 

Пару слов про telemetry. Наверно, не стоит все бросать и заменять логи на телеметрию уже сегодня. Идея вроде путная, и вроде уже много сервисов по сбору эвентов от приложений. Но хз. Мне зашла такая обертка (см. ниже) которая вроде как форсит вас юзать короткие функции и одновременно добавляет эвент на входе функции и эвент на выходе:

#[tracing::instrument(     name = "Adding a new subscriber",     skip(form, pool),     fields(         subscriber_email = %form.email,         subscriber_name= %form.name     ) )] pub async fn subscribe(...) {} 

В книге еще много чего интересного, поэтому приятного чтения.


ссылка на оригинал статьи https://habr.com/ru/articles/827782/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *