From 79c197a0b08776348ed19cedf6f12e8386c0a22c Mon Sep 17 00:00:00 2001 From: Joel Alvarez Date: Tue, 28 Jul 2026 13:22:00 -0700 Subject: [PATCH] docs: add actor model chapter --- README.md | 2 +- ROADMAP.md | 11 +-- diagrams/10-modelo-de-actores.mmd | 6 ++ docs/10-modelo-de-actores.md | 94 +++++++++++++++++++ .../plans/2026-07-28-rust-async-course.md | 2 +- examples/actor_counter_basic.rs | 25 +++++ examples/soluciones/actor_increment.rs | 20 ++++ examples/soluciones/actor_multiple_queries.rs | 27 ++++++ examples/soluciones/actor_shutdown.rs | 13 +++ 9 files changed, 192 insertions(+), 8 deletions(-) create mode 100644 diagrams/10-modelo-de-actores.mmd create mode 100644 docs/10-modelo-de-actores.md create mode 100644 examples/actor_counter_basic.rs create mode 100644 examples/soluciones/actor_increment.rs create mode 100644 examples/soluciones/actor_multiple_queries.rs create mode 100644 examples/soluciones/actor_shutdown.rs diff --git a/README.md b/README.md index 7d84894..431b13e 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ RFC-0001 §10. | 07 | Tokio y runtime de producción | planned | | 08 | select!, cancelación y timeouts | planned | | 09 | Canales y sincronización asíncrona | draft | -| 10 | Modelo de actores | planned | +| 10 | Modelo de actores | draft | Estados posibles: `planned`, `draft`, `implemented`, `tested`, `benchmarked`, `reviewed`, `published`. diff --git a/ROADMAP.md b/ROADMAP.md index 396fd9e..bffd32c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -6,11 +6,10 @@ por terminar (RFC-0001 §1). ## Estado Actual -La fundación y los capítulos 01–08 están implementados en modo `draft`. El -capítulo 09 cuenta con diseño y modelo de canales; su material educativo se -mantiene como borrador hasta la revisión humana. El siguiente paso natural es -el issue #34, que especifica propiedad, mensajes, fallas y alternativas para -el modelo de actores. +La fundación y los capítulos 01–10 están implementados en modo `draft`. El +material educativo se mantiene como borrador hasta la revisión humana. El +siguiente paso natural es el issue #40, para completar glosario, ruta de +lectura y referencias cruzadas antes de la revisión transversal del curso. El checklist detallado vive en [`docs/superpowers/plans/2026-07-28-rust-async-course.md`](docs/superpowers/plans/2026-07-28-rust-async-course.md). @@ -32,7 +31,7 @@ los issues accionables y debe mantener su vista principal agrupada por | 07 | Tokio y runtime de producción | planned | | 08 | select!, cancelación y timeouts | planned | | 09 | Canales y sincronización asíncrona | draft | -| 10 | Modelo de actores | planned | +| 10 | Modelo de actores | draft | ## Alineación RFC-0001 diff --git a/diagrams/10-modelo-de-actores.mmd b/diagrams/10-modelo-de-actores.mmd new file mode 100644 index 0000000..0a73b5a --- /dev/null +++ b/diagrams/10-modelo-de-actores.mmd @@ -0,0 +1,6 @@ +flowchart LR + A[Clientes] --> M[Buzón acotado] + M --> T[Task actor] + T --> S[Estado privado] + T --> R[Respuesta oneshot] + M --> B[Backpressure] diff --git a/docs/10-modelo-de-actores.md b/docs/10-modelo-de-actores.md new file mode 100644 index 0000000..12720e5 --- /dev/null +++ b/docs/10-modelo-de-actores.md @@ -0,0 +1,94 @@ +# Modelo de Actores + +> **Curso:** Rust Async · **Capítulo:** 10 · **Prerequisitos:** capítulos 07–09 · **Código:** `src/actor_model.rs` · **Estado:** draft + +## Concepto + +Un actor es una tarea que posee su estado y procesa mensajes de uno en uno. +Sus clientes no cambian ese estado directamente: piden transiciones mediante un +buzón. Esta frontera convierte la propiedad en una decisión visible del diseño. + +## Problema + +Un canal mueve trabajo, pero no dice quién puede mutar el estado que representa +ese trabajo. Si varias tareas comparten un contador, un inventario o una sesión, +deben coordinar cada acceso. Un actor concentra esa responsabilidad en una +sola task, donde el orden de mensajes decide el orden de las transiciones. + +```mermaid +flowchart LR + A[Cliente A] --> M[Buzón acotado] + B[Cliente B] --> M + M --> T[Task actor] + T --> S[Estado privado] + T --> R[Respuesta oneshot] +``` + +## Alternativas y Decisión + +Un `Mutex` es apropiado para una sección crítica pequeña y un canal simple es +apropiado para transferir trabajo. Un actor combina ambas necesidades cuando el +protocolo, el ciclo de vida y la propiedad del estado deben ser explícitos. El +precio es una cola adicional: las operaciones de un actor no se ejecutan en +paralelo entre sí. + +El modelo usa `tokio::mpsc` para el buzón y `oneshot` para las consultas. El +contador vive dentro de la task; por tanto, cada consulta recibe el valor que el +actor observa cuando procesa su mensaje, no una lectura especulativa del +cliente. + +## Modelo + +`CounterMessage::Increment` solicita una transición y +`CounterMessage::Get` transporta el canal de respuesta de una consulta. El +actor termina cuando se cierran todos los emisores, después de procesar los +mensajes que ya aceptó. + +```rust +use rust_async::actor_model::{spawn_counter, CounterMessage}; +use tokio::sync::oneshot; + +let (sender, task) = spawn_counter(2); +sender.send(CounterMessage::Increment(3)).await?; +let (reply, response) = oneshot::channel(); +sender.send(CounterMessage::Get(reply)).await?; +assert_eq!(response.await?, 3); +drop(sender); +task.await?; +# Ok::<(), Box>(()) +``` + +## Ejemplo Progresivo + +`actor_counter_basic` inicia el actor, suma dos cantidades, consulta su estado +y cierra el buzón. Las soluciones recorren un incremento, varias consultas y el +cierre controlado. Cada una trata los errores de envío o respuesta como parte +del protocolo, no como un caso imposible. + +## Ejercicios + +1. Agrega un mensaje `Reset` y explica qué consultas pueden observar antes o + después de esa transición. +2. Define un mensaje que devuelva el valor anterior y el nuevo en una sola + respuesta. +3. Diseña la estrategia de un cliente cuando el buzón está lleno: esperar, + rechazar o agrupar trabajo. +4. Propón qué mensajes, métricas y persistencia necesitaría un actor de + reservas antes de considerarlo apto para producción. + +## Benchmark + +No se añade un benchmark. El costo relevante depende de la capacidad del +buzón, el número de clientes, el trabajo por mensaje y la política del runtime. +Un microbenchmark de contador local no justificaría decisiones de arquitectura. +Una medición futura deberá declarar la carga, el hardware y el comportamiento +ante saturación. + +## Límites y Referencias + +Este actor no ofrece supervisión, reinicio, persistencia ni entrega confiable. +Una tarea que falla y un canal de respuesta cerrado son fallas distintas que el +cliente debe decidir cómo manejar. + +Referencias: [Tokio mpsc](https://docs.rs/tokio/latest/tokio/sync/mpsc/) y +[Tokio oneshot](https://docs.rs/tokio/latest/tokio/sync/oneshot/). diff --git a/docs/superpowers/plans/2026-07-28-rust-async-course.md b/docs/superpowers/plans/2026-07-28-rust-async-course.md index b39ea8a..6f950c9 100644 --- a/docs/superpowers/plans/2026-07-28-rust-async-course.md +++ b/docs/superpowers/plans/2026-07-28-rust-async-course.md @@ -85,7 +85,7 @@ Para cada capítulo, antes de pasar al siguiente: - [ ] Capítulo 10: modelo de actores. - [x] #34 Especificar propiedad, mensajes, fallas y alternativas. - [x] #36 Implementar y probar un modelo educativo mínimo. - - [ ] #38 Escribir capítulo, diagrama, ejemplos y ejercicios. + - [x] #38 Escribir capítulo, diagrama, ejemplos y ejercicios. - [ ] Completar ruta de lectura, glosario, referencias cruzadas y verificación final de coherencia del curso. diff --git a/examples/actor_counter_basic.rs b/examples/actor_counter_basic.rs new file mode 100644 index 0000000..11d06fc --- /dev/null +++ b/examples/actor_counter_basic.rs @@ -0,0 +1,25 @@ +use rust_async::actor_model::{spawn_counter, CounterMessage}; +use tokio::sync::oneshot; + +#[tokio::main] +async fn main() { + let (sender, task) = spawn_counter(2); + sender + .send(CounterMessage::Increment(3)) + .await + .expect("actor abierto"); + sender + .send(CounterMessage::Increment(4)) + .await + .expect("actor abierto"); + + let (reply, response) = oneshot::channel(); + sender + .send(CounterMessage::Get(reply)) + .await + .expect("actor abierto"); + println!("contador: {}", response.await.expect("actor responde")); + + drop(sender); + task.await.expect("actor termina correctamente"); +} diff --git a/examples/soluciones/actor_increment.rs b/examples/soluciones/actor_increment.rs new file mode 100644 index 0000000..2b406be --- /dev/null +++ b/examples/soluciones/actor_increment.rs @@ -0,0 +1,20 @@ +use rust_async::actor_model::{spawn_counter, CounterMessage}; +use tokio::sync::oneshot; + +#[tokio::main] +async fn main() { + let (sender, task) = spawn_counter(2); + sender + .send(CounterMessage::Increment(1)) + .await + .expect("actor abierto"); + let (reply, response) = oneshot::channel(); + sender + .send(CounterMessage::Get(reply)) + .await + .expect("actor abierto"); + assert_eq!(response.await.expect("actor responde"), 1); + + drop(sender); + task.await.expect("actor termina correctamente"); +} diff --git a/examples/soluciones/actor_multiple_queries.rs b/examples/soluciones/actor_multiple_queries.rs new file mode 100644 index 0000000..4a6f386 --- /dev/null +++ b/examples/soluciones/actor_multiple_queries.rs @@ -0,0 +1,27 @@ +use rust_async::actor_model::{spawn_counter, CounterMessage}; +use tokio::sync::oneshot; + +#[tokio::main] +async fn main() { + let (sender, task) = spawn_counter(3); + let (first_reply, first_response) = oneshot::channel(); + sender + .send(CounterMessage::Get(first_reply)) + .await + .expect("actor abierto"); + assert_eq!(first_response.await.expect("actor responde"), 0); + + sender + .send(CounterMessage::Increment(5)) + .await + .expect("actor abierto"); + let (second_reply, second_response) = oneshot::channel(); + sender + .send(CounterMessage::Get(second_reply)) + .await + .expect("actor abierto"); + assert_eq!(second_response.await.expect("actor responde"), 5); + + drop(sender); + task.await.expect("actor termina correctamente"); +} diff --git a/examples/soluciones/actor_shutdown.rs b/examples/soluciones/actor_shutdown.rs new file mode 100644 index 0000000..67ee8c2 --- /dev/null +++ b/examples/soluciones/actor_shutdown.rs @@ -0,0 +1,13 @@ +use rust_async::actor_model::{spawn_counter, CounterMessage}; + +#[tokio::main] +async fn main() { + let (sender, task) = spawn_counter(1); + sender + .send(CounterMessage::Increment(1)) + .await + .expect("actor abierto"); + + drop(sender); + task.await.expect("actor termina al cerrar el buzón"); +}