Skip to content

Commit dcfb822

Browse files
authored
Merge pull request #15 from lupodevelop/import/4.0.0
Import/4.0.0
2 parents 4aa2f04 + e135856 commit dcfb822

72 files changed

Lines changed: 12988 additions & 1218 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@ jobs:
1414
- uses: actions/checkout@v4
1515
- uses: erlef/setup-beam@v1
1616
with:
17-
otp-version: "28"
18-
gleam-version: "1.14.0"
19-
rebar3-version: "3"
20-
# elixir-version: "1"
17+
otp-version: "27.0"
18+
gleam-version: "1.16.0"
19+
- uses: actions/setup-node@v4
20+
with:
21+
node-version: "20"
2122
- run: gleam deps download
23+
- run: epmd -daemon
2224
- run: gleam test
2325
- run: gleam format --check src test
26+
- run: gleam docs build

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,9 @@ erl_crash.dump
55
**/build
66
*.log
77
/notes
8-
8+
BACKENDS.md
9+
.markdownlint-cli2.jsonc
10+
.tmp-checks/docs.exit
11+
.tmp-checks/format.exit
12+
.tmp-checks/gleam-test.exit
13+
.tmp-checks/markdownlint.exit

CHANGELOG.md

Lines changed: 720 additions & 20 deletions
Large diffs are not rendered by default.

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2025 lupodevelop
3+
Copyright (c) 2025 Scaratti Daniele - lupodevelop
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 73 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,80 @@
1+
<!-- markdownlint-disable MD033 -->
2+
13
# distribute
24

3-
<p align="center">
4-
<img src="assets/img/distribute.png" alt="distribute logo" width="200" />
5-
</p>
5+
<img src="assets/img/logo.png" alt="logo" height="218" />
66

77
Typed distributed messaging for Gleam on the BEAM.
88

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

12-
## What this is
13-
14-
Erlang already gives you distribution, but from Gleam you lose type info at
15-
the node boundary, everything crosses the wire as raw terms. `distribute`
16-
puts binary codecs in front of `:global` and `Subject` so the compiler can
17-
catch mismatches before messages leave the process.
18-
19-
"Typed" here means checked at encode/decode boundaries. There is no shared
20-
type system across nodes. The BEAM doesn't work that way.
12+
`distribute` is a thin typed safety layer over Erlang's distribution
13+
primitives. It puts binary codecs in front of `:global` and `Subject` so
14+
the compiler catches protocol mismatches before messages leave the
15+
process. Payload size and dead-target detection are enforced at every
16+
I/O boundary, so a misbehaving peer cannot OOM your node or hang it.
2117

2218
## Install
2319

2420
```sh
2521
gleam add distribute
2622
```
2723

28-
## Usage
29-
30-
### Fire-and-forget
24+
Requires Gleam 1.16 or newer, Erlang targetÉ only.
3125

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

3628
```gleam
3729
import distribute
3830
import distribute/codec
39-
import distribute/registry
40-
import distribute/receiver
41-
42-
// one TypedName, shared across the codebase
43-
let greeter = registry.named("greeter", codec.string())
44-
45-
// start + register
46-
let assert Ok(gs) = distribute.start_actor(greeter, Nil, fn(msg, _state) {
47-
io.println("Got: " <> msg)
48-
receiver.Continue(Nil)
49-
})
50-
let assert Ok(Nil) = distribute.register(greeter, gs)
51-
52-
// from any node
53-
let assert Ok(remote) = distribute.lookup(greeter)
54-
let assert Ok(Nil) = distribute.send(remote, "hello")
55-
```
56-
57-
### Request / response
58-
59-
Include a reply `Subject(BitArray)` in your message type and use
60-
`codec.subject()` to serialize it. The `Subject` carries node info, so
61-
replies route back across nodes automatically.
62-
63-
```gleam
64-
import distribute/codec
65-
import distribute/global
6631
import distribute/receiver
67-
import gleam/erlang/process
6832
69-
type CounterMsg {
70-
Inc(Int)
71-
Get(reply: process.Subject(BitArray))
72-
}
33+
let greeter = distribute.named("greeter", codec.string())
7334
74-
// handler side
75-
fn handle(msg, state) {
76-
case msg {
77-
Inc(n) -> receiver.Continue(state + n)
78-
Get(reply) -> {
79-
let _ = global.reply(reply, state, codec.int_encoder())
80-
receiver.Continue(state)
81-
}
82-
}
83-
}
84-
85-
// caller side
86-
let assert Ok(count) = global.call(counter, Get, codec.int_decoder(), 5000)
87-
```
88-
89-
`global.call` creates a temporary subject, sends the request, waits for
90-
the response, decodes it. Same idea as `gen_server:call`.
91-
92-
### Codecs
93-
94-
Primitives: `codec.int()`, `codec.string()`, `codec.float()`, `codec.bool()`,
95-
`codec.bitarray()`, `codec.nil()`.
96-
97-
Composites: `codec.list(c)`, `codec.subject()`, `codec.map(c, wrap, unwrap)`,
98-
`composite.option(c)`, `composite.result(ok, err)`,
99-
`composite.tuple2(a, b)`, `composite.tuple3(a, b, c)`.
100-
101-
For your own types, use `codec.map`:
102-
103-
```gleam
104-
type UserId { UserId(Int) }
35+
let assert Ok(_gs) =
36+
distribute.start_registered(greeter, Nil, fn(msg, _state) {
37+
io.println("got: " <> msg)
38+
receiver.Continue(Nil)
39+
})
10540
106-
let user_id_codec = codec.map(codec.int(), UserId, fn(uid) {
107-
let UserId(n) = uid
108-
n
109-
})
41+
let assert Ok(target) = distribute.lookup(greeter)
42+
let assert Ok(Nil) = distribute.send(target, "hello")
11043
```
11144

112-
Gleam has no derive macros or reflection, so codecs for complex types
113-
are manual. The combinators handle the serialization so you just wire
114-
the fields together!
115-
116-
## Modules
117-
118-
| Module | Does |
119-
|--------|------|
120-
| `distribute` | Facade: start node, connect, send, lookup |
121-
| `distribute/actor` | Named actors, supervision, pools |
122-
| `distribute/cluster` | `net_kernel` start/connect/ping |
123-
| `distribute/codec` | Binary codecs for primitives + `subject()` |
124-
| `distribute/codec/composite` | Option, Result, Tuple codecs |
125-
| `distribute/codec/variant` | Build codecs for Custom Types (ADTs) |
126-
| `distribute/codec/tagged` | Tagged messages with version field |
127-
| `distribute/global` | `GlobalSubject(msg)`, `call`, `reply` |
128-
| `distribute/cluster/monitor` | `NodeUp`, `NodeDown` typed events |
129-
| `distribute/registry` | `TypedName(msg)`, `:global` registration |
130-
| `distribute/receiver` | Typed receive, OTP actor wrappers |
45+
For the full walkthrough see [docs/quickstart.md](./docs/quickstart.md).
46+
47+
## Documentation
48+
49+
All long-form docs live in [`docs/`](./docs/README.md):
50+
51+
- [Quickstart](./docs/quickstart.md). Boot, configure, send.
52+
- [Recipes](./docs/recipes.md). Counter, pool, versioned protocol,
53+
cluster events, custom records.
54+
- [Actors and registry](./docs/actors_and_registry.md).
55+
- [Messaging](./docs/messaging.md). `send` / `receive` / `call`.
56+
- [Codecs and types](./docs/codecs_and_types.md). Wire format, custom
57+
records, tagged messages.
58+
- [Safety and limits](./docs/safety_and_limits.md). Payload limits,
59+
threat model, recommended sizing.
60+
61+
## What you get
62+
63+
- **Typed boundary.** `TypedName(msg)` binds a name to a codec.
64+
Registration and lookup share the same `msg` type, and the compiler
65+
rejects mismatches.
66+
- **Hard payload caps.** `max_payload_size_bytes` is enforced before
67+
encode and before decode on every path: `send`, `receive`, `call`,
68+
`reply`, actor handlers, selectors.
69+
- **Fast-fail calls.** `global.call` monitors the target. A dead target
70+
returns `Error(TargetDown)` immediately. Late replies and DOWN
71+
messages are drained from the caller's mailbox.
72+
- **OTP-native.** Real OTP gen_server-flavored actors via
73+
`gleam_otp/actor`, real supervisors, real child specs. `observer`,
74+
`sys:get_status`, restart strategies all work. No magic.
75+
- **Single-source codec.** Encoding and decoding happen through one
76+
`Codec(a)` value. Combinators (`map`, `list`, `option`, `tuple`,
77+
`tagged`) cover the common cases without macros.
13178

13279
### Custom Type Codecs
13380

@@ -175,26 +122,30 @@ distribute.unsubscribe(m)
175122

176123
## Caveats
177124

178-
**What the types catch** — within one codebase, `TypedName` and
179-
`GlobalSubject` prevent mixing up message types at compile time.
180-
181-
**What they don't** — two separate codebases using different codecs for
182-
the same name. The codec will reject the binary at runtime, not at compile
183-
time. Same for Erlang code sending raw terms to a `distribute` actor.
184-
185-
**Subject construction** — Gleam's `Subject` is opaque. To build one from
186-
a remote PID and a deterministic tag (how registry lookup works), we
187-
construct the `{subject, Pid, Tag}` tuple in
188-
[one Erlang function](src/distribute_ffi_utils.erl). If `gleam_erlang`
189-
changes the internal representation, that single function needs updating.
190-
191-
**No auto-derive** — Gleam doesn't have macros. Complex message codecs
192-
are manual. The combinators (`map`, `list`, `option`, `tuple2`, etc.)
193-
keep it manageable, but it's not zero-boilerplate.
125+
- **Two codebases, one wire.** `TypedName` enforces type safety inside
126+
one codebase. Mismatched codecs across separate codebases produce
127+
runtime decode errors, not compile errors.
128+
- **No auto-derive.** Gleam has no macros, so complex codecs are
129+
manual. The combinators keep them short.
130+
- **`terminate` caveat in `gleam_otp/actor` 1.x.** External shutdown
131+
paths do not currently invoke an OTP-style `terminate` callback. If
132+
an actor owns files, sockets, ETS tables, ports, or other external
133+
resources, use the linked resource-owner pattern documented in
134+
[docs/recipes.md](./docs/recipes.md) and
135+
[docs/safety_and_limits.md](./docs/safety_and_limits.md).
136+
- **One internal coupling.** We construct `Subject` from a remote PID
137+
via [one Erlang FFI function](src/distribute_ffi_utils.erl). That is
138+
the single point that depends on `gleam_erlang`'s subject layout.
194139

195140
## Development
196141

197142
```sh
198-
gleam test
199-
gleam docs build
143+
gleam test # full suite (includes mandatory real-cluster Z2/Z3)
144+
epmd -daemon # start Erlang distribution daemon if not already running
145+
gleam dev # multi-node playground
146+
gleam docs build # local API docs
200147
```
148+
149+
## License
150+
151+
MIT. See [LICENSE](./LICENSE).

ROADMAP.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<!-- markdownlint-disable MD024 MD049 MD060 -->
2+
3+
# Roadmap
4+
5+
Indicative release sequencing for the v4.x line. This is a
6+
**tentative** outline, not a commitment: priorities and dates may
7+
shift as user feedback arrives and audit cycles flag new work.
8+
What stays fixed is the SemVer contract: patch releases carry
9+
bug fixes only, minor releases are strictly additive, with no
10+
breaking changes inside v4.
11+
12+
## v4.0.0 shipped
13+
14+
First production-grade release. Hardened wire format, single-
15+
write configuration, atom-budget guardrail, fast-fail call path,
16+
mailbox-safe `call_isolated`, conflict-resolver hook on the
17+
registry, structured telemetry on every load-bearing boundary.
18+
See [`CHANGELOG.md`](./CHANGELOG.md) for the full breakdown.
19+
20+
## v4.0.1 patch (target: within ~2 weeks of v4.0.0)
21+
22+
Bug fix only, no new features.
23+
24+
- **`registry.lookup_async` zombie under net-split.** When the
25+
peer that holds a registered name leaves the cluster mid-poll,
26+
the polling worker today spins until its deadline expires.
27+
Under fan-in (many concurrent lookups toward a vanishing
28+
peer) this wastes scheduler time at scale. The fix is to
29+
subscribe `cluster_monitor` from the poller and abort early on
30+
`NodeDown` of the relevant peer. Multi-node Z-suite test
31+
asserts that 100+ pending lookups toward a deliberately-killed
32+
peer all return inside a small grace window rather than
33+
running to deadline.
34+
35+
## v4.1.0 minor (target: weeks-to-months)
36+
37+
Strictly additive. No breaking changes for users staying on
38+
`:global`.
39+
40+
- **Backend abstraction (Phase 1).** Introduce an opaque
41+
registry-backend type with one constructor today
42+
(`global_backend()`) so that future minor releases can add
43+
more constructors without changing user code. Internal
44+
refactor only; behaviour identical to v4.0 for any caller
45+
that does not opt in to a non-default backend.
46+
- **Ergonomic retouch.** Candidate items: a `LookupOptions`
47+
record to replace the positional `(timeout_ms,
48+
poll_interval_ms)` pair on the polling lookups, plus
49+
whatever small adjustments accumulate from real-world use.
50+
51+
## v4.2.0 minor (Target: Absolute Priority)
52+
53+
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.
54+
55+
- **`syn`-backed registry as a sibling package.** Likely shipped
56+
as `distribute_syn` so users staying on `:global` do not pull
57+
the `syn` Erlang application into their dependency closure.
58+
Multi-node integration tests parity with the `:global`
59+
backend; migration notes covering cold-flip semantics; no
60+
live `:global``syn` migration tooling in this release.
61+
62+
## Beyond v4.2
63+
64+
Open. Theorical. Possible directions: Raft-backed registry (`khepri` /
65+
`ra`), performance work on the codec hot path. Each of these
66+
warrants its own design pass before it lands on a release. None
67+
of them are blockers for the current API.

assets/img/logo.png

206 KB
Loading

0 commit comments

Comments
 (0)