← Índice

7.2 El estante compartido

Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.

Los errores más peligrosos son los que el compilador calla. No porque no los vea - es que simplemente no los comprueba: su dominio son los tipos y la posesión, no la corrección del comportamiento. POST /tasks devolvió una tarea. GET /tasks devolvió una lista vacía. El compilador está satisfecho. No es su error - es un error de diseño.

Lo que necesitas

El primer problema está resuelto: State pasa el almacén a los handlers. Pero cada uno recibe su propia copia del vector. Aquí está el segundo problema: compartir entre hilos, de solo lectura por ahora.

Arc<T> es un tipo de posesión compartida: Arc::clone no copia los datos - crea otro propietario de los mismos datos. Cómo funciona exactamente está en el Lore al final de este capítulo, si quieres leerlo antes del código.

Arc tiene un límite: permite llamar a métodos que toman &self - list y get_task solo leen, y con eso les basta. add y done modifican datos - de momento seguirán siendo stubs.

La construcción

Envuelve Vec<Task> en Arc. Añade eprintln! a list y get_task - mostrará la dirección del vector en memoria y demostrará que las peticiones están mirando el mismo almacén. add y done también cambian de tipo, pero siguen siendo stubs. En crates/api/src/routes.rs:

// crates/api/src/routes.rs - CAMBIADO
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: la tarea no se guarda - Arc no permite &mut
async fn add(_: State<Arc<Vec<Task>>>, Json(req): Json<AddRequest>) -> Json<Task> {
    Json(Task::new(0, &req.title).unwrap())
}

// CAMBIADO: Arc<Vec<Task>>; eprintln! mostrará la dirección - será la misma para todas las peticiones
async fn list(State(tasks): State<Arc<Vec<Task>>>) -> Json<Vec<Task>> {
    eprintln!("list: vec @ {:p}", tasks.as_ptr()); // {:p} - dirección del vector en memoria
    Json(tasks.to_vec())
}

// CAMBIADO: Arc<Vec<Task>>; eprintln! para la misma prueba
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: el estado no cambia - Arc no permite &mut
async fn done(_: State<Arc<Vec<Task>>>, Path(id): Path<u64>) -> Json<Task> {
    Json(Task::new(id, "stub").unwrap())
}

// CAMBIADO: router acepta 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)
}

En crates/api/src/main.rs - un único almacén, envuelto en Arc:

// crates/api/src/main.rs - CAMBIADO
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![ // CAMBIADO: Arc<Vec<Task>> en lugar de 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();
}

El resultado

El servidor en un terminal, dos peticiones en otro:

$ 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",...}

En el terminal del servidor:

list: vec @ 0x55f3a1b2c4d0
get:  vec @ 0x55f3a1b2c4d0   ← misma dirección

Ambas peticiones trabajan con el mismo vector.

add y done siguen siendo stubs: cómo modificar datos compartidos con seguridad es el siguiente paso.

El código completo de tq para este capítulo está en 7-many-hands/02-one-handle/.


Lore: Arc y el contador de referencias

Arc son las siglas de Atomically Reference Counted (conteo atómico de referencias). Cada Arc::clone incrementa el contador de forma atómica; cuando el último propietario sale del alcance, el contador cae a cero y los datos se liberan. Atómico significa que varios hilos pueden crear y destruir propietarios simultáneamente. El coste son dos incrementos atómicos por clon; una referencia ordinaria no lleva ningún coste de ese tipo. Para la mayoría de las aplicaciones pasa desapercibido.