Skip to content

Latest commit

Β 

History

History
391 lines (302 loc) Β· 11.3 KB

File metadata and controls

391 lines (302 loc) Β· 11.3 KB

Distributed Clustering Implementation Summary

This document summarizes the implementation of distributed clustering and location transparency in joerl.

Overview

joerl now supports distributed actor systems inspired by Erlang/OTP, enabling actors to communicate transparently across multiple nodes. The implementation follows a layered architecture:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Application Layer (Your Actors)      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  ActorSystem (Unified Local/Remote)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  EPMD Client/Server (Discovery)       β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  TCP Transport (Node-to-Node)         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

What Was Implemented

1. EPMD (Erlang Port Mapper Daemon)

Files:

  • src/epmd.rs - Protocol definitions
  • src/epmd/server.rs - EPMD server implementation
  • src/epmd/client.rs - EPMD client library

Features:

  • βœ… Binary protocol using bincode
  • βœ… Node registration/unregistration
  • βœ… Node lookup and discovery
  • βœ… Keep-alive mechanism (60s timeout)
  • βœ… Automatic dead node removal
  • βœ… Erlang-compatible port (4369)
  • βœ… Standalone server (examples/epmd_server.rs)

Protocol Messages:

  • Register - Register node with host/port
  • Unregister - Remove node from registry
  • Lookup - Find node by name
  • ListNodes - Get all registered nodes
  • Ping/Pong - Health check
  • KeepAlive - Maintain registration

2. Distributed System

File: src/distributed.rs

Features:

  • βœ… ActorSystem::new_distributed() - Unified distributed actor system
  • ⚠️ DistributedSystem - Deprecated wrapper (use ActorSystem::new_distributed() instead)
  • βœ… EPMD integration for discovery
  • βœ… Node ID assignment (hash of name)
  • βœ… TCP listener for incoming connections
  • βœ… Node registry with connection pooling
  • βœ… Automatic reconnection on failure
  • βœ… Graceful shutdown

Key Types:

pub struct DistributedSystem {
    system: Arc<ActorSystem>,      // Underlying actor system
    node_name: String,              // Unique node name
    node_id: u32,                   // Hashed node ID
    epmd_client: EpmdClient,        // Discovery client
    node_registry: Arc<NodeRegistry>, // Connection pool
    _listener_handle: ...,          // TCP listener
}

pub struct NetworkMessage {
    to: Pid,                        // Target actor
    from: Option<Pid>,              // Sender (optional)
    payload: Vec<u8>,               // Serialized message
}

API (Current - v0.5+):

// Create distributed system - unified API!
let system = ActorSystem::new_distributed(
    "my_node",
    "127.0.0.1:5000", 
    "127.0.0.1:4369"
).await?;

// Spawn actors - same API as local!
let actor = system.spawn(MyActor);

// Connect to remote node
system.connect_to_node("other_node").await?;

// List all nodes
let nodes = system.list_nodes().await?;

// Shutdown gracefully
system.shutdown().await?;

3. Location Transparency Foundation

File: src/pid.rs (enhanced)

Changes:

  • βœ… Added serde support for serialization
  • βœ… Made with_node() public for remote Pids
  • βœ… Node ID already supported (node: u32 field)
  • βœ… is_local() method to check if Pid is local
  • βœ… Display format shows node: <node.id.0>

Usage:

// Local Pid
let local = Pid::new();
assert!(local.is_local());
println!("{}", local); // <0.123.0>

// Remote Pid
let remote = Pid::with_node(42, 100);
assert!(!remote.is_local());
println!("{}", remote); // <42.100.0>

4. Examples

Files:

  • examples/epmd_server.rs - Standalone EPMD server
  • examples/distributed_cluster.rs - Multi-node cluster demo
  • examples/distributed_system_example.rs - Legacy DistributedSystem example (deprecated, see remote_ping_pong.rs)
  • examples/remote_ping_pong.rs - Current unified ActorSystem distributed example

Running:

# Terminal 1: EPMD
cargo run --example epmd_server

# Terminal 2: Node A
cargo run --example distributed_system_example -- node_a 5001

# Terminal 3: Node B
cargo run --example distributed_system_example -- node_b 5002

# Nodes automatically discover each other!

5. Documentation

Files:

  • CLUSTERING.md - Comprehensive clustering guide
  • QUICKSTART_CLUSTERING.md - 5-minute quick start
  • IMPLEMENTATION_SUMMARY.md - This file
  • Updated src/lib.rs with module docs

Test Coverage

Total Tests: 52 (all passing)

  • EPMD protocol: 3 tests
  • EPMD client: 2 tests
  • EPMD server: 4 tests
  • Distributed system: 2 tests
  • Pid serialization: included in existing tests
  • Original tests: 41 tests (all still passing)

Test Examples:

# Run all tests
cargo test

# Run EPMD tests
cargo test epmd

# Run distributed tests
cargo test distributed

Architecture Details

Node Discovery Flow

1. Node starts β†’ Create DistributedSystem
2. Extract host/port from listen address
3. Connect to EPMD server
4. Register node with EPMD
5. Start keep-alive loop (20s interval)
6. Start TCP listener for incoming connections
7. Periodically query EPMD for peers
8. Connect to discovered nodes

Connection Management

// NodeRegistry maintains connection pool
struct NodeRegistry {
    connections: Arc<DashMap<String, Arc<NodeConnection>>>,
}

// Each connection handles reconnection
struct NodeConnection {
    node_info: NodeInfo,
    stream: RwLock<Option<TcpStream>>,
}

// Automatic reconnection on send failure
if stream_guard.is_none() {
    // Reconnect to node
    *stream_guard = Some(TcpStream::connect(&addr).await?);
}

Message Format

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  4 bytes: message length (u32 BE) β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  N bytes: bincode(NetworkMessage) β”‚
β”‚    - to: Pid                       β”‚
β”‚    - from: Option<Pid>             β”‚
β”‚    - payload: Vec<u8>              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Current Limitations & Future Work

What Works Now

βœ… Node discovery via EPMD
βœ… Node registration/unregistration
βœ… Keep-alive mechanism
βœ… TCP connections between nodes
βœ… Connection pooling
βœ… Automatic reconnection
βœ… Graceful shutdown
βœ… Local actor messaging
βœ… Pid serialization

Not Yet Implemented

❌ Remote message sending - Foundation is in place, but full implementation needs:

  • Message serialization (requires Message to be serializable)
  • Node ID β†’ Node name mapping
  • Automatic message routing

❌ Remote actor spawn - Spawning actors on remote nodes

❌ Security - TLS, authentication, encryption

❌ Advanced features:

  • Global registry (name β†’ Pid across nodes)
  • Remote monitoring/linking
  • Node health monitoring
  • Cluster-wide supervision

Why Remote Messaging Isn't Fully Implemented

The challenge with remote messaging is that Message is Box<dyn Any + Send>, which cannot be serialized directly. Options:

  1. User serialization - Users serialize before sending

    let data = serde_json::to_vec(&my_data)?;
    remote_actor.send(Box::new(data)).await?;
  2. Trait-based - Require Message: Serialize

    pub trait RemoteMessage: Any + Send + Serialize + Deserialize {}
  3. Wrapper types - Special remote message types

    #[derive(Serialize, Deserialize)]
    enum RemoteCommand {
        Echo(String),
        Compute(i32, i32),
    }

The current implementation provides the transport infrastructure while leaving message serialization as a design choice for users.

Performance Characteristics

EPMD Server

  • Throughput: Handles concurrent connections
  • Latency: Sub-millisecond for lookup
  • Memory: O(n) where n = number of nodes
  • Scalability: Suitable for 100s of nodes

DistributedSystem

  • Node Discovery: 5-second polling interval (configurable)
  • Keep-Alive: 20-second interval β†’ 60-second timeout
  • Connection Pool: Reuses connections per node
  • Reconnection: Automatic on failure

Design Decisions

1. Why bincode for serialization?

  • Pros: Fast, efficient, Rust-native
  • Cons: Not human-readable, version-sensitive
  • Alternative: Could use MessagePack, JSON, or custom format

2. Why hash node names to IDs?

  • Consistent: Same name β†’ same ID
  • Compact: u32 fits in Pid
  • Fast: O(1) hashing
  • Alternative: Could use incrementing IDs with central authority

3. Why separate EPMD server?

  • Erlang compatible: Same architecture as Erlang/OTP
  • Flexibility: Can run independently, custom implementations
  • Reliability: Nodes can restart without EPMD restart
  • Alternative: Could use peer-to-peer discovery

4. Why not implement full remote messaging?

  • Design choice: Message serialization strategy varies by use case
  • Flexibility: Users can choose their serialization format
  • Infrastructure first: Transport layer is reusable
  • Future work: Can add opinionated remote messaging later

Migration Guide

From DistributedSystem (Deprecated) to Unified ActorSystem

Before (v0.4 and earlier - DEPRECATED):

let dist_system = DistributedSystem::new(
    "my_node",
    "127.0.0.1:5000",
    "127.0.0.1:4369"
).await?;

let actor = dist_system.system().spawn(MyActor);

After (v0.5+ - CURRENT):

let system = ActorSystem::new_distributed(
    "my_node",
    "127.0.0.1:5000",
    "127.0.0.1:4369"
).await?;

let actor = system.spawn(MyActor);

Benefits of Unified API:

  • No need to call .system() - direct access to all methods
  • Same API for local and distributed systems
  • Simpler mental model - one system type
  • Better Erlang/OTP alignment

Dependencies Added

bincode = "1.3"
serde = { version = "1.0", features = ["derive"] }

Both are widely-used, mature libraries with no additional transitive dependencies.

Conclusion

The distributed clustering implementation provides a solid foundation for building distributed actor systems in joerl. The architecture follows Erlang/OTP patterns while being fully Rust-native.

Key Achievements:

  • βœ… Complete EPMD implementation (server + client)
  • βœ… Node discovery and registration
  • βœ… TCP transport with connection pooling
  • βœ… Location transparency infrastructure (Pid with node support)
  • βœ… Comprehensive documentation
  • βœ… Working examples
  • βœ… All tests passing

Next Steps for Users:

  1. Start with EPMD and basic clustering
  2. Implement message serialization for your use case
  3. Build on the transport infrastructure
  4. Consider contributing advanced features back!

Status: βœ… Production-ready for node discovery and clustering foundation
Remote Messaging: 🚧 Infrastructure ready, user implementation required


For questions or contributions, see the main project README.