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
- but the next
listdoes not see it. The server terminal shows why:
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
tqcode for this chapter is in7-many-hands/01-the-state-two-threads-share/.