Workshop 3: Runtime and Actor Lifecycle
Learning Objectives
By the end of this workshop, you will be able to:
- Trace startup flow - Follow the daemon from
main()to running actors - Explain initialization order - Know why actors start in a specific sequence
- Use handle patterns - Understand message-passing vs shared-state handles
- Coordinate shutdown - Know how graceful shutdown works across actors
- Run the daemon - Start and interact with a local ICN node
Goal
Trace the daemon startup path, understand actor initialization order, and explore how the supervisor manages the actor lifecycle.
Prerequisites
- Completed Module 3: Runtime Architecture
- ICN binaries built (
cargo build)
Estimated time
2-3 hours
Related Materials
- Module 3: Runtime Architecture - Background reading
- Workshop 4: Identity and Trust - Next workshop
- CLAUDE.md Architecture Section - Actor communication patterns
Part 1: Startup Path Walkthrough
Steps
- Open
icn/bins/icnd/src/main.rs - Identify the config loading and validation flow
- Find where the keystore is unlocked
- Trace the call to the runtime
Questions to answer
- What happens if the keystore passphrase is incorrect?
- How is the config file located?
- What signal handlers are registered?
Code to find
// Look for the runtime entry point
Runtime::new(config, identity).await?.run().await
Checkpoint
- You can identify where config is loaded
- You understand keystore unlock flow
Part 2: Supervisor Initialization
Steps
- Open
icn/crates/icn-core/src/supervisor/mod.rs - Find the
Supervisor::new()method - List all actors that are created
Expected initialization order
- Storage layer (Sled)
- Identity (load from keystore)
- Trust graph
- Network actor
- Gossip actor
- Ledger
- Gateway/RPC servers
Questions to answer
- Why does trust initialize before gossip?
- What would break if network started before identity?
- How are actors connected to each other?
Code to trace
// Find the actor wiring section
self.wire_network_to_gossip().await?;
self.wire_gossip_to_ledger().await?;
Checkpoint
- You can list the actor initialization order
- You understand why the order matters
Part 3: Actor Handle Pattern
Steps
Search for
Handlestructs in the codebase:grep -r "pub struct.*Handle" icn/crates/ --include="*.rs" | head -10Note: These shell commands are for learning purposes. When automating searches in ICN development, prefer using IDE search or dedicated tools.
Open
icn/crates/icn-gossip/src/gossip.rsFind the
GossipHandledefinitionIdentify how external code interacts with the actor
Questions to answer
- What channel type is used for actor communication?
- How does the handle pattern prevent shared mutable state?
- What happens when a handle's sender is dropped?
Actual pattern
Note: ICN uses two patterns for actor handles:
Message-passing handles (e.g.,
NetworkActor):// icn/crates/icn-net/src/actor/mod.rs pub struct NetworkHandle { tx: mpsc::Sender<NetworkCommand>, } impl NetworkHandle { pub async fn send_message(&self, msg: NetworkMessage) -> Result<()> { let (reply_tx, reply_rx) = oneshot::channel(); self.tx.send(NetworkCommand::Send { msg, reply: reply_tx }).await?; reply_rx.await? } }Shared-state handles (e.g.,
GossipActor):// icn/crates/icn-gossip/src/gossip.rs:1876 pub type GossipHandle = Arc<RwLock<GossipActor>>; // Usage requires acquiring lock: let mut gossip = gossip_handle.write().await; gossip.subscribe(topic, subscriber_did).await?;
The gossip actor uses the shared-state pattern with Arc<RwLock<>> rather than
message passing. Different actors use different patterns based on their needs.
Checkpoint
- You understand both handle patterns (message-passing and shared-state)
- You know which pattern GossipActor uses
Part 4: Shutdown Coordination
Steps
Search for shutdown handling:
grep -r "shutdown" icn/crates/icn-core/src/ --include="*.rs" | head -15Find the broadcast channel for shutdown signals
Trace how each actor receives the shutdown signal
Questions to answer
- What type of channel is used for shutdown?
- How do actors clean up resources on shutdown?
- What order do actors shut down?
Expected pattern
// See icn/crates/icn-core/src/runtime.rs:24 for broadcast channel creation
// Broadcast allows multiple receivers
let (shutdown_tx, _) = broadcast::channel::<()>(1);
// Each actor receives a receiver
let mut shutdown_rx = shutdown_tx.subscribe();
tokio::select! {
_ = shutdown_rx.recv() => {
// Cleanup and exit
}
// ... other branches
}
Checkpoint
- You can trace the shutdown signal path
- You understand graceful shutdown ordering
Part 5: Callback Wiring
Steps
- Open
icn/crates/icn-core/src/supervisor/mod.rs - Find the callback wiring code
- Trace how network messages reach the gossip actor
Diagram
sequenceDiagram
participant Remote as Remote Peer
participant Net as NetworkActor
participant TLS as TLS/QUIC
participant Handler as IncomingMessageHandler
participant Gossip as GossipActor
Remote->>Net: QUIC packet
Net->>TLS: Decrypt & verify
TLS-->>Net: Decrypted bytes
Net->>Net: Parse NetworkMessage
Net->>Handler: invoke callback(msg)
Handler->>Gossip: handle_message(gossip_msg)
Gossip-->>Handler: Result<()>
Handler-->>Net: Continue processing
Why callbacks? The callback pattern allows the supervisor to wire actors together without either actor knowing about the other's implementation details. This enables testing actors in isolation and swapping implementations.
Questions to answer
- Why use callbacks instead of direct actor references?
- How is the callback type defined (trait or closure)?
- What happens if the callback fails?
Checkpoint
- You can explain the network-to-gossip bridge
- You understand why callbacks enable loose coupling
Part 6: Running the Daemon
Steps
Build the daemon:
cd icn && cargo buildCreate a test config (or use default):
export ICN_DATA=$(mktemp -d) export ICN_PASSPHRASE="workshop-test" ./target/debug/icnctl --data-dir "$ICN_DATA" id initStart the daemon:
./target/debug/icnd --data-dir "$ICN_DATA"In another terminal, check status:
./target/debug/icnctl --data-dir "$ICN_DATA" status
Expected output
- Daemon starts and logs initialization steps
- Status shows node DID and connection info
Questions to answer
- What log messages appear during startup?
- What ports are opened?
- What happens if you start a second daemon with the same data dir?
Checkpoint
- You successfully started the daemon
- You can query its status
Cleanup
rm -rf "$ICN_DATA"
Summary
After completing this workshop you should be able to:
- Trace the complete daemon startup path
- Explain the actor initialization order and why it matters
- Understand the handle pattern for actor communication
- Describe how shutdown is coordinated across actors
- Run and interact with the daemon
Key Takeaways
| Concept | Key Point |
|---|---|
| Startup Order | Storage → Identity → Trust → Network → Gossip → Ledger → Gateway |
| Handle Patterns | Message-passing (mpsc) for commands, Arc |
| Shutdown | Broadcast channel notifies all actors; reverse initialization order |
| Callback Wiring | Enables loose coupling; supervisor connects actors without dependencies |
| Config Loading | Data directory contains keystore, config, and database |
Try It Yourself
Challenge 1: Add debug logging to trace message flow
RUST_LOG=icn_gossip=debug,icn_net=debug ./target/debug/icnd --data-dir "$ICN_DATA"
Watch the logs as you interact with the daemon. Can you identify:
- When a peer connects?
- When gossip messages are exchanged?
- When the ledger processes an entry?
Challenge 2: Explore the actor handles
Use grep to find all the different handle types in the codebase. Which ones use
message-passing and which use shared state?
Troubleshooting
"Failed to unlock keystore"
Ensure ICN_PASSPHRASE is set correctly before starting the daemon.
"Address already in use"
Another process (possibly another icnd) is using the port. Check with lsof -i :<port>.
"Config file not found"
Use --data-dir to specify where ICN should look for configuration.
Next steps
Proceed to Workshop 4: Identity and Trust Hands-On