Перейти к основному содержимому
Нормативная версия документации - английская

Эта страница переведена с английского языковой моделью. При любом расхождении верен английский оригинал.

Учебник: собираем первый сервис

Этот учебник собирает сервис заказов с нуля и разбирает каждую его часть. Сервис работает на in-memory брокере из ядра, поэтому ему не нужны ни сервер, ни адаптация библиотеки брокера. Переход на настоящий брокер - правка в одну строку, её показывает шаг 7.

1. Создайте крейт​

cargo new orders-service
cd orders-service
Cargo.toml
[package]
name = "orders-service"
version = "0.1.0"
edition = "2024"

[dependencies]
ruststream = { version = "0.7", features = ["macros", "memory", "json", "asyncapi", "logging"] }
serde = { version = "1", features = ["derive"] }
tracing = "0.1"

2. Опишите сообщение и обработчик​

Обработчик - это async fn. Первым параметром он получает полезную нагрузку, уже декодированную из сообщения. Макрос #[subscriber] делает из функции определение подписчика с тем же именем.

src/orders.rs
use ruststream::runtime::HandlerOutcome;
use ruststream::schemars::JsonSchema;
use ruststream::{Outgoing, subscriber};
use serde::{Deserialize, Serialize};

/// An order placed by a customer.
#[derive(Debug, Deserialize, JsonSchema)]
pub(crate) struct Order {
pub(crate) id: u64,
pub(crate) quantity: u32,
}

#[subscriber("orders")]
pub(crate) async fn handle(order: &Order) -> HandlerOutcome {
tracing::info!(order.id, order.quantity, "order received");
HandlerOutcome::ack()
}

Обработчик возвращает исход, HandlerOutcome:

  • ack подтверждает сообщение;
  • nack отбрасывает его или возвращает в очередь.

Вместо исхода можно вернуть () или Result<(), E>. Тогда Ok подтверждает сообщение, а Err отбрасывает.

JsonSchema добавляет схему полезной нагрузки в AsyncAPI-документ из шага 6, а doc-комментарий типа становится описанием сообщения. Фича asyncapi реэкспортирует schemars, поэтому отдельная зависимость не нужна.

3. Свяжите обработчик с приложением​

Приложение - это функция с атрибутом #[ruststream::app]. В ней with_broker подключает брокер, а include регистрирует на нём обработчик:

src/main.rs
mod orders;

use ruststream::memory::MemoryBroker;
use ruststream::runtime::{AppInfo, RustStream};

use crate::orders::handle;

#[ruststream::app]
fn app() -> RustStream {
RustStream::new(AppInfo::new("orders-service", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(handle);
})
}

Запустите сервис:

cargo run -- run

Фича logging выводит журнал сервиса в терминал: сначала строки запуска, затем по строке на каждый заказ, который обработчик записывает через tracing::info!.

Кодек по умолчанию

include декодирует сообщения кодеком по умолчанию. Это json, если фича включена, иначе cbor, иначе msgpack. Другой кодек для всех обработчиков брокера задаётся один раз через with_broker_codec(broker, codec, |b| ...). Правила выбора кодека описаны в разделе Кодеки.

4. Ответьте на сообщения​

Переключить обработчик в reply-форму можно, указав аргумент reply. Значение, которое возвращает обработчик в reply-форме, публикуется как ответ. Тип ответа выводит Outgoing. Тип без этого derive не компилируется.

Адрес ответа задаётся одним из двух способов:

  • В самом аргументе: #[subscriber("orders", reply("confirmations"))]. Тогда тип ответа выводит Outgoing без имени.
  • На типе ответа: #[outgoing(name = "confirmations")]. Тогда хватает аргумента reply без значения.

Если адрес объявлен на типе, он важнее адреса в reply("..."). Пример ниже объявляет адрес на типе:

src/orders.rs
/// The service's answer to an order.
#[derive(Debug, Serialize, JsonSchema, Outgoing)]
#[outgoing(name = "confirmations")]
pub(crate) struct Confirmation {
pub(crate) id: u64,
pub(crate) accepted: bool,
}

#[subscriber("orders", reply)]
pub(crate) async fn confirm(order: &Order) -> Confirmation {
Confirmation {
id: order.id,
accepted: order.quantity > 0,
}
}

Подключите confirm тем же include, что и handle:

src/main.rs
use crate::orders::{confirm, handle};

#[ruststream::app]
fn app() -> RustStream {
RustStream::new(AppInfo::new("orders-service", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(handle);
b.include(confirm);
})
}

Ответ публикуется политикой брокера по умолчанию и кодируется кодеком по умолчанию. Другие способы публикации, в том числе изнутри обработчика, описаны в разделе Публикация и ответы.

5. Наведите порядок роутером​

Когда обработчиков становится много, вынесите их в отдельный модуль и соберите в Router. Роутер - это набор обработчиков, который приложение подключает к брокеру одним вызовом.

src/routes.rs
use ruststream::memory::prelude::*;

use crate::orders;

// The reply wiring is a publish policy: pure declaration, so the router needs no broker at all.
pub(crate) fn orders() -> impl RouterDef<MemoryBroker> {
Router::new()
.include(orders::handle)
.include(orders::confirm)
.out_reply(Publish)
.build()
}

include добавляет в роутер обычный обработчик. Для обработчика в reply-форме он возвращает цепочку:

  • .out_reply(..) задаёт политику публикации ответа;
  • .build() завершает регистрацию.

Без .out_reply(..) метод .build() берёт политику брокера по умолчанию, ту же, что include в шаге 4.

Приложение подключает роутер через include_router:

src/main.rs
mod orders;
mod routes;

use ruststream::memory::MemoryBroker;
use ruststream::runtime::{AppInfo, RustStream};

#[ruststream::app]
fn app() -> RustStream {
RustStream::new(AppInfo::new("orders-service", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
let router = routes::orders();
b.include_router(router);
})
}

Остальные возможности роутера описаны в разделе Роутинг.

6. Посмотрите AsyncAPI-документ​

cargo run -- asyncapi gen

Команда печатает AsyncAPI-документ сервиса. Каждый подписчик добавляет в него канал и операцию receive. handle и confirm слушают канал orders, но у каждого своя подписка и своя операция. Ответ добавляет операцию send на канале confirmations.

Схемы полезных нагрузок лежат в components.messages. Флаги вывода (-o, --yaml) и устройство документа описаны в руководстве по AsyncAPI.

7. Перейдите на настоящий брокер​

Ничто из написанного выше не привязано к in-memory брокеру. Чтобы перейти на другой брокер, добавьте в зависимости адаптацию его клиентской библиотеки и создайте брокер в with_broker вместо MemoryBroker::new(), например NatsBroker::new("nats://localhost:4222"). Это одна строка. Обработчики, роутер и кодеки остаются прежними. Список адаптаций и замену для каждой из них даёт раздел Брокеры.

Готовый сервис - это компилируемый пример

Каждый фрагмент этой страницы взят из examples/tutorial, который CI собирает при каждом изменении. first_app.rs и reply_app.rs - это сервис после шагов 3 и 4, а main.rs - готовый. Запустить его можно командой cargo run --example tutorial --features macros,memory,json,asyncapi,logging -- run.

Что дальше​

  • Middleware - сквозная логика вокруг обработчиков.
  • Жизненный цикл - разделяемое состояние и хуки старта и остановки.
  • Тестирование - тесты только что написанных обработчиков прямо в процессе.
  • Метрики - счётчики и гистограммы Prometheus.