Skip to content

Commit 64516e5

Browse files
am-kantoxwarp-agent
andcommitted
feat: add send_info_after() to GenServerContext for scheduled info messages
- Add send_info_after() method to GenServerContext for scheduling info messages - Add whereis() method to GenServerContext for name lookup - Update gen_server_self_message example to demonstrate scheduled messaging - Example now shows both immediate (cast) and scheduled (send_info_after) patterns - Properly wraps messages in GenServerMsg::Info for handle_info() delivery This enables GenServers to easily schedule periodic tasks and delayed operations to themselves using the scheduler, matching Erlang's timer:send_after/2 pattern. Co-Authored-By: Warp <agent@warp.dev>
1 parent 06c5bc2 commit 64516e5

2 files changed

Lines changed: 271 additions & 9 deletions

File tree

joerl/examples/gen_server_self_message.rs

Lines changed: 122 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
//! Example: GenServer sending messages to itself
22
//!
3-
//! Demonstrates a GenServer that sends cast messages to itself to implement
4-
//! a self-incrementing counter loop.
3+
//! Demonstrates two patterns for GenServer self-messaging:
4+
//! 1. Using cast() to send immediate messages to itself
5+
//! 2. Using send_after() to schedule delayed messages (via scheduler)
56
//!
67
//! This pattern is useful for:
78
//! - Background processing loops
8-
//! - Periodic tasks without using timers
9+
//! - Periodic tasks and timers
910
//! - State machines that need to trigger their own state transitions
1011
//! - Work queues that process items sequentially
1112
1213
use async_trait::async_trait;
13-
use joerl::gen_server::{CallResponse, GenServer, GenServerContext, GenServerRef, spawn};
14-
use joerl::{ActorSystem, ExitReason};
14+
use joerl::gen_server::{spawn, CallResponse, GenServer, GenServerContext, GenServerRef};
15+
use joerl::{ActorSystem, ExitReason, Message};
1516
use std::sync::Arc;
17+
use std::time::Duration;
1618

1719
// ============================================================================
18-
// Self-Incrementing Counter
20+
// Example 1: Immediate self-messaging with cast()
1921
// ============================================================================
2022

2123
struct Counter;
@@ -107,12 +109,108 @@ impl GenServer for Counter {
107109
}
108110
}
109111

112+
// ============================================================================
113+
// Example 2: Scheduled self-messaging with send_after()
114+
// ============================================================================
115+
116+
struct Ticker;
117+
118+
#[derive(Debug)]
119+
enum TickerCall {
120+
Start,
121+
}
122+
123+
#[derive(Debug)]
124+
enum TickerCast {}
125+
126+
#[async_trait]
127+
impl GenServer for Ticker {
128+
type State = (u32, u32); // (tick_count, max_ticks)
129+
type Call = TickerCall;
130+
type Cast = TickerCast;
131+
type CallReply = u32;
132+
133+
async fn init(&mut self, _ctx: &mut GenServerContext<'_, Self>) -> Self::State {
134+
println!("[Ticker] Initializing");
135+
(0, 5) // Start at 0, max 5 ticks
136+
}
137+
138+
async fn handle_call(
139+
&mut self,
140+
call: Self::Call,
141+
state: &mut Self::State,
142+
ctx: &mut GenServerContext<'_, Self>,
143+
) -> CallResponse<Self::CallReply> {
144+
match call {
145+
TickerCall::Start => {
146+
println!("[Ticker] Starting periodic ticker");
147+
// Schedule first tick to ourselves after 100ms using send_info_after
148+
let timer_ref = ctx.send_info_after(
149+
Box::new("tick"),
150+
Duration::from_millis(100),
151+
);
152+
println!("[Ticker] Scheduled timer: {:?}", timer_ref);
153+
CallResponse::Reply(state.0)
154+
}
155+
}
156+
}
157+
158+
async fn handle_cast(
159+
&mut self,
160+
_cast: Self::Cast,
161+
_state: &mut Self::State,
162+
_ctx: &mut GenServerContext<'_, Self>,
163+
) {
164+
// No casts for this server
165+
}
166+
167+
async fn handle_info(
168+
&mut self,
169+
msg: Message,
170+
state: &mut Self::State,
171+
ctx: &mut GenServerContext<'_, Self>,
172+
) {
173+
// Handle scheduled "tick" messages
174+
if let Some(tick) = msg.downcast_ref::<&str>()
175+
&& *tick == "tick"
176+
{
177+
state.0 += 1;
178+
println!("[Ticker] Tick #{}", state.0);
179+
180+
if state.0 < state.1 {
181+
// Schedule next tick after 100ms using send_info_after
182+
ctx.send_info_after(Box::new("tick"), Duration::from_millis(100));
183+
} else {
184+
println!("[Ticker] Reached max ticks ({}), stopping", state.1);
185+
ctx.stop(ExitReason::Normal);
186+
}
187+
}
188+
}
189+
190+
async fn terminate(
191+
&mut self,
192+
reason: &ExitReason,
193+
state: &mut Self::State,
194+
_ctx: &mut GenServerContext<'_, Self>,
195+
) {
196+
println!(
197+
"[Ticker] Terminated with reason: {}, final ticks: {}",
198+
reason, state.0
199+
);
200+
}
201+
}
202+
110203
#[tokio::main]
111204
async fn main() {
112-
println!("=== GenServer Self-Messaging Example ===\n");
205+
println!("=== GenServer Self-Messaging Examples ===\n");
113206

114207
let system = Arc::new(ActorSystem::new());
115208

209+
// ========================================================================
210+
// Example 1: Immediate self-messaging with cast()
211+
// ========================================================================
212+
println!("--- Example 1: Immediate Self-Messaging ---");
213+
116214
// Spawn the counter GenServer
117215
let counter_ref = spawn(&system, Counter);
118216
println!("[Main] Spawned counter with PID: {}\n", counter_ref.pid());
@@ -129,7 +227,22 @@ async fn main() {
129227
.await;
130228

131229
// Wait for the counter to finish (it will stop itself at max value)
132-
tokio::time::sleep(tokio::time::Duration::from_millis(800)).await;
230+
tokio::time::sleep(Duration::from_millis(200)).await;
231+
232+
// ========================================================================
233+
// Example 2: Scheduled self-messaging with send_after()
234+
// ========================================================================
235+
println!("\n--- Example 2: Scheduled Self-Messaging (Ticker) ---");
236+
237+
// Spawn the ticker GenServer
238+
let ticker_ref = spawn(&system, Ticker);
239+
println!("[Main] Spawned ticker with PID: {}\n", ticker_ref.pid());
240+
241+
// Start the periodic ticker
242+
let _ = ticker_ref.call(TickerCall::Start).await;
243+
244+
// Wait for ticks to complete (5 ticks * 100ms + buffer)
245+
tokio::time::sleep(Duration::from_millis(650)).await;
133246

134-
println!("\n=== Example completed ===");
247+
println!("\n=== All examples completed ===");
135248
}

joerl/src/gen_server.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,155 @@ impl<'a, G: GenServer> GenServerContext<'a, G> {
404404
.take()
405405
.expect("reply_handle() called outside handle_call or already taken")
406406
}
407+
408+
/// Looks up a process by name.
409+
///
410+
/// Returns the Pid if the name is registered, or None otherwise.
411+
///
412+
/// In Erlang: `whereis(Name)`
413+
pub fn whereis(&self, name: &str) -> Option<Pid> {
414+
self.actor_ctx.whereis(name)
415+
}
416+
417+
/// Schedules a message to be sent after a delay.
418+
///
419+
/// Returns a TimerRef that can be used to cancel the timer.
420+
///
421+
/// In Erlang: `erlang:send_after(Time, Dest, Message)`
422+
///
423+
/// # Examples
424+
///
425+
/// ```rust
426+
/// use joerl::gen_server::{GenServer, GenServerContext, CallResponse};
427+
/// use joerl::scheduler::Destination;
428+
/// use joerl::Message;
429+
/// use async_trait::async_trait;
430+
/// use std::time::Duration;
431+
///
432+
/// struct TimerServer;
433+
///
434+
/// #[derive(Debug)]
435+
/// enum Call { Start }
436+
///
437+
/// #[derive(Debug)]
438+
/// enum Cast {}
439+
///
440+
/// #[async_trait]
441+
/// impl GenServer for TimerServer {
442+
/// type State = u32;
443+
/// type Call = Call;
444+
/// type Cast = Cast;
445+
/// type CallReply = ();
446+
///
447+
/// async fn init(&mut self, _ctx: &mut GenServerContext<'_, Self>) -> Self::State {
448+
/// 0
449+
/// }
450+
///
451+
/// async fn handle_call(
452+
/// &mut self,
453+
/// _call: Self::Call,
454+
/// _state: &mut Self::State,
455+
/// ctx: &mut GenServerContext<'_, Self>,
456+
/// ) -> CallResponse<Self::CallReply> {
457+
/// // Schedule a message to ourselves
458+
/// ctx.send_after(
459+
/// Destination::Pid(ctx.pid()),
460+
/// Box::new("tick"),
461+
/// Duration::from_secs(1)
462+
/// );
463+
/// CallResponse::Reply(())
464+
/// }
465+
///
466+
/// async fn handle_cast(&mut self, _cast: Self::Cast, _state: &mut Self::State, _ctx: &mut GenServerContext<'_, Self>) {}
467+
/// }
468+
/// ```
469+
pub fn send_after(
470+
&self,
471+
dest: crate::scheduler::Destination,
472+
msg: Message,
473+
duration: std::time::Duration,
474+
) -> Option<crate::scheduler::TimerRef> {
475+
self.actor_ctx.send_after(dest, msg, duration)
476+
}
477+
478+
/// Schedules an info message to be sent to this GenServer after a delay.
479+
///
480+
/// This is a convenience wrapper that properly wraps the message in `GenServerMsg::Info`
481+
/// so it will be delivered to `handle_info()`. Use this instead of `send_after()` when
482+
/// scheduling messages to GenServers.
483+
///
484+
/// Returns a TimerRef that can be used to cancel the timer.
485+
///
486+
/// In Erlang: `erlang:send_after(Time, self(), Message)` for info messages
487+
///
488+
/// # Examples
489+
///
490+
/// ```rust
491+
/// use joerl::gen_server::{GenServer, GenServerContext, CallResponse};
492+
/// use joerl::Message;
493+
/// use async_trait::async_trait;
494+
/// use std::time::Duration;
495+
///
496+
/// struct TickerServer;
497+
///
498+
/// #[derive(Debug)]
499+
/// enum Call { Start }
500+
///
501+
/// #[derive(Debug)]
502+
/// enum Cast {}
503+
///
504+
/// #[async_trait]
505+
/// impl GenServer for TickerServer {
506+
/// type State = u32;
507+
/// type Call = Call;
508+
/// type Cast = Cast;
509+
/// type CallReply = ();
510+
///
511+
/// async fn init(&mut self, _ctx: &mut GenServerContext<'_, Self>) -> Self::State {
512+
/// 0
513+
/// }
514+
///
515+
/// async fn handle_call(
516+
/// &mut self,
517+
/// _call: Self::Call,
518+
/// _state: &mut Self::State,
519+
/// ctx: &mut GenServerContext<'_, Self>,
520+
/// ) -> CallResponse<Self::CallReply> {
521+
/// // Schedule an info message to ourselves
522+
/// ctx.send_info_after(
523+
/// Box::new("tick"),
524+
/// Duration::from_secs(1)
525+
/// );
526+
/// CallResponse::Reply(())
527+
/// }
528+
///
529+
/// async fn handle_cast(&mut self, _cast: Self::Cast, _state: &mut Self::State, _ctx: &mut GenServerContext<'_, Self>) {}
530+
///
531+
/// async fn handle_info(
532+
/// &mut self,
533+
/// msg: Message,
534+
/// state: &mut Self::State,
535+
/// _ctx: &mut GenServerContext<'_, Self>,
536+
/// ) {
537+
/// if let Some(_tick) = msg.downcast_ref::<&str>() {
538+
/// *state += 1;
539+
/// }
540+
/// }
541+
/// }
542+
/// ```
543+
pub fn send_info_after(
544+
&self,
545+
msg: Message,
546+
duration: std::time::Duration,
547+
) -> Option<crate::scheduler::TimerRef> {
548+
// Wrap the message in GenServerMsg::Info so handle_info will receive it
549+
let info_msg: GenServerMsg<G> = GenServerMsg::Info { message: msg };
550+
self.actor_ctx.send_after(
551+
crate::scheduler::Destination::Pid(self.pid()),
552+
Box::new(info_msg),
553+
duration,
554+
)
555+
}
407556
}
408557

409558
/// Internal message types for GenServer

0 commit comments

Comments
 (0)