Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
11 changes: 5 additions & 6 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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

Expand Down
6 changes: 6 additions & 0 deletions diagrams/10-modelo-de-actores.mmd
Original file line number Diff line number Diff line change
@@ -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]
94 changes: 94 additions & 0 deletions docs/10-modelo-de-actores.md
Original file line number Diff line number Diff line change
@@ -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<dyn std::error::Error>>(())
```

## 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/).
2 changes: 1 addition & 1 deletion docs/superpowers/plans/2026-07-28-rust-async-course.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
25 changes: 25 additions & 0 deletions examples/actor_counter_basic.rs
Original file line number Diff line number Diff line change
@@ -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");
}
20 changes: 20 additions & 0 deletions examples/soluciones/actor_increment.rs
Original file line number Diff line number Diff line change
@@ -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");
}
27 changes: 27 additions & 0 deletions examples/soluciones/actor_multiple_queries.rs
Original file line number Diff line number Diff line change
@@ -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");
}
13 changes: 13 additions & 0 deletions examples/soluciones/actor_shutdown.rs
Original file line number Diff line number Diff line change
@@ -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");
}
Loading