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 docs/superpowers/plans/2026-07-28-rust-async-course.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Para cada capítulo, antes de pasar al siguiente:

- [ ] Capítulo 10: modelo de actores.
- [x] #34 Especificar propiedad, mensajes, fallas y alternativas.
- [ ] #36 Implementar y probar un modelo educativo mínimo.
- [x] #36 Implementar y probar un modelo educativo mínimo.
- [ ] #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
35 changes: 35 additions & 0 deletions src/actor_model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! Modelo educativo de un actor que posee un contador.

use tokio::sync::{mpsc, oneshot};
use tokio::task::JoinHandle;

/// Mensajes que entiende el actor contador.
pub enum CounterMessage {
/// Suma una cantidad al estado que posee el actor.
Increment(u64),
/// Devuelve el valor observado por el actor al procesar la consulta.
Get(oneshot::Sender<u64>),
}

/// Inicia un actor contador y devuelve su buzón y la tarea que lo ejecuta.
///
/// El actor termina al cerrar todos los emisores. `capacity` conserva el
/// backpressure de un canal acotado.
#[must_use]
pub fn spawn_counter(capacity: usize) -> (mpsc::Sender<CounterMessage>, JoinHandle<()>) {
let (sender, mut receiver) = mpsc::channel(capacity);
let task = tokio::spawn(async move {
let mut count = 0_u64;

while let Some(message) = receiver.recv().await {
match message {
CounterMessage::Increment(amount) => count += amount,
CounterMessage::Get(reply) => {
let _ = reply.send(count);
}
}
}
});

(sender, task)
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#![forbid(unsafe_code)]

pub mod actor_model;
pub mod async_channels;
pub mod cooperative;
pub mod coordination;
Expand Down
51 changes: 51 additions & 0 deletions tests/actor_model_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use rust_async::actor_model::{spawn_counter, CounterMessage};
use tokio::sync::oneshot;

#[tokio::test]
async fn actor_applies_messages_in_order_and_owns_its_state() {
let (sender, task) = spawn_counter(2);
sender
.send(CounterMessage::Increment(3))
.await
.expect("actor should accept the message");
sender
.send(CounterMessage::Increment(4))
.await
.expect("actor should accept the message");

let (reply_sender, reply_receiver) = oneshot::channel();
sender
.send(CounterMessage::Get(reply_sender))
.await
.expect("actor should accept the query");

assert_eq!(reply_receiver.await.expect("actor should reply"), 7);

drop(sender);
task.await.expect("actor task should finish cleanly");
}

#[tokio::test]
async fn actor_can_reply_to_more_than_one_query() {
let (sender, task) = spawn_counter(3);
let (first_reply_sender, first_reply_receiver) = oneshot::channel();
sender
.send(CounterMessage::Get(first_reply_sender))
.await
.expect("actor should accept the query");
assert_eq!(first_reply_receiver.await.expect("actor should reply"), 0);

sender
.send(CounterMessage::Increment(1))
.await
.expect("actor should accept the message");
let (second_reply_sender, second_reply_receiver) = oneshot::channel();
sender
.send(CounterMessage::Get(second_reply_sender))
.await
.expect("actor should accept the query");
assert_eq!(second_reply_receiver.await.expect("actor should reply"), 1);

drop(sender);
task.await.expect("actor task should finish cleanly");
}
Loading