7.1 Состояние двух потоков
В цеху с несколькими мастерами один журнал - на общей полке. Не у первого мастера на верстаке, не у второго: на полке, куда есть доступ у всех. Иначе каждый ведёт свой учёт, и к вечеру никто не знает картины целиком. Когда нет общей полки, у каждого своя версия. А своя версия у каждого - значит, ни у кого нет правды.
Что нужно
Чтобы подключить хранилище к серверу, нужно решить три задачи: как передать его обработчикам, как поделить между несколькими потоками, и как безопасно менять из любого из них. Каждая - отдельная глава. Здесь - первая.
Обработчики - это функции, которые axum вызывает по одной для каждого входящего
запроса. add, list, get_task не знают о main() и не имеют доступа к
переменным внутри него. Нужно хранилище, которое живёт дольше одного запроса и
доступно всем обработчикам сразу. Прежде чем подключать TaskStore, разберёмся
с механизмом: пока в роли хранилища выступит Vec<Task>.
У axum для этого есть State: .with_state() прикрепляет объект к роутеру,
а экстрактор State<S> - тот же принцип, что у Path и Json - достаёт его в
обработчике. State<S> требует, чтобы S реализовывал Clone: axum клонирует состояние
при каждом вызове обработчика.
Постройка
Vec<T> реализует Clone только когда T: Clone - значит, нужен Clone на Task,
и на Status - поле Task тоже должно реализовывать Clone.
В crates/core/src/task/mod.rs:
// CHANGED: Clone нужен, чтобы Vec<Task> мог реализовать Clone
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Status { Todo, Done }
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Task { ... }
В crates/api/src/routes.rs заменяем заглушки на State<Vec<Task>>. В add и list
добавим eprintln! - он напечатает адрес вектора в памяти и поможет понять, что происходит
с данными при каждом запросе:
// crates/api/src/routes.rs - CHANGED
use axum::{
Json, Router,
extract::{Path, State},
routing::{get, patch, post},
};
use serde::Deserialize;
use tq_core::task::Task;
#[derive(Deserialize)]
struct AddRequest {
title: String,
}
// State(mut tasks) - axum передаёт владение клоном; mut нужен, чтобы изменить вектор
async fn add(State(mut tasks): State<Vec<Task>>, Json(req): Json<AddRequest>) -> Json<Task> {
eprintln!("add: vec @ {:p}", tasks.as_ptr()); // {:p} - адрес вектора в памяти
let id = (tasks.len() + 1) as u64;
let task = Task::new(id, &req.title).unwrap();
tasks.push(task.clone()); // clone: задача нужна и в векторе, и в ответе
Json(task)
}
async fn list(State(tasks): State<Vec<Task>>) -> Json<Vec<Task>> {
eprintln!("list: vec @ {:p}", tasks.as_ptr());
Json(tasks)
}
async fn get_task(State(tasks): State<Vec<Task>>, Path(id): Path<u64>) -> Json<Task> {
Json(tasks.into_iter().find(|t| t.id == id).unwrap())
}
async fn done(State(mut tasks): State<Vec<Task>>, Path(id): Path<u64>) -> Json<Task> {
let task = tasks.iter_mut().find(|t| t.id == id).unwrap();
task.complete();
Json(task.clone())
}
pub fn router(tasks: Vec<Task>) -> Router {
Router::new()
.route("/tasks", post(add).get(list))
.route("/tasks/{id}", get(get_task))
.route("/tasks/{id}/done", patch(done))
.with_state(tasks) // прикрепляет состояние к роутеру
}
В crates/api/src/main.rs собираем начальное состояние и передаём его роутеру
через .with_state():
// crates/api/src/main.rs - CHANGED
mod routes;
use axum::{Router, routing::get};
use tq_core::task::Task;
async fn health() -> String {
"tq ok".to_string()
}
#[tokio::main]
async fn main() {
let tasks: Vec<Task> = vec![ // начальное состояние: axum клонирует его в каждый запрос
Task::new(1, "buy milk").unwrap(),
Task::new(2, "send report").unwrap(),
];
let app = Router::new()
.route("/", get(health))
.merge(routes::router(tasks));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Результат
В одном терминале - сервер (make serve), в другом - три запроса подряд:
$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk",...},{"id":2,"title":"send report",...}]
$ curl -s -X POST localhost:3000/tasks \
-H "Content-Type: application/json" -d '{"title":"call dentist"}'
{"id":3,"title":"call dentist",...}
$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk",...},{"id":2,"title":"send report",...}]
call dentist исчезла. Сервер принял запрос, вернул задачу с id 3 - но следующий
list её не видит. В терминале сервера видно почему:
list: vec @ 0x55f3a1b2c4d0
add: vec @ 0x55f3a1b2c5e8 ← другой адрес
list: vec @ 0x55f3a1b2c6a0 ← снова другой
Каждый запрос получил свой клон вектора. add добавил задачу в свой клон - и унёс
её с собой, когда запрос завершился. Следующий list знать ничего не знал о call dentist.
Clone выполнил задание компилятора. У каждого обработчика теперь есть журнал -
просто у каждого свой. Журнал на общей полке - следующий шаг.
Полный код
tqдля этой главы -7-many-hands/01-the-state-two-threads-share/.