Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
fe77147
Ignore /notes directory in .gitignore
lupodevelop Mar 6, 2026
d8aacdc
Update .gitignore
lupodevelop Apr 29, 2026
ac2853f
Add atom budget and safety/validation utilities
lupodevelop Apr 29, 2026
5375944
Return tagged tuples and add ownership ACL
lupodevelop Apr 30, 2026
8fbaeda
Improve cluster FFI safety, telemetry & API
lupodevelop Apr 30, 2026
d050e48
Add config_ffi module for persistent config
lupodevelop Apr 30, 2026
37665fd
Add test FFI for cluster monitor events
lupodevelop May 1, 2026
97f6168
Add telemetry_ffi module for sink storage
lupodevelop May 1, 2026
a467e3b
Return typed atom budget errors and handle system_limit
lupodevelop May 1, 2026
ddfb967
Add test helpers for conflict resolver FFI
lupodevelop May 1, 2026
e437bd9
Add conflict FFI for :global name resolver
lupodevelop May 1, 2026
1d7dace
Add conflict_ffi_helpers module
lupodevelop May 2, 2026
840f954
Refactor distribute facade and add APIs
lupodevelop May 2, 2026
b4c36da
Add telemetry API and install function
lupodevelop May 3, 2026
84cd506
Improve error handling in start_node/2
lupodevelop May 10, 2026
76816fb
Bound await_worker_down teardown wait
lupodevelop May 10, 2026
0810785
Improve codecs: 32-bit lengths and strict checks
lupodevelop May 10, 2026
0a16a11
doc - fix
lupodevelop May 10, 2026
21178c5
Update distribute.gleam
lupodevelop May 10, 2026
fd3db5c
actor: graceful orphan shutdown & resource owner
lupodevelop May 10, 2026
6a9eec2
Update cluster.gleam
lupodevelop May 10, 2026
e16bb59
Add cluster event monitor actor
lupodevelop May 10, 2026
043dad9
Harden composite codec encoders/decoders
lupodevelop May 10, 2026
562e457
Tagged codec: add bounds checks and sized_decoder
lupodevelop May 10, 2026
1ebc0b5
Create config.gleam
lupodevelop May 10, 2026
e9deb06
Improve GlobalSubject messaging safety and telemetry
lupodevelop May 10, 2026
788b045
Add telemetry sink and Event types
lupodevelop May 10, 2026
8aa512d
Add conflict resolvers for distribute
lupodevelop May 10, 2026
f1260f0
Enhance receiver decoding, observability, and safety
lupodevelop May 12, 2026
af60566
registry: add validation, telemetry & async lookup
lupodevelop May 12, 2026
7564076
Update test.yml
lupodevelop May 12, 2026
ed36f78
Update actor_test.gleam
lupodevelop May 12, 2026
e776efc
Create call_robustness_test.gleam
lupodevelop May 12, 2026
8c3a0a6
Add test helper to read gleam.toml version
lupodevelop May 13, 2026
43d8bab
Add call_isolated and call_default tests
lupodevelop May 13, 2026
2f4adeb
Create config_test.gleam
lupodevelop May 13, 2026
27e07ab
Add cluster_monitor tests
lupodevelop May 13, 2026
8ef7183
Add cluster tests for atom-budget & errors
lupodevelop May 13, 2026
6e8d5a0
Add strict codec tests for bounds and caps
lupodevelop May 13, 2026
127c516
Add composite codec edge-case tests
lupodevelop May 13, 2026
64f1e52
Add conflict tests and update distribute tests
lupodevelop May 13, 2026
9044d66
Add extensive global subject edge-case tests
lupodevelop May 13, 2026
29f77cf
Add multinode peer smoke test
lupodevelop May 13, 2026
5ce16c4
Add multinode test peer helper (FFI)
lupodevelop May 13, 2026
ac70f0a
Add multinode_real_ffi test helper
lupodevelop May 13, 2026
3956dee
Add real multinode integration tests
lupodevelop May 13, 2026
61b02c0
Add simulated multinode decode tests
lupodevelop May 13, 2026
f1115b9
Add comprehensive receiver tests
lupodevelop May 13, 2026
24163f3
Expand registry tests and use test_helpers
lupodevelop May 13, 2026
d35d657
Create subject_layout_test.gleam
lupodevelop May 13, 2026
a10205d
Create subject_layout_test_ffi.erl
lupodevelop May 13, 2026
1575096
Add tagged version range and size tests
lupodevelop May 13, 2026
b45e84a
Add telemetry tests for distribute
lupodevelop May 13, 2026
45e805b
Add test FFI helpers for telemetry tests
lupodevelop May 13, 2026
c3b6831
Add test helper for generating unique IDs
lupodevelop May 13, 2026
6cfcd28
Add dev multi-node smoke test and peer FFI
lupodevelop May 13, 2026
f298b27
Add documentation for distribute v4
lupodevelop May 13, 2026
cccf012
Create logo.png
lupodevelop May 13, 2026
a083b34
Update LICENSE
lupodevelop May 13, 2026
6d295b5
Add ROADMAP.md with v4.x release plan
lupodevelop May 13, 2026
24e3616
Bump project and gleam_stdlib versions
lupodevelop May 13, 2026
dd22d1e
Update changelog to v4.0.0 and refresh README
lupodevelop May 13, 2026
4606afa
Merge main into import/4.0.0, keep branch on all conflicts
lupodevelop May 13, 2026
c886d75
Remove external :telemetry dependency artifacts from main merge
lupodevelop May 13, 2026
e2b8e38
Remove telemetry Erlang dependency added by main merge
lupodevelop May 13, 2026
2dd3a66
Fix CI: remove telemetry dep, skip Z2/Z3 when epmd unavailable
lupodevelop May 13, 2026
1a58bb1
Fix test suite: skip multinode tests without epmd, fix variant codec …
lupodevelop May 13, 2026
e135856
Silence OTP crash reports when epmd unavailable in multinode tests
lupodevelop May 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,13 @@ jobs:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: "28"
gleam-version: "1.14.0"
rebar3-version: "3"
# elixir-version: "1"
otp-version: "27.0"
gleam-version: "1.16.0"
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: gleam deps download
- run: epmd -daemon
- run: gleam test
- run: gleam format --check src test
- run: gleam docs build
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ erl_crash.dump
**/build
*.log
/notes

BACKENDS.md
.markdownlint-cli2.jsonc
.tmp-checks/docs.exit
.tmp-checks/format.exit
.tmp-checks/gleam-test.exit
.tmp-checks/markdownlint.exit
740 changes: 720 additions & 20 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2025 lupodevelop
Copyright (c) 2025 Scaratti Daniele - lupodevelop

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
195 changes: 73 additions & 122 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,133 +1,80 @@
<!-- markdownlint-disable MD033 -->

# distribute

<p align="center">
<img src="assets/img/distribute.png" alt="distribute logo" width="200" />
</p>
<img src="assets/img/logo.png" alt="logo" height="218" />

Typed distributed messaging for Gleam on the BEAM.

[![Package Version](https://img.shields.io/hexpm/v/distribute)](https://hex.pm/packages/distribute)
[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/distribute/)

## What this is

Erlang already gives you distribution, but from Gleam you lose type info at
the node boundary, everything crosses the wire as raw terms. `distribute`
puts binary codecs in front of `:global` and `Subject` so the compiler can
catch mismatches before messages leave the process.

"Typed" here means checked at encode/decode boundaries. There is no shared
type system across nodes. The BEAM doesn't work that way.
`distribute` is a thin typed safety layer over Erlang's distribution
primitives. It puts binary codecs in front of `:global` and `Subject` so
the compiler catches protocol mismatches before messages leave the
process. Payload size and dead-target detection are enforced at every
I/O boundary, so a misbehaving peer cannot OOM your node or hang it.

## Install

```sh
gleam add distribute
```

## Usage

### Fire-and-forget
Requires Gleam 1.16 or newer, Erlang targetÉ only.

Define a `TypedName(msg)` that pairs a name with a codec, then use it on
both sides. The compiler won't let you register a `String` actor and look
it up as `Int`.
## 30-second taste

```gleam
import distribute
import distribute/codec
import distribute/registry
import distribute/receiver

// one TypedName, shared across the codebase
let greeter = registry.named("greeter", codec.string())

// start + register
let assert Ok(gs) = distribute.start_actor(greeter, Nil, fn(msg, _state) {
io.println("Got: " <> msg)
receiver.Continue(Nil)
})
let assert Ok(Nil) = distribute.register(greeter, gs)

// from any node
let assert Ok(remote) = distribute.lookup(greeter)
let assert Ok(Nil) = distribute.send(remote, "hello")
```

### Request / response

Include a reply `Subject(BitArray)` in your message type and use
`codec.subject()` to serialize it. The `Subject` carries node info, so
replies route back across nodes automatically.

```gleam
import distribute/codec
import distribute/global
import distribute/receiver
import gleam/erlang/process

type CounterMsg {
Inc(Int)
Get(reply: process.Subject(BitArray))
}
let greeter = distribute.named("greeter", codec.string())

// handler side
fn handle(msg, state) {
case msg {
Inc(n) -> receiver.Continue(state + n)
Get(reply) -> {
let _ = global.reply(reply, state, codec.int_encoder())
receiver.Continue(state)
}
}
}

// caller side
let assert Ok(count) = global.call(counter, Get, codec.int_decoder(), 5000)
```

`global.call` creates a temporary subject, sends the request, waits for
the response, decodes it. Same idea as `gen_server:call`.

### Codecs

Primitives: `codec.int()`, `codec.string()`, `codec.float()`, `codec.bool()`,
`codec.bitarray()`, `codec.nil()`.

Composites: `codec.list(c)`, `codec.subject()`, `codec.map(c, wrap, unwrap)`,
`composite.option(c)`, `composite.result(ok, err)`,
`composite.tuple2(a, b)`, `composite.tuple3(a, b, c)`.

For your own types, use `codec.map`:

```gleam
type UserId { UserId(Int) }
let assert Ok(_gs) =
distribute.start_registered(greeter, Nil, fn(msg, _state) {
io.println("got: " <> msg)
receiver.Continue(Nil)
})

let user_id_codec = codec.map(codec.int(), UserId, fn(uid) {
let UserId(n) = uid
n
})
let assert Ok(target) = distribute.lookup(greeter)
let assert Ok(Nil) = distribute.send(target, "hello")
```

Gleam has no derive macros or reflection, so codecs for complex types
are manual. The combinators handle the serialization so you just wire
the fields together!

## Modules

| Module | Does |
|--------|------|
| `distribute` | Facade: start node, connect, send, lookup |
| `distribute/actor` | Named actors, supervision, pools |
| `distribute/cluster` | `net_kernel` start/connect/ping |
| `distribute/codec` | Binary codecs for primitives + `subject()` |
| `distribute/codec/composite` | Option, Result, Tuple codecs |
| `distribute/codec/variant` | Build codecs for Custom Types (ADTs) |
| `distribute/codec/tagged` | Tagged messages with version field |
| `distribute/global` | `GlobalSubject(msg)`, `call`, `reply` |
| `distribute/cluster/monitor` | `NodeUp`, `NodeDown` typed events |
| `distribute/registry` | `TypedName(msg)`, `:global` registration |
| `distribute/receiver` | Typed receive, OTP actor wrappers |
For the full walkthrough see [docs/quickstart.md](./docs/quickstart.md).

## Documentation

All long-form docs live in [`docs/`](./docs/README.md):

- [Quickstart](./docs/quickstart.md). Boot, configure, send.
- [Recipes](./docs/recipes.md). Counter, pool, versioned protocol,
cluster events, custom records.
- [Actors and registry](./docs/actors_and_registry.md).
- [Messaging](./docs/messaging.md). `send` / `receive` / `call`.
- [Codecs and types](./docs/codecs_and_types.md). Wire format, custom
records, tagged messages.
- [Safety and limits](./docs/safety_and_limits.md). Payload limits,
threat model, recommended sizing.

## What you get

- **Typed boundary.** `TypedName(msg)` binds a name to a codec.
Registration and lookup share the same `msg` type, and the compiler
rejects mismatches.
- **Hard payload caps.** `max_payload_size_bytes` is enforced before
encode and before decode on every path: `send`, `receive`, `call`,
`reply`, actor handlers, selectors.
- **Fast-fail calls.** `global.call` monitors the target. A dead target
returns `Error(TargetDown)` immediately. Late replies and DOWN
messages are drained from the caller's mailbox.
- **OTP-native.** Real OTP gen_server-flavored actors via
`gleam_otp/actor`, real supervisors, real child specs. `observer`,
`sys:get_status`, restart strategies all work. No magic.
- **Single-source codec.** Encoding and decoding happen through one
`Codec(a)` value. Combinators (`map`, `list`, `option`, `tuple`,
`tagged`) cover the common cases without macros.

### Custom Type Codecs

Expand Down Expand Up @@ -175,26 +122,30 @@ distribute.unsubscribe(m)

## Caveats

**What the types catch** — within one codebase, `TypedName` and
`GlobalSubject` prevent mixing up message types at compile time.

**What they don't** — two separate codebases using different codecs for
the same name. The codec will reject the binary at runtime, not at compile
time. Same for Erlang code sending raw terms to a `distribute` actor.

**Subject construction** — Gleam's `Subject` is opaque. To build one from
a remote PID and a deterministic tag (how registry lookup works), we
construct the `{subject, Pid, Tag}` tuple in
[one Erlang function](src/distribute_ffi_utils.erl). If `gleam_erlang`
changes the internal representation, that single function needs updating.

**No auto-derive** — Gleam doesn't have macros. Complex message codecs
are manual. The combinators (`map`, `list`, `option`, `tuple2`, etc.)
keep it manageable, but it's not zero-boilerplate.
- **Two codebases, one wire.** `TypedName` enforces type safety inside
one codebase. Mismatched codecs across separate codebases produce
runtime decode errors, not compile errors.
- **No auto-derive.** Gleam has no macros, so complex codecs are
manual. The combinators keep them short.
- **`terminate` caveat in `gleam_otp/actor` 1.x.** External shutdown
paths do not currently invoke an OTP-style `terminate` callback. If
an actor owns files, sockets, ETS tables, ports, or other external
resources, use the linked resource-owner pattern documented in
[docs/recipes.md](./docs/recipes.md) and
[docs/safety_and_limits.md](./docs/safety_and_limits.md).
- **One internal coupling.** We construct `Subject` from a remote PID
via [one Erlang FFI function](src/distribute_ffi_utils.erl). That is
the single point that depends on `gleam_erlang`'s subject layout.

## Development

```sh
gleam test
gleam docs build
gleam test # full suite (includes mandatory real-cluster Z2/Z3)
epmd -daemon # start Erlang distribution daemon if not already running
gleam dev # multi-node playground
gleam docs build # local API docs
```

## License

MIT. See [LICENSE](./LICENSE).
67 changes: 67 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!-- markdownlint-disable MD024 MD049 MD060 -->

# Roadmap

Indicative release sequencing for the v4.x line. This is a
**tentative** outline, not a commitment: priorities and dates may
shift as user feedback arrives and audit cycles flag new work.
What stays fixed is the SemVer contract: patch releases carry
bug fixes only, minor releases are strictly additive, with no
breaking changes inside v4.

## v4.0.0 shipped

First production-grade release. Hardened wire format, single-
write configuration, atom-budget guardrail, fast-fail call path,
mailbox-safe `call_isolated`, conflict-resolver hook on the
registry, structured telemetry on every load-bearing boundary.
See [`CHANGELOG.md`](./CHANGELOG.md) for the full breakdown.

## v4.0.1 patch (target: within ~2 weeks of v4.0.0)

Bug fix only, no new features.

- **`registry.lookup_async` zombie under net-split.** When the
peer that holds a registered name leaves the cluster mid-poll,
the polling worker today spins until its deadline expires.
Under fan-in (many concurrent lookups toward a vanishing
peer) this wastes scheduler time at scale. The fix is to
subscribe `cluster_monitor` from the poller and abort early on
`NodeDown` of the relevant peer. Multi-node Z-suite test
asserts that 100+ pending lookups toward a deliberately-killed
peer all return inside a small grace window rather than
running to deadline.

## v4.1.0 minor (target: weeks-to-months)

Strictly additive. No breaking changes for users staying on
`:global`.

- **Backend abstraction (Phase 1).** Introduce an opaque
registry-backend type with one constructor today
(`global_backend()`) so that future minor releases can add
more constructors without changing user code. Internal
refactor only; behaviour identical to v4.0 for any caller
that does not opt in to a non-default backend.
- **Ergonomic retouch.** Candidate items: a `LookupOptions`
record to replace the positional `(timeout_ms,
poll_interval_ms)` pair on the polling lookups, plus
whatever small adjustments accumulate from real-world use.

## v4.2.0 minor (Target: Absolute Priority)

Opt-in second backend: `syn`. Attualmente `:global` strozza i cluster superiori a 50 nodi, rendendo `distribute` limitato. Il passaggio a `syn` è il vero step enterprise.

- **`syn`-backed registry as a sibling package.** Likely shipped
as `distribute_syn` so users staying on `:global` do not pull
the `syn` Erlang application into their dependency closure.
Multi-node integration tests parity with the `:global`
backend; migration notes covering cold-flip semantics; no
live `:global` → `syn` migration tooling in this release.

## Beyond v4.2

Open. Theorical. Possible directions: Raft-backed registry (`khepri` /
`ra`), performance work on the codec hot path. Each of these
warrants its own design pass before it lands on a release. None
of them are blockers for the current API.
Binary file added assets/img/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading