7.2 The Shared Shelf
The most dangerous errors are the ones the compiler stays silent about. Not because it does
not see them - it simply does not check them: its domain is types and ownership, not
correctness of behaviour. POST /tasks returned a task. GET /tasks returned an empty
list. The compiler is satisfied. That is not its mistake - it is a mistake of design.
What You Need
The first problem is solved: State passes the store to the handlers. But each gets its
own copy of the vector. Here is the second problem: sharing between threads, read-only for
now.
Arc<T> is a shared-ownership type: Arc::clone does not copy the data - it creates
another owner of the same data. How exactly this works is in the Lore at the end of this
chapter, if you want to read it before the code.
Arc has a boundary: it allows calling methods that take &self - list and get_task
only read, and that is enough for them. add and done modify data - they will stay as
stubs for now.
The Build
Wrap Vec<Task> in Arc. Add eprintln! to list and get_task - it will show the
vector’s address in memory and prove that requests are looking at the same store. add and
done change type too, but remain stubs. In 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,
}
// stub: task is not saved - Arc does not allow &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! will show the address - it will be the same for all requests
async fn list(State(tasks): State<Arc<Vec<Task>>>) -> Json<Vec<Task>> {
eprintln!("list: vec @ {:p}", tasks.as_ptr()); // {:p} - the vector's address in memory
Json(tasks.to_vec())
}
// CHANGED: Arc<Vec<Task>>; eprintln! for the same proof
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())
}
// stub: status is not changed - Arc does not allow &mut
async fn done(_: State<Arc<Vec<Task>>>, Path(id): Path<u64>) -> Json<Task> {
Json(Task::new(id, "stub").unwrap())
}
// CHANGED: router accepts 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)
}
In crates/api/src/main.rs - one store, wrapped in 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>> instead of 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();
}
The Result
Server in one terminal, two requests in another:
$ 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",...}
In the server terminal:
list: vec @ 0x55f3a1b2c4d0
get: vec @ 0x55f3a1b2c4d0 ← same address
Both requests are working with the same vector.
add and done are still stubbed: how to safely modify shared data is the next step.
The complete
tqcode for this chapter is in7-many-hands/02-one-handle/.
Lore: Arc and the Reference Count
Arc stands for Atomically Reference Counted. Each Arc::clone atomically increments
the counter; when the last owner goes out of scope, the counter drops to zero and the data
is freed. Atomic means that multiple threads can create and destroy owners simultaneously.
The cost is two atomic increments per clone; an ordinary reference carries no such cost.
For most applications it goes unnoticed.