joerl — это библиотека модели акторов для Rust, вдохновленная Erlang и названная в честь Джо Армстронга, создателя Erlang. Если вам когда-либо приходилось строить конкурентные системы на Erlang/OTP и вы думали: «Эх, был бы здесь хоть намек на систему типов», — то вот она, ваша прелесть. Я начинал этот проект просто потренироваться в расте немного, но меня затянуло и я довел код более-менее до ума. Сам я на расте писать буду вряд ли, если кто-то ближе к телу захочет попробовать — буду признателен.
Публикую сейчас, потому что свободное время у меня заканчивается, много допилов в ближайшее время ждать не стоит, основную функциональность, которую хотел, я сделал, а карму мне скоро выкрутят в минус и придётся публиковаться через песочницу.
Еще одна нафиг библиотека?
Для эрлангистов: Та же терминология, те же концепции. Плавный переход, надеюсь, очень плавный; можно почти без кода запустить кластер. EPMD в комплекте, прозрачная адресация акторов, короче, distributed как мы привыкли;
Production-ready: Встроенная телеметрия, мониторинг здоровья системы, распределенный обмен сообщениями — все, что нужно для боевого применения;
Тщательно протестирована: Обширное property-based тестирование гарантирует корректность. Не верьте на слово — проверьте сами (но продакшена она пока не видала);
Производительность: Нормальная производительность, вроде. Раст же.
Простой пример: Актор-счетчик
Начнем с простейшего примера — актора-счетчика. Ничего не делает, но дает представление о внешнем виде кода:
use joerl::{Actor, ActorContext, ActorSystem, Message};
use async_trait::async_trait;
// Определяем актор
struct Counter {
count: i32,
}
#[async_trait]
impl Actor for Counter {
async fn started(&mut self, ctx: &mut ActorContext) {
println!("Счетчик запущен с pid {}", ctx.pid());
}
async fn handle_message(&mut self, msg: Message, ctx: &mut ActorContext) {
if let Some(cmd) = msg.downcast_ref::<&str>() {
match *cmd {
"increment" => {
self.count += 1;
println!("[{}] Счет: {}", ctx.pid(), self.count);
},
"get" => {
println!("[{}] Текущий счет: {}", ctx.pid(), self.count);
},
"stop" => {
ctx.stop(joerl::ExitReason::Normal);
},
_ => {}
}
}
}
async fn stopped(&mut self, reason: &joerl::ExitReason, ctx: &mut ActorContext) {
println!("[{}] Счетчик остановлен: {}", ctx.pid(), reason);
}
}
#[tokio::main]
async fn main() {
let system = ActorSystem::new();
let counter = system.spawn(Counter { count: 0 });
counter.send(Box::new("increment")).await.unwrap();
counter.send(Box::new("increment")).await.unwrap();
counter.send(Box::new("get")).await.unwrap();
counter.send(Box::new("stop")).await.unwrap();
// Дадим сообщениям время обработаться
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
}
Сообщения принципиально сделаны type-erased — и позволяют отправлять что угодно; я не считаю типы — полезной херней.
Телеметрия и наблюдаемость
Одна из сильных сторон joerl — встроенный мониторинг для production. Не нужно гадать, что пошло не так в три часа ночи. Включаем фичу telemetry:
[dependencies]
joerl = { version = "0.5", features = ["telemetry"] }
metrics-exporter-prometheus = "0.15"
Добавляем телеметрию в приложение:
use joerl::telemetry;
use metrics_exporter_prometheus::PrometheusBuilder;
#[tokio::main]
async fn main() -> Result<(), Box> {
// Запускаем Prometheus exporter
PrometheusBuilder::new()
.with_http_listener(([127, 0, 0, 1], 9090))
.install()?;
telemetry::init();
let system = ActorSystem::new();
// ... ваши акторы
Ok(())
}
Теперь открываем http://localhost:9090/metrics и видим:
▸ Жизненный цикл акторов: создание, остановка, паники
▸ Пропускная способность: сообщений отправлено/обработано в секунду
▸ Глубина mailbox: индикаторы backpressure
▸ Перезапуски супервизоров: статистика восстановления после сбоев
▸ Готовая интеграция с Grafana/Prometheus
▸ Поддержка OpenTelemetry для распределенного трейсинга
Прозрачный distribution
joerl обеспечивает location-transparent messaging: тот же API для локальных и удаленных акторов. Никакой разницы, работаете вы на одной машине или в кластере из десятков нод. Эх, почти, как в эрланге!
Запускаем два узла:
# Терминал 1: Запускаем EPMD сервер
cargo run --example epmd_server
# Терминал 2: Запускаем узел A
cargo run --example distributed_cluster -- node_a 5001
# Терминал 3: Запускаем узел B
cargo run --example distributed_cluster -- node_b 5002
Узлы автоматически обнаруживают друг друга через EPMD (Erlang Port Mapper Daemon). Как в Erlang, только лучше — потому что на Rust (шутка, конечно, вообще не лучше).
Пример кода:
use joerl::ActorSystem;
#[tokio::main]
async fn main() {
// Создаем распределенную систему
let system = ActorSystem::new_distributed(
"mynode@localhost",
5000,
"127.0.0.1:4369" // адрес EPMD
).await.unwrap();
// Создаем актор — работает точно так же, как локально
let actor = system.spawn(MyActor::new());
// Отправляем сообщение — работает для локальных И удаленных акторов
actor.send(Box::new("hello")).await.unwrap();
// Подключаемся к другому узлу
system.connect_to_node("othernode@localhost").await.unwrap();
// Получаем pid удаленного актора и прозрачно отправляем сообщения
// ... тот же API, никаких изменений в коде!
}
▸ Единый API: spawn(), send(), link() работают идентично
▸ Автоматическое обнаружение: EPMD обрабатывает регистрацию узлов
▸ Двунаправленные связи: Полная семантика соединений в стиле Erlang
▸ Сериализация: Trait-based сериализация сообщений с глобальным реестром
Ну и мониторинг всякий, линкинг и прочий дуинг работают тоже прозрачно.
Для разработчиков на Erlang/OTP
Если вы знаете Erlang, вы уже знаете joerl. Смотрите:
Erlang |
joerl |
Описание |
|---|---|---|
|
|
Создать новый процесс |
|
|
Отправить сообщение |
|
|
Синхронный RPC |
|
|
Асинхронное сообщение |
|
|
Двунаправленная связь |
|
|
Однонаправленный монитор |
|
|
Обработка сбоев |
|
|
Сигнал выхода |
|
|
Дерево супервизоров |
Пример: Конвертация эрланговского gen_server в joerl:
Erlang:
-module(counter).
-behaviour(gen_server).
init([]) -> {ok, 0}.
handle_call(get, _From, State) ->
{reply, State, State};
handle_call({add, N}, _From, State) ->
{reply, State + N, State + N}.
handle_cast(increment, State) ->
{noreply, State + 1}.
joerl:
use joerl::gen_server::{GenServer, GenServerContext};
struct Counter;
#[async_trait]
impl GenServer for Counter {
type State = i32;
type Call = CounterCall;
type Cast = CounterCast;
type CallReply = i32;
async fn init(&mut self, _ctx: &mut GenServerContext<'_, Self>) -> Self::State {
0
}
async fn handle_call(
&mut self,
call: Self::Call,
state: &mut Self::State,
_ctx: &mut GenServerContext<'_, Self>,
) -> Self::CallReply {
match call {
CounterCall::Get => *state,
CounterCall::Add(n) => {
*state += n;
*state
}
}
}
async fn handle_cast(
&mut self,
cast: Self::Cast,
state: &mut Self::State,
_ctx: &mut GenServerContext<'_, Self>,
) {
match cast {
CounterCast::Increment =>; *state += 1,
}
}
}
Код почти не изменился: акторы, супервизоры, связи, мониторы, появилась type safety и производительность Rust (не уверен, что это плюс, впрочем).
Property-Based тестирование: доказательство корректности
joerl использует обширное property-based тестирование с QuickCheck для проверки корректности. Вместо написания отдельных тестовых случаев определяются свойства, которые должны выполняться для всех входных данных, затем генерируются сотни случайных тестов.
Пример property-теста:
use quickcheck_macros::quickcheck;
/// Свойство: Сериализация Pid должна быть без потерь
#[quickcheck]
fn prop_pid_serialization_roundtrip(pid: Pid) -> bool {
let serialized = serde_json::to_string(&pid).unwrap();
let deserialized: Pid = serde_json::from_str(&serialized).unwrap();
pid == deserialized
}
QuickCheck генерирует случайные значения Pid и проверяет, что свойство выполняется для всех них.
Запуск property-тестов:
# Запустить все property-тесты
cargo test --tests proptest
# Запустить 1000 случайных случаев на свойство
QUICKCHECK_TESTS=1000 cargo test --test proptest_pid
# Запустить конкретный тест
cargo test --test proptest_serialization prop_message_roundtrip
Property-based тестирование — в 2025 году для меня — не прихоть, а доказательство того, что код работает не только на примерах из README, но и в реальной жизни со всеми ее сюрпризами.
Примеры валяются в директории examples, сравнение с actix — тоже есть.
В общем, оно как-то работает, как-то проверено и как-то задокументировано. Что еще нужно?
Dhwtj
Удивительно, правда?
Вы вещаете из глубокого технологического колодца и ваше бубнение непонятно здешней публике. А адаптироваться вы не хотите.