Skip to content

Commit c0c517a

Browse files
committed
Make it safe & Gleam native
1 parent 7f21f54 commit c0c517a

File tree

4 files changed

+77
-76
lines changed

4 files changed

+77
-76
lines changed

README.md

Lines changed: 14 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,13 @@
22

33
Gleam bindings to [Manifold](https://github.com/discord/manifold) - an Elixir library for fast message passing between BEAM nodes.
44

5-
> [!WARNING]
6-
> This library depends on internal implementation details of Gleam's `erlang/process` module. It is not officially supported and may break with future Gleam releases. Use at your own risk.
7-
85
## What is Manifold?
96

107
Manifold is an Elixir library developed by Discord that optimizes sending the same message to many processes. Instead of sending messages sequentially (which can be slow with thousands of processes), Manifold uses a divide-and-conquer approach that distributes the work across multiple sender processes, achieving much better performance at scale.
118

129
## Installation
1310

14-
This library is intentionally not published to hex, because it depends on internal implementation details of Gleam's `erlang/process` module. If Gleam opens up the Subject type, we can implement this library in a more official way and publish to hex.
15-
16-
For now, please depend on it as a git dependency in your `gleam.toml`:
11+
Add to your `gleam.toml` as a git dependency:
1712

1813
```toml
1914
[dependencies]
@@ -22,31 +17,23 @@ gleam_manifold = { git = "[email protected]:otters/gleam_manifold.git", ref = "<com
2217

2318
## Usage
2419

25-
### Type-safe sending to Subjects
20+
### Creating and using Subjects
2621

27-
The primary way to use this library is with Gleam's type-safe `Subject` system:
22+
This library provides its own Subject type for type-safe message passing:
2823

2924
```gleam
3025
import gleam/erlang/process
3126
import gleam_manifold as manifold
3227
3328
pub fn example() {
34-
let subject = process.new_subject()
29+
let subject = manifold.new_subject()
30+
let pid = process.self()
3531
3632
// Send a message through Manifold
37-
manifold.send(subject, "Hello world")
38-
}
39-
```
40-
41-
### Sending to PIDs
42-
43-
For cases where you need to send to raw PIDs (not type-safe):
44-
45-
```gleam
46-
import gleam_manifold as manifold
33+
manifold.send(pid, subject, "Hello world")
4734
48-
pub fn send_to_pid(pid: process.Pid) {
49-
manifold.send_pid(pid, "Hello")
35+
// Receive the message
36+
let assert Ok(message) = manifold.receive(subject, 1000)
5037
}
5138
```
5239

@@ -55,24 +42,18 @@ pub fn send_to_pid(pid: process.Pid) {
5542
Send the same message to multiple processes at once:
5643

5744
```gleam
45+
import gleam/erlang/process
5846
import gleam_manifold as manifold
5947
60-
pub fn broadcast_join(pids: List(process.Pid), name: String) {
61-
manifold.send_multi_pid(pids, "Hello, " <> name)
48+
pub fn broadcast(pids: List(process.Pid), message: String) {
49+
let subject = manifold.new_subject()
50+
manifold.send_multi(pids, subject, message)
6251
}
6352
```
6453

65-
## Implementation Notes
66-
67-
### Internals Hack
68-
69-
This library uses internals of Gleam's `erlang/process` implementation by unwrapping the opaque `Subject` type in Erlang. This is technically a hack since `Subject` is meant to be opaque, but it's necessary to extract the PID and reference tag needed for sending to Gleam's Subject type. The implementation pattern matches on the internal `{subject, Pid, Ref}` tuple structure in `gleam_manifold_ffi.erl`.
70-
71-
### Current Limitations
72-
73-
1. **No options parameter support**: The Elixir Manifold library supports an options parameter for tuning performance characteristics (like partition size). This Gleam wrapper doesn't currently expose these options, though this could be added in the future.
54+
## Current Limitations
7455

75-
2. **No named subjects support**: Named subjects (registered process names) are not currently supported. The library will fail with an assertion error if you try to use a named subject. This is fixable but hasn't been implemented yet.
56+
**No options parameter support**: The Elixir Manifold library supports an options parameter for tuning performance characteristics (like partition size). This Gleam wrapper doesn't currently expose these options, though this could be added in the future.
7657

7758
## Testing
7859

src/gleam_manifold.gleam

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
import gleam/erlang/process
22
import gleam/erlang/reference
33

4-
pub fn send(subject: process.Subject(message), message: message) -> Nil {
5-
let assert Ok(#(pid, ref)) = unwrap_subject(subject)
6-
as "Named subjects are currently unsupported in gleam_manifold.send()"
7-
manifold_send_subject(pid, #(ref, message))
8-
Nil
4+
pub opaque type Subject(message) {
5+
Subject(tag: reference.Reference)
6+
}
7+
8+
type Message(message) =
9+
#(reference.Reference, message)
10+
11+
pub fn new_subject() -> Subject(message) {
12+
Subject(reference.new())
913
}
1014

1115
/// You almost certainly do not want to use this.
1216
/// Absolutely prefer to use the type-safe variant of
1317
/// this function `manifold.send(subject, message)`
14-
pub fn send_pid(pid: process.Pid, message: message) -> Nil {
15-
manifold_send_raw(pid, message)
18+
pub fn send(
19+
pid: process.Pid,
20+
subject: Subject(message),
21+
message: message,
22+
) -> Nil {
23+
manifold_send(pid, #(subject.tag, message))
1624
Nil
1725
}
1826

@@ -26,26 +34,28 @@ pub fn send_pid(pid: process.Pid, message: message) -> Nil {
2634
/// impossible because they are unique. You would have to use something
2735
/// like `process.select_other` to take advantage of receiving values
2836
/// with this function still within Gleam.
29-
pub fn send_multi_pid(pids: List(process.Pid), message: message) -> Nil {
30-
manifold_send_multi(pids, message)
37+
pub fn send_multi(
38+
pids: List(process.Pid),
39+
subject: Subject(message),
40+
message: message,
41+
) -> Nil {
42+
manifold_send_multi(pids, #(subject.tag, message))
3143
Nil
3244
}
3345

3446
type DoNotLeak
3547

36-
@external(erlang, "gleam_manifold_ffi", "unwrap_subject")
37-
fn unwrap_subject(
38-
subject: process.Subject(message),
39-
) -> Result(#(process.Pid, reference.Reference), Nil)
48+
@external(erlang, "gleam_manifold_ffi", "receive")
49+
pub fn receive(subject: Subject(message), timeout: Int) -> Result(message, Nil)
4050

41-
@external(erlang, "Elixir.Manifold", "send")
42-
fn manifold_send_subject(
43-
pid: process.Pid,
44-
message: #(reference.Reference, message),
45-
) -> DoNotLeak
51+
@external(erlang, "gleam_manifold_ffi", "receive")
52+
pub fn receive_forever(subject: Subject(message)) -> message
4653

4754
@external(erlang, "Elixir.Manifold", "send")
48-
fn manifold_send_raw(pid: process.Pid, message: message) -> DoNotLeak
55+
fn manifold_send(pid: process.Pid, message: Message(message)) -> DoNotLeak
4956

5057
@external(erlang, "Elixir.Manifold", "send")
51-
fn manifold_send_multi(pid: List(process.Pid), message: message) -> DoNotLeak
58+
fn manifold_send_multi(
59+
pid: List(process.Pid),
60+
message: Message(message),
61+
) -> DoNotLeak

src/gleam_manifold_ffi.erl

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
-module(gleam_manifold_ffi).
2-
-export([unwrap_subject/1]).
2+
-export(['receive'/1, 'receive'/2]).
33

4-
unwrap_subject({subject, Pid, Ref}) ->
5-
{ok, {Pid, Ref}};
6-
unwrap_subject(_Subject) ->
7-
{error, nil}.
4+
'receive'({subject, Ref}) ->
5+
receive
6+
{Ref, Message} -> Message
7+
end.
8+
9+
'receive'({subject, Ref}, Timeout) ->
10+
receive
11+
{Ref, Message} -> {ok, Message}
12+
after Timeout ->
13+
{error, nil}
14+
end.

test/gleam_manifold_test.gleam

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,48 +7,51 @@ pub fn main() -> Nil {
77
}
88

99
pub fn simple_test() {
10-
let subject = process.new_subject()
11-
process.spawn(fn() { manifold.send(subject, "Hello world") })
12-
assert process.receive(subject, 100) == Ok("Hello world")
10+
let subject = manifold.new_subject()
11+
let pid = process.self()
12+
process.spawn(fn() { manifold.send(pid, subject, "Hello world") })
13+
assert manifold.receive(subject, 100) == Ok("Hello world")
1314
}
1415

1516
pub fn many_pids_test() {
16-
let subject = process.new_subject()
17+
let subject = manifold.new_subject()
18+
let pid = process.self()
1719

1820
let proc = fn() {
1921
let selector = process.new_selector() |> process.select_other(identity)
20-
let assert Ok(result) = process.selector_receive(selector, 5)
21-
process.send(subject, result)
22+
let assert Ok(#(_ref, result)) = process.selector_receive(selector, 5)
23+
manifold.send(pid, subject, result)
2224
}
2325

2426
let a = process.spawn(proc)
2527
let b = process.spawn(proc)
2628

27-
manifold.send_pid(a, "hello from a")
28-
manifold.send_pid(b, "hello from b")
29+
manifold.send(a, subject, "hello from a")
30+
manifold.send(b, subject, "hello from b")
2931

30-
assert process.receive(subject, 5) == Ok("hello from a")
31-
assert process.receive(subject, 5) == Ok("hello from b")
32-
assert process.receive(subject, 5) == Error(Nil)
32+
assert manifold.receive(subject, 5) == Ok("hello from a")
33+
assert manifold.receive(subject, 5) == Ok("hello from b")
34+
assert manifold.receive(subject, 5) == Error(Nil)
3335
}
3436

3537
pub fn multi_pids_test() {
36-
let subject = process.new_subject()
38+
let subject = manifold.new_subject()
39+
let pid = process.self()
3740

3841
let proc = fn() {
3942
let selector = process.new_selector() |> process.select_other(identity)
40-
let assert Ok(result) = process.selector_receive(selector, 5)
41-
process.send(subject, result)
43+
let assert Ok(#(_ref, result)) = process.selector_receive(selector, 5)
44+
manifold.send(pid, subject, result)
4245
}
4346

4447
let a = process.spawn(proc)
4548
let b = process.spawn(proc)
4649

47-
manifold.send_multi_pid([a, b], "hello from both")
50+
manifold.send_multi([a, b], subject, "hello from both")
4851

49-
assert process.receive(subject, 5) == Ok("hello from both")
50-
assert process.receive(subject, 5) == Ok("hello from both")
51-
assert process.receive(subject, 5) == Error(Nil)
52+
assert manifold.receive(subject, 5) == Ok("hello from both")
53+
assert manifold.receive(subject, 5) == Ok("hello from both")
54+
assert manifold.receive(subject, 5) == Error(Nil)
5255
}
5356

5457
@external(erlang, "gleam_erlang_ffi", "identity")

0 commit comments

Comments
 (0)