← Оглавление

7.2 Общая полка

Самые опасные ошибки те, о которых компилятор молчит. Не потому что он их не видит - он их попросту не проверяет: его область - типы и владение, не корректность поведения. POST /tasks вернул задачу. GET /tasks вернул пустой список. Компилятор доволен. Это не его ошибка - это ошибка выбора.

Что нужно

Первая задача решена: State передаёт хранилище обработчикам. Но каждый получает свою копию вектора. Здесь - вторая задача: поделить между потоками, пока только для чтения.

Arc<T> - тип совместного владения: Arc::clone не копирует данные, он создаёт ещё одного владельца тех же данных. Как именно это работает - в Lore в конце главы, если интересно до кода.

У Arc есть граница: он позволяет вызывать методы, которые берут &self - list и get_task только читают, им этого достаточно. add и done меняют данные - их пока оставим заглушками.

Постройка

Оборачиваем Vec<Task> в Arc. В list и get_task добавим eprintln! - он покажет адрес вектора в памяти и докажет, что запросы смотрят на одно хранилище. add и done тип тоже меняют, но остаются заглушками. В crates/api/src/routes.rs:

// crates/api/src/routes.rs - CHANGED
use axum::{
    Json, Router,
    extract::{Path, State},
    routing::{get, patch, post},
};
use std::sync::Arc;
use serde::Deserialize;
use tq_core::task::Task;

#[derive(Deserialize)]
struct AddRequest {
    title: String,
}

// заглушка: задача не сохраняется - Arc не позволяет &mut
async fn add(_: State<Arc<Vec<Task>>>, Json(req): Json<AddRequest>) -> Json<Task> {
    Json(Task::new(0, &req.title).unwrap())
}

// CHANGED: Arc<Vec<Task>>; eprintln! покажет адрес - он будет одинаковым для всех запросов
async fn list(State(tasks): State<Arc<Vec<Task>>>) -> Json<Vec<Task>> {
    eprintln!("list: vec @ {:p}", tasks.as_ptr()); // {:p} - адрес вектора в памяти
    Json(tasks.to_vec())
}

// CHANGED: Arc<Vec<Task>>; eprintln! для того же доказательства
async fn get_task(State(tasks): State<Arc<Vec<Task>>>, Path(id): Path<u64>) -> Json<Task> {
    eprintln!("get:  vec @ {:p}", tasks.as_ptr());
    Json(tasks.iter().find(|t| t.id == id).unwrap().clone())
}

// заглушка: статус не меняется - Arc не позволяет &mut
async fn done(_: State<Arc<Vec<Task>>>, Path(id): Path<u64>) -> Json<Task> {
    Json(Task::new(id, "stub").unwrap())
}

// CHANGED: router принимает Arc<Vec<Task>>
pub fn router(tasks: Arc<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 - одно хранилище, обёрнутое в Arc:

// crates/api/src/main.rs - CHANGED
mod routes;

use axum::{Router, routing::get};
use std::sync::Arc;
use tq_core::task::Task;

async fn health() -> String {
    "tq ok".to_string()
}

#[tokio::main]
async fn main() {
    let tasks = Arc::new(vec![ // CHANGED: Arc<Vec<Task>> вместо Vec<Task>
        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();
}

Результат

В одном терминале - сервер, в другом - два запроса подряд:

$ curl -s localhost:3000/tasks
[{"id":1,"title":"buy milk",...},{"id":2,"title":"send report",...}]

$ curl -s localhost:3000/tasks/1
{"id":1,"title":"buy milk",...}

В терминале сервера:

list: vec @ 0x55f3a1b2c4d0
get:  vec @ 0x55f3a1b2c4d0   ← тот же адрес

Здесь оба запроса работают с одним и тем же вектором.

add и done пока заглушены: как безопасно изменить общий вектор - следующий шаг.

Полный код tq для этой главы - 7-many-hands/02-one-handle/.


Lore: Arc и счётчик ссылок

Arc расшифровывается как Atomically Reference Counted. Каждый Arc::clone атомарно увеличивает счётчик; когда последний владелец уходит из области видимости, счётчик падает до нуля и данные освобождаются. Атомарность означает, что несколько потоков могут создавать и уничтожать владельцев одновременно. Цена - два атомарных инкремента на каждый клон; у обычной ссылки этой цены нет. Для большинства приложений она не замечается.