Step 5 of 7
Shared state: an in-memory store
A real server handles many requests at the same time, on several threads (or async tasks), and they all need the same data: the users, the items, the counters. That's the shared state problem from the concurrency modules, and the solution is the same: wrap the data in a Mutex and share it with an Arc.
use std::collections::BTreeMap;
use std::sync::{Arc, Mutex};
use std::thread;
#[derive(Default)]
struct Store {
notes: BTreeMap<u32, String>,
next_id: u32,
}
#[derive(Default)]
struct AppState {
store: Mutex<Store>,
}
fn create_note(state: &AppState, text: &str) -> u32 {
let mut store = state.store.lock().unwrap();
store.next_id += 1;
let id = store.next_id;
store.notes.insert(id, text.to_string());
id
}
fn count_notes(state: &AppState) -> usize {
state.store.lock().unwrap().notes.len()
}
fn main() {
let state = Arc::new(AppState::default());
let mut clients = Vec::new();
for client in 0..4 {
let state = Arc::clone(&state);
clients.push(thread::spawn(move || {
for i in 0..25 {
create_note(&state, &format!("note {i} from client {client}"));
}
}));
}
for c in clients {
c.join().unwrap();
}
println!("{} notes", count_notes(&state));
let store = state.store.lock().unwrap();
println!("ids {} to {}", store.notes.keys().next().unwrap(), store.notes.keys().last().unwrap());
}
100 notes
ids 1 to 100
Piece by piece:
Storeholds the data and the id counter, behind oneMutex. Taking the next id and inserting happen under the same lock, so two requests can never get the same id. (With two separate locks, another thread could slip in between.)- Handlers take
&AppState. They don't care that it lives in anArc; the server (here, our four threads standing in for clients) owns theArcand hands out references. Web frameworks do exactly this. lock().unwrap()is accepted practice:lockonly fails if another thread panicked while holding the lock ("poisoning"), and then crashing loudly is reasonable.- Keep the lock for as short a time as possible: do the parsing and validation before locking, and don't do slow work (network calls, big computations) while holding it. Every other request waits meanwhile.
- Lots of reads and few writes?
RwLocklets many readers in at once. In production, this in-memoryStorebecomes a database connection pool (likesqlx::PgPool), which is already safe to share; the handlers keep the same shape.
Common mistakes
- Two locks for one operation (check-then-act):
if !store.lock().unwrap().contains(..) { store.lock().unwrap().insert(..) }lets another thread insert in between. Lock once, do both. - Locking the same
Mutextwice in one call, for example calling a helper that locks while you still hold the guard: the thread waits for itself forever (a deadlock).std::sync::Mutexisn't reentrant. - Forgetting
Arcand trying to move the state into several threads:error[E0382]: use of moved value: `state`.
Your task
Write handle for an inventory API. The types and helpers are ready; the routes are:
GET /items:200, a JSON array of all items in id orderPOST /itemswith{"name": "mug", "qty": 3}:201and the new item (ids start at 1); a body that doesn't parse:400{"error":"invalid JSON"}GET /items/{id}:200and the item, or404{"error":"item 7 not found"}DELETE /items/{id}:204with an empty body, or the same404- an id that isn't a number:
400{"error":"bad id"}; a known path with another method:405{"error":"method not allowed"}; anything else:404{"error":"not found"}
Challenge 2
Add replacement: write fn update_item(state: &AppState, id: u32, body: &str) -> Response for PUT /items/{id}. The body is a full item without id ({"name": "big mug", "qty": 2}). Answer 400 {"error":"invalid JSON"} if it doesn't parse, 404 {"error":"item 7 not found"} if there's no such item, otherwise replace its name and qty and answer 200 with the updated item.
Challenge 3
Protect the API with a rate limiter: each client may make limit requests. Write fn check(&self, client: &str) -> Response on Limiter: count the request; while the client's count is within the limit answer 200 with {"remaining":<requests left>}, after that 429 with {"error":"too many requests"}. It must be correct when many threads call it at once.