Skip to content

Commit 356c890

Browse files
committed
docs: add comprehensive Getting Started guides in English and Russian
Add two new documentation files to help users get started with joerl: - GETTING_STARTED.md: English version with clear, professional technical writing - GETTING_STARTED_RU.md: Russian version with sharp, engaging style Both guides cover: - Simple counter actor example demonstrating core concepts - Telemetry and observability with Prometheus integration - Transparent distribution with location-transparent messaging - Erlang/OTP migration guide with side-by-side code comparisons - Property-based testing approach as proof of correctness The guides emphasize joerl's key strengths: - Familiar Erlang/OTP concepts and terminology - Production-ready monitoring out of the box - Well-tested with extensive property-based tests - Zero-cost abstractions built on tokio
1 parent ebeb221 commit 356c890

3 files changed

Lines changed: 680 additions & 285 deletions

File tree

GETTING_STARTED.md

Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
# Getting Started with joerl
2+
3+
**joerl** is an Erlang-inspired actor model library for Rust, designed to make building concurrent, fault-tolerant systems as natural in Rust as it is in Erlang/OTP.
4+
5+
## Why joerl?
6+
7+
- **Familiar for Erlang developers**: Same concepts, same terminology, seamless mental model transfer
8+
- **Production-ready**: Built-in telemetry, health monitoring, and distributed messaging
9+
- **Well-tested**: Extensive property-based testing ensures correctness
10+
- **Zero-cost abstractions**: Built on tokio for maximum performance
11+
12+
## A Simple Example: Counter Actor
13+
14+
Let's build a simple counter actor to demonstrate the basics:
15+
16+
```rust
17+
use joerl::{Actor, ActorContext, ActorSystem, Message};
18+
use async_trait::async_trait;
19+
20+
// Define your actor
21+
struct Counter {
22+
count: i32,
23+
}
24+
25+
#[async_trait]
26+
impl Actor for Counter {
27+
async fn started(&mut self, ctx: &mut ActorContext) {
28+
println!("Counter started with pid {}", ctx.pid());
29+
}
30+
31+
async fn handle_message(&mut self, msg: Message, ctx: &mut ActorContext) {
32+
if let Some(cmd) = msg.downcast_ref::<&str>() {
33+
match *cmd {
34+
"increment" => {
35+
self.count += 1;
36+
println!("[{}] Count: {}", ctx.pid(), self.count);
37+
},
38+
"get" => {
39+
println!("[{}] Current count: {}", ctx.pid(), self.count);
40+
},
41+
"stop" => {
42+
ctx.stop(joerl::ExitReason::Normal);
43+
},
44+
_ => {}
45+
}
46+
}
47+
}
48+
49+
async fn stopped(&mut self, reason: &joerl::ExitReason, ctx: &mut ActorContext) {
50+
println!("[{}] Counter stopped: {}", ctx.pid(), reason);
51+
}
52+
}
53+
54+
#[tokio::main]
55+
async fn main() {
56+
let system = ActorSystem::new();
57+
let counter = system.spawn(Counter { count: 0 });
58+
59+
counter.send(Box::new("increment")).await.unwrap();
60+
counter.send(Box::new("increment")).await.unwrap();
61+
counter.send(Box::new("get")).await.unwrap();
62+
counter.send(Box::new("stop")).await.unwrap();
63+
64+
// Wait for messages to process
65+
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
66+
}
67+
```
68+
69+
**Key concepts:**
70+
- **Actor**: Encapsulates state (`count`) and behavior (`handle_message`)
71+
- **ActorSystem**: Runtime that manages all actors
72+
- **Message passing**: Type-erased messages allow any type to be sent
73+
- **Lifecycle hooks**: `started()` and `stopped()` for initialization/cleanup
74+
75+
## Telemetry and Observability
76+
77+
One of joerl's strengths is built-in production monitoring. Enable the `telemetry` feature:
78+
79+
```toml
80+
[dependencies]
81+
joerl = { version = "0.5", features = ["telemetry"] }
82+
metrics-exporter-prometheus = "0.15"
83+
```
84+
85+
Add telemetry to your application:
86+
87+
```rust
88+
use joerl::telemetry;
89+
use metrics_exporter_prometheus::PrometheusBuilder;
90+
91+
#[tokio::main]
92+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
93+
// Start Prometheus exporter
94+
PrometheusBuilder::new()
95+
.with_http_listener(([127, 0, 0, 1], 9090))
96+
.install()?;
97+
98+
telemetry::init();
99+
100+
let system = ActorSystem::new();
101+
// ... your actors
102+
103+
Ok(())
104+
}
105+
```
106+
107+
Now visit `http://localhost:9090/metrics` to see:
108+
- **Actor lifecycle**: spawns, stops, panics
109+
- **Message throughput**: messages sent/processed per second
110+
- **Mailbox depth**: backpressure indicators
111+
- **Supervisor restarts**: fault recovery statistics
112+
113+
**Why it matters:**
114+
- Zero-configuration monitoring out of the box
115+
- Production debugging without guesswork
116+
- Grafana/Prometheus integration ready
117+
- OpenTelemetry support for distributed tracing
118+
119+
## Transparent Distribution
120+
121+
joerl provides location-transparent messaging: the same API works for local and remote actors.
122+
123+
**Start two nodes:**
124+
125+
```bash
126+
# Terminal 1: Start EPMD server
127+
cargo run --example epmd_server
128+
129+
# Terminal 2: Start node A
130+
cargo run --example distributed_cluster -- node_a 5001
131+
132+
# Terminal 3: Start node B
133+
cargo run --example distributed_cluster -- node_b 5002
134+
```
135+
136+
Nodes automatically discover each other via EPMD (Erlang Port Mapper Daemon).
137+
138+
**Code example:**
139+
140+
```rust
141+
use joerl::ActorSystem;
142+
143+
#[tokio::main]
144+
async fn main() {
145+
// Create distributed system
146+
let system = ActorSystem::new_distributed(
147+
"mynode@localhost",
148+
5000,
149+
"127.0.0.1:4369" // EPMD address
150+
).await.unwrap();
151+
152+
// Spawn actor - works exactly like local
153+
let actor = system.spawn(MyActor::new());
154+
155+
// Send message - works for local AND remote actors
156+
actor.send(Box::new("hello")).await.unwrap();
157+
158+
// Connect to another node
159+
system.connect_to_node("othernode@localhost").await.unwrap();
160+
161+
// Get remote actor pid and send messages transparently
162+
// ... same API, zero code changes!
163+
}
164+
```
165+
166+
**Key features:**
167+
- **Same API**: `spawn()`, `send()`, `link()` work identically
168+
- **Automatic discovery**: EPMD handles node registration
169+
- **Bidirectional links**: Full Erlang-style connection semantics
170+
- **Serialization**: Trait-based message serialization with global registry
171+
172+
This is **exactly** how Erlang works: write once, deploy anywhere.
173+
174+
## For Erlang/OTP Developers
175+
176+
If you know Erlang, you already know joerl. Here's the mapping:
177+
178+
| Erlang | joerl | Notes |
179+
|--------|-------|-------|
180+
| `spawn/1` | `system.spawn(actor)` | Spawn new process |
181+
| `Pid ! Msg` | `actor.send(msg).await` | Send message |
182+
| `gen_server:call/2` | `server.call(request).await` | Synchronous RPC |
183+
| `gen_server:cast/2` | `server.cast(msg).await` | Async message |
184+
| `link/1` | `system.link(pid1, pid2)` | Bidirectional link |
185+
| `monitor/2` | `actor.monitor(from)` | Unidirectional monitor |
186+
| `process_flag(trap_exit, true)` | `ctx.trap_exit(true)` | Handle failures |
187+
| `{'EXIT', Pid, Reason}` | `Signal::Exit { from, reason }` | Exit signal |
188+
| `supervisor:start_link/2` | `spawn_supervisor(&system, spec)` | Supervision tree |
189+
190+
**Example: Converting Erlang gen_server to joerl:**
191+
192+
Erlang:
193+
```erlang
194+
-module(counter).
195+
-behaviour(gen_server).
196+
197+
init([]) -> {ok, 0}.
198+
199+
handle_call(get, _From, State) ->
200+
{reply, State, State};
201+
handle_call({add, N}, _From, State) ->
202+
{reply, State + N, State + N}.
203+
204+
handle_cast(increment, State) ->
205+
{noreply, State + 1}.
206+
```
207+
208+
joerl:
209+
```rust
210+
use joerl::gen_server::{GenServer, GenServerContext};
211+
212+
struct Counter;
213+
214+
#[async_trait]
215+
impl GenServer for Counter {
216+
type State = i32;
217+
type Call = CounterCall;
218+
type Cast = CounterCast;
219+
type CallReply = i32;
220+
221+
async fn init(&mut self, _ctx: &mut GenServerContext<'_, Self>) -> Self::State {
222+
0
223+
}
224+
225+
async fn handle_call(
226+
&mut self,
227+
call: Self::Call,
228+
state: &mut Self::State,
229+
_ctx: &mut GenServerContext<'_, Self>,
230+
) -> Self::CallReply {
231+
match call {
232+
CounterCall::Get => *state,
233+
CounterCall::Add(n) => {
234+
*state += n;
235+
*state
236+
}
237+
}
238+
}
239+
240+
async fn handle_cast(
241+
&mut self,
242+
cast: Self::Cast,
243+
state: &mut Self::State,
244+
_ctx: &mut GenServerContext<'_, Self>,
245+
) {
246+
match cast {
247+
CounterCast::Increment => *state += 1,
248+
}
249+
}
250+
}
251+
```
252+
253+
**Migration is straightforward:**
254+
1. Map your gen_server callbacks to trait methods
255+
2. Use `async`/`await` where Erlang would block
256+
3. Keep your mental model: actors, supervision, links, monitors
257+
4. Gain Rust's type safety and performance
258+
259+
## Property-Based Testing: Proof of Correctness
260+
261+
joerl uses extensive property-based testing with QuickCheck to verify correctness. Instead of hand-written test cases, properties are defined that should hold for **all** inputs, then hundreds of random test cases are generated.
262+
263+
**What's tested:**
264+
265+
1. **Pid properties**: Serialization roundtrips, node semantics, equality
266+
2. **Message serialization**: Lossless encoding, determinism, edge cases
267+
3. **EPMD protocol**: All protocol messages, NodeInfo properties
268+
4. **Actor lifecycle**: Spawn, message handling, termination
269+
5. **Supervision**: Restart strategies, failure propagation
270+
271+
**Example property test:**
272+
273+
```rust
274+
use quickcheck_macros::quickcheck;
275+
276+
/// Property: Pid serialization must be lossless
277+
#[quickcheck]
278+
fn prop_pid_serialization_roundtrip(pid: Pid) -> bool {
279+
let serialized = serde_json::to_string(&pid).unwrap();
280+
let deserialized: Pid = serde_json::from_str(&serialized).unwrap();
281+
pid == deserialized
282+
}
283+
```
284+
285+
QuickCheck generates random `Pid` values and verifies the property holds for all of them.
286+
287+
**Running property tests:**
288+
289+
```bash
290+
# Run all property tests
291+
cargo test --tests proptest
292+
293+
# Run 1000 random cases per property
294+
QUICKCHECK_TESTS=1000 cargo test --test proptest_pid
295+
296+
# Run specific test
297+
cargo test --test proptest_serialization prop_message_roundtrip
298+
```
299+
300+
**Why this matters:**
301+
- **Confidence**: Tests cover cases you'd never think of manually
302+
- **Edge cases**: Finds corner cases (empty strings, max values, etc.)
303+
- **Regression prevention**: Failed test cases can be saved and replayed
304+
- **Living documentation**: Properties describe what the code guarantees
305+
306+
For full details, see [PROPERTY_TESTING.md](PROPERTY_TESTING.md).
307+
308+
## Next Steps
309+
310+
1. **Learn by example**: Check the [`examples/`](joerl/examples/) directory
311+
- `counter.rs` - Basic actor
312+
- `gen_server_counter.rs` - GenServer pattern
313+
- `supervision_tree.rs` - Fault tolerance
314+
- `telemetry_demo.rs` - Monitoring
315+
- `distributed_cluster.rs` - Multi-node systems
316+
317+
2. **Read the docs**: Comprehensive API documentation at [docs.rs/joerl](https://docs.rs/joerl)
318+
319+
3. **Understand supervision**: Erlang's "let it crash" philosophy is core to joerl
320+
321+
4. **Explore distribution**: See [DISTRIBUTED.md](DISTRIBUTED.md) for clustering details
322+
323+
5. **Monitor in production**: See [TELEMETRY.md](TELEMETRY.md) for observability setup
324+
325+
## Summary
326+
327+
joerl brings Erlang/OTP's proven concurrent programming model to Rust:
328+
329+
- **Simple**: Start with basic actors, add complexity as needed
330+
- **Observable**: Built-in telemetry for production debugging
331+
- **Distributed**: Write once, deploy across multiple nodes
332+
- **Familiar**: Direct mapping from Erlang concepts
333+
- **Verified**: Property-based testing ensures correctness
334+
335+
Whether you're coming from Erlang or new to the actor model, joerl provides a robust foundation for building fault-tolerant, distributed systems in Rust.

0 commit comments

Comments
 (0)