← Table of contents

7.1 The State Two Threads Share

In a workshop with several craftsmen, one ledger sits on a shared shelf. Not at the first craftsman’s bench, not at the second’s - on the shelf, where everyone has access. Otherwise each keeps their own record, and by evening no one knows the full picture. When there is no shared shelf, everyone has their own version. And everyone having their own version means no one has the truth.

What You Need

To connect the store to the server, three problems must be solved: how to pass it to the handlers, how to share it across multiple threads, and how to safely modify it from any of them. Each is a separate chapter. This is the first.

Handlers are functions that axum calls once per incoming request. add, list, get_task know nothing about main() and have no access to its variables. A store is needed that outlives a single request and is accessible to all handlers at once. Before wiring up TaskStore, the mechanism itself will be demonstrated with Vec<Task> standing in as the store.

axum provides State for this: .with_state() attaches an object to the router, and the State<S> extractor - the same principle as Path and Json - retrieves it in a handler. State<S> requires S to implement Clone: axum clones the state on every handler call.

The Build

Vec<T> implements Clone only when T: Clone - so Clone is needed on Task, and on Status too, since it is a field of Task. In crates/core/src/task/mod.rs:

// CHANGED: Clone is needed so Vec<Task> can implement Clone
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Status { Todo, Done }

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Task { ... }

In crates/api/src/routes.rs, replace the stubs with State<Vec<Task>>. Add eprintln! to add and list - it will print the vector’s address in memory and make visible what happens to the data on each request:

// 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 passes ownership of the clone; mut is needed to modify the vec
async fn add(State(mut tasks): State<Vec<Task>>, Json(req): Json<AddRequest>) -> Json<Task> {
    eprintln!("add:  vec @ {:p}", tasks.as_ptr()); // {:p} - the vector's address in memory
    let id = (tasks.len() + 1) as u64;
    let task = Task::new(id, &req.title).unwrap();
    tasks.push(task.clone()); // clone: the task is needed both in the vec and in the response
    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) // attaches the state to the router
}

In crates/api/src/main.rs, assemble the initial state and pass it to the router:

// 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![ // initial state: axum clones it into every request
        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();
}

The Result

Server in one terminal (make serve), three requests in another:

$ 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 has vanished. The server accepted the request, returned the task with id 3

list: vec @ 0x55f3a1b2c4d0
add:  vec @ 0x55f3a1b2c5e8   ← different address
list: vec @ 0x55f3a1b2c6a0   ← different again

Every request received its own clone of the vector. add added the task to its clone - and took it away when the request finished. The next list knew nothing of call dentist.

Clone satisfied the compiler. Every handler now has a ledger - just its own. The ledger on the shared shelf is the next step.

The complete tq code for this chapter is in 7-many-hands/01-the-state-two-threads-share/.