flow_machines 0.1.4-arm64-darwin
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +29 -0
- data/LICENSE.txt +21 -0
- data/README.md +230 -0
- data/ext/flow_machines_native/core/CHANGELOG.md +22 -0
- data/ext/flow_machines_native/core/README.md +59 -0
- data/ext/flow_machines_native/extconf.rb +3 -0
- data/lib/flow_machines/version.rb +5 -0
- data/lib/flow_machines.rb +7 -0
- data/lib/flow_machines_native/flow_machines_native.bundle +0 -0
- data/sig/flow_machines.rbs +27 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3896aabb78e47473a72032f06dc03b2395c3955fe55053bdb10e5109df87a2c2
|
|
4
|
+
data.tar.gz: f0e02286ecf2d4dbac4fd14edf376322b49d59bfd06c17f47c14034fe6fe2489
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 9a874c4a5596e523971fbdc16ddeadba68cf42059e5d4fe25ac307b0e0a8251ebec06138f2c9e2492d79bc54ba34892113ba30a6454ed31c7a4fd25685950fb2
|
|
7
|
+
data.tar.gz: 631fb48a83775ccd520716d7404f9a3e0f0340f7edb337a25b5be418073eefc31120463d942fcb517b19a017a7e952a63a80765681c8cf3af8c3108d1bfa98c5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.4](https://github.com/seuros/flow_machines/compare/flow_machines/v0.1.3...flow_machines/v0.1.4) (2026-11-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* crates.io keywords and categories for the core ([14830b5](https://github.com/seuros/flow_machines/commit/14830b52d44337c74302591f4c73ebcbe8bac3e4))
|
|
9
|
+
|
|
10
|
+
## [0.1.3](https://github.com/seuros/flow_machines/compare/flow_machines/v0.1.2...flow_machines/v0.1.3) (2026-11-06)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* keep test sources out of the packaged gem and crate ([3c9e544](https://github.com/seuros/flow_machines/commit/3c9e544f3b2140a179455b34236fd0f2ab9747a7))
|
|
16
|
+
|
|
17
|
+
## [0.1.2](https://github.com/seuros/flow_machines/compare/flow_machines/v0.1.1...flow_machines/v0.1.2) (2026-11-06)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* cross builds no longer compile for the host ([5a0009f](https://github.com/seuros/flow_machines/commit/5a0009fdef9267dc83ca7704b1e9f0f6373d3a5a))
|
|
23
|
+
|
|
24
|
+
## [0.1.1](https://github.com/seuros/flow_machines/compare/flow_machines-v0.1.0...flow_machines/v0.1.1) (2026-11-06)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Features
|
|
28
|
+
|
|
29
|
+
* initial extraction ([e087042](https://github.com/seuros/flow_machines/commit/e0870427dde8239fc30fc2fa10e6e204ebab9289))
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abdelkader Boudih
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# FlowMachines
|
|
2
|
+
|
|
3
|
+
> Your producer has a bigger engine than your consumer. Somebody has to say "hold." 🌊
|
|
4
|
+
|
|
5
|
+
Backpressure for Ruby, in two small parts. A `CreditWindow` counts how much work is allowed in flight and refuses anything past the line. A `Watermarks` controller watches a queue's occupancy and tells you when to pause the producer and when it's safe to resume, without flapping between the two every time the count twitches.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
gem 'flow_machines'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
window = FlowMachines::CreditWindow.new(capacity: 100)
|
|
15
|
+
control = FlowMachines::Watermarks.new(low: 20, high: 80)
|
|
16
|
+
|
|
17
|
+
# Producer side: ask before you enqueue.
|
|
18
|
+
def enqueue(job)
|
|
19
|
+
return false unless window.try_acquire(1)
|
|
20
|
+
queue << job
|
|
21
|
+
producer.pause if control.observe(queue.length) == :pause
|
|
22
|
+
true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Consumer side: give the credit back when the work is done.
|
|
26
|
+
def finish(job)
|
|
27
|
+
queue.delete(job)
|
|
28
|
+
window.release(1)
|
|
29
|
+
producer.resume if control.observe(queue.length) == :resume
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The window stops the queue from growing past 100. The watermarks tell the producer to stop at 80 and start again at 20. Everything in between is your queue and your producer. The gem only decides.
|
|
34
|
+
|
|
35
|
+
## A Message to the Resistance
|
|
36
|
+
|
|
37
|
+
So the ingest service got faster. Good news, until the worker pool behind it started falling over every evening at seven.
|
|
38
|
+
|
|
39
|
+
You know the shape of this one. The producer runs at whatever speed the producer runs at. The consumer runs slower. The gap goes into a queue, and the queue goes into memory, and memory is finite, and at seven the OOM killer arrives like a bailiff. Nobody wrote a bug. Everything did its job. The system just never had a word for "enough."
|
|
40
|
+
|
|
41
|
+
The first fix everyone reaches for is a hard cap: drop the job when the queue is full. That's load shedding, and sometimes it's right. But if the jobs matter, dropping them just moves the outage to wherever those jobs were supposed to land.
|
|
42
|
+
|
|
43
|
+
The second fix is to tell the producer to slow down. That's backpressure, and it needs two things: a count that actually limits what's in flight, and a signal that says "pause" at one level and "resume" at a lower one, so the producer isn't toggled on and off a thousand times a second as the count hovers around the line.
|
|
44
|
+
|
|
45
|
+
Those two things are this gem.
|
|
46
|
+
|
|
47
|
+
## Chapter 1: Stardate 2025.186, the AMS Cerf
|
|
48
|
+
|
|
49
|
+
> **AMS Cerf**, NCC-4209, *Cerf*-class traffic escort.
|
|
50
|
+
> Count the berths before inviting the next freighter through the gate.
|
|
51
|
+
|
|
52
|
+
The escort doesn't fly the freighters and doesn't own the dock. It keeps the count of berths, issues a credit for each one, and raises the hold signal when the approach lane fills up. Captains who ignore the signal still crash. Captains who obey it never do.
|
|
53
|
+
|
|
54
|
+
Somewhere in the approach lane, a freshly commissioned lieutenant files a flight plan:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
loop do
|
|
58
|
+
job = upstream.next
|
|
59
|
+
queue.push(job) # "Ruby arrays grow automatically, it's fine"
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
"That," says the dock master, "is how a lane becomes a debris field."
|
|
64
|
+
|
|
65
|
+
## The Flight Plan
|
|
66
|
+
|
|
67
|
+
The watermarks controller has two states and moves between them only at the edges.
|
|
68
|
+
|
|
69
|
+
```mermaid
|
|
70
|
+
stateDiagram-v2
|
|
71
|
+
[*] --> running
|
|
72
|
+
running --> running: observe(n) with n below high, unchanged
|
|
73
|
+
running --> paused: observe(n) with n at or above high, pause
|
|
74
|
+
paused --> paused: observe(n) with n above low, unchanged
|
|
75
|
+
paused --> running: observe(n) with n at or below low, resume
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
And an evening at the dock, with a window of 3 and watermarks at 1 and 3:
|
|
79
|
+
|
|
80
|
+
```mermaid
|
|
81
|
+
sequenceDiagram
|
|
82
|
+
participant P as Producer
|
|
83
|
+
participant W as CreditWindow(3)
|
|
84
|
+
participant M as Watermarks(1, 3)
|
|
85
|
+
participant C as Consumer
|
|
86
|
+
|
|
87
|
+
P->>W: try_acquire(1)
|
|
88
|
+
W-->>P: true (1 in flight)
|
|
89
|
+
P->>M: observe(1)
|
|
90
|
+
M-->>P: unchanged
|
|
91
|
+
|
|
92
|
+
P->>W: try_acquire(1)
|
|
93
|
+
W-->>P: true (2 in flight)
|
|
94
|
+
P->>M: observe(2)
|
|
95
|
+
M-->>P: unchanged
|
|
96
|
+
|
|
97
|
+
P->>W: try_acquire(1)
|
|
98
|
+
W-->>P: true (3 in flight)
|
|
99
|
+
P->>M: observe(3)
|
|
100
|
+
M-->>P: pause
|
|
101
|
+
Note over P: stops producing
|
|
102
|
+
|
|
103
|
+
P->>W: try_acquire(1)
|
|
104
|
+
W-->>P: false (window full)
|
|
105
|
+
|
|
106
|
+
C->>W: release(1)
|
|
107
|
+
W-->>C: true (2 in flight)
|
|
108
|
+
C->>M: observe(2)
|
|
109
|
+
M-->>C: unchanged
|
|
110
|
+
Note over P: still paused, no flapping
|
|
111
|
+
|
|
112
|
+
C->>W: release(1)
|
|
113
|
+
W-->>C: true (1 in flight)
|
|
114
|
+
C->>M: observe(1)
|
|
115
|
+
M-->>C: resume
|
|
116
|
+
Note over P: starts producing again
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## The Credit Window
|
|
120
|
+
|
|
121
|
+
A window has a capacity. Acquiring takes units out of it, releasing puts them back. Both are all-or-nothing.
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
window = FlowMachines::CreditWindow.new(capacity: 3)
|
|
125
|
+
window.capacity # => 3
|
|
126
|
+
window.in_flight # => 0
|
|
127
|
+
window.available # => 3
|
|
128
|
+
|
|
129
|
+
window.try_acquire(2) # => true
|
|
130
|
+
window.try_acquire(2) # => false, only 1 left and we asked for 2
|
|
131
|
+
window.in_flight # => 2
|
|
132
|
+
window.available # => 1
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A refused acquire takes nothing. There is no partial grant, so you never have to figure out how much you actually got.
|
|
136
|
+
|
|
137
|
+
Release is the same deal in reverse:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
window.release(3) # => false, only 2 are out
|
|
141
|
+
window.release(2) # => true
|
|
142
|
+
window.release(1) # => false, nothing is out
|
|
143
|
+
window.available # => 3
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The window is a count, not a ledger. It doesn't know who acquired what. If your code acquires on enqueue and forgets to release on a cancelled job, the window will slowly fill with ghosts. Release every unit you acquire exactly once, including on the error path.
|
|
147
|
+
|
|
148
|
+
## The Watermarks
|
|
149
|
+
|
|
150
|
+
A watermarks controller starts running. Feed it the current occupancy after every change and it answers with one of three words.
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
control = FlowMachines::Watermarks.new(low: 2, high: 5)
|
|
154
|
+
control.state # => :running
|
|
155
|
+
|
|
156
|
+
control.observe(4) # => :unchanged
|
|
157
|
+
control.observe(5) # => :pause
|
|
158
|
+
control.paused? # => true
|
|
159
|
+
control.observe(5) # => :unchanged, already paused
|
|
160
|
+
control.observe(3) # => :unchanged, above low, stay paused
|
|
161
|
+
control.observe(2) # => :resume
|
|
162
|
+
control.observe(0) # => :unchanged
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The gap between `low` and `high` is the whole point. A controller that paused at 5 and resumed at 4 would flap every time one job finished. This one pauses at 5 and won't resume until the queue drains to 2. That's hysteresis, and it's why your producer doesn't get whiplash.
|
|
166
|
+
|
|
167
|
+
`:pause` and `:resume` are edges. You get each one once per crossing. If a producer connects late and missed the edge, ask `state` or `paused?` instead of waiting for a signal that already went by.
|
|
168
|
+
|
|
169
|
+
Occupancy is whatever you want to measure. Queue length is the obvious choice. Bytes buffered, open connections, or outstanding requests all work just as well, as long as you measure the same thing every time.
|
|
170
|
+
|
|
171
|
+
## Putting Them Together
|
|
172
|
+
|
|
173
|
+
The window and the controller don't know about each other. You wire them around your queue. Here's a cancellation path, the one everyone forgets:
|
|
174
|
+
|
|
175
|
+
```ruby
|
|
176
|
+
window = FlowMachines::CreditWindow.new(capacity: 4)
|
|
177
|
+
control = FlowMachines::Watermarks.new(low: 1, high: 3)
|
|
178
|
+
queue = []
|
|
179
|
+
|
|
180
|
+
3.times do |id|
|
|
181
|
+
window.try_acquire(1)
|
|
182
|
+
queue << id
|
|
183
|
+
control.observe(queue.length) # :unchanged, :unchanged, :pause
|
|
184
|
+
end
|
|
185
|
+
control.paused? # => true
|
|
186
|
+
|
|
187
|
+
# A job is cancelled before it ever ran. Still a release.
|
|
188
|
+
queue.pop
|
|
189
|
+
window.release(1) # => true
|
|
190
|
+
control.observe(queue.length) # => :unchanged, 2 is above low
|
|
191
|
+
|
|
192
|
+
# One finishes normally.
|
|
193
|
+
queue.shift
|
|
194
|
+
window.release(1) # => true
|
|
195
|
+
control.observe(queue.length) # => :resume
|
|
196
|
+
window.in_flight # => 1, same as queue.length
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
If the cancelled job hadn't released its credit, `in_flight` would read 2 while the queue held 1, and the window would be one berth short forever.
|
|
200
|
+
|
|
201
|
+
## The Rules of the Escort
|
|
202
|
+
|
|
203
|
+
- **Counts are Integers** in `0..4_294_967_295`. Capacity, acquire, and release units must be at least 1. Floats and strings raise `TypeError`, zero raises `ArgumentError`, negatives and anything past the range raise `RangeError`.
|
|
204
|
+
- **Thresholds obey `0 <= low < high`.** Equal thresholds or a reversed pair raise `ArgumentError`. A `low` of zero is fine: resume only when fully drained.
|
|
205
|
+
- **Keywords only.** `CreditWindow.new(3)` and `Watermarks.new(1, 2)` raise `ArgumentError`. So does an unknown keyword.
|
|
206
|
+
- **A rejected observation changes nothing.** Pass the controller a float and it raises, but its state is exactly what it was before.
|
|
207
|
+
- **No copies.** `dup` and `clone` raise `TypeError`. Live accounting isn't something you want two of.
|
|
208
|
+
- **Frozen means frozen.** `freeze` either object and every mutation raises `FrozenError`.
|
|
209
|
+
|
|
210
|
+
## What You Think You're Doing vs Reality
|
|
211
|
+
|
|
212
|
+
### You Think: "I'll cap the queue and drop what doesn't fit."
|
|
213
|
+
### Reality: You've moved the outage downstream and called it resilience.
|
|
214
|
+
|
|
215
|
+
Dropping is a fine answer when the work is disposable. When it isn't, every dropped job is a retry somewhere else, a reconciliation report, or a customer email. Backpressure keeps the work and slows the source. Pick the one that matches what the jobs are worth.
|
|
216
|
+
|
|
217
|
+
### You Think: "Pause at 80, resume at 79, nice and responsive."
|
|
218
|
+
### Reality: Your producer now has a 79-to-80 oscillation you can hear from the next room.
|
|
219
|
+
|
|
220
|
+
One finished job resumes the producer. One enqueued job pauses it. Repeat a few thousand times a second. The gap between `low` and `high` is not slack. It's the thing that makes the signal mean something.
|
|
221
|
+
|
|
222
|
+
## What's Yours
|
|
223
|
+
|
|
224
|
+
The gem decides. It doesn't enqueue, dequeue, block, sleep, spawn threads, or talk to another process. Your queue is yours, your producer is yours, and so is making the producer actually stop when it hears `:pause`. If the window and the queue are touched from more than one thread, wrap the acquire-and-push and the pop-and-release in your own lock so the count and the queue never disagree.
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT License. See [LICENSE.txt](LICENSE.txt) for details.
|
|
229
|
+
|
|
230
|
+
**Remember: A queue that can grow forever is just an outage with a delay on it.**
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.2](https://github.com/seuros/flow_machines/compare/flow-machines-v0.2.1...flow-machines-v0.2.2) (2026-11-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* crates.io keywords and categories for the core ([14830b5](https://github.com/seuros/flow_machines/commit/14830b52d44337c74302591f4c73ebcbe8bac3e4))
|
|
9
|
+
|
|
10
|
+
## [0.2.1](https://github.com/seuros/flow_machines/compare/flow-machines-v0.2.0...flow-machines-v0.2.1) (2026-11-06)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* keep test sources out of the packaged gem and crate ([3c9e544](https://github.com/seuros/flow_machines/commit/3c9e544f3b2140a179455b34236fd0f2ab9747a7))
|
|
16
|
+
|
|
17
|
+
## [0.2.0](https://github.com/seuros/flow_machines/compare/flow-machines-v0.1.0...flow-machines-v0.2.0) (2026-11-06)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* initial extraction ([e087042](https://github.com/seuros/flow_machines/commit/e0870427dde8239fc30fc2fa10e6e204ebab9289))
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# flow-machines
|
|
2
|
+
|
|
3
|
+
> **AMS Cerf**, NCC-4209, *Cerf*-class traffic escort.
|
|
4
|
+
|
|
5
|
+
Engine-room flight manual: count docking credits before admitting more traffic.
|
|
6
|
+
|
|
7
|
+
Decision-only flow control with no runtime, I/O, allocator, or dependencies.
|
|
8
|
+
|
|
9
|
+
## Capacity Credits
|
|
10
|
+
|
|
11
|
+
```rust
|
|
12
|
+
use core::num::NonZeroU32;
|
|
13
|
+
use flow_machines::CreditWindow;
|
|
14
|
+
|
|
15
|
+
# fn example() -> Option<()> {
|
|
16
|
+
let mut window = CreditWindow::new(NonZeroU32::new(2)?);
|
|
17
|
+
assert!(window.try_acquire(NonZeroU32::MIN));
|
|
18
|
+
assert!(window.try_acquire(NonZeroU32::MIN));
|
|
19
|
+
assert!(!window.try_acquire(NonZeroU32::MIN));
|
|
20
|
+
assert!(window.release(NonZeroU32::MIN));
|
|
21
|
+
assert_eq!(window.available(), 1);
|
|
22
|
+
# Some(())
|
|
23
|
+
# }
|
|
24
|
+
# assert_eq!(example(), Some(()));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Acquisition and release are all-or-nothing. Failed operations leave state
|
|
28
|
+
unchanged; capacity never overflows. Units can be requests, messages, or bytes,
|
|
29
|
+
but must be consistent within a window.
|
|
30
|
+
|
|
31
|
+
This is **aggregate accounting**, not an owned-permit API. The adapter must return
|
|
32
|
+
each acquired credit exactly once, including on cancellation and failure.
|
|
33
|
+
A release larger than the outstanding balance is rejected, but repeated releases
|
|
34
|
+
cannot always be distinguished from legitimate completions. Credits are local
|
|
35
|
+
state, not authenticated distributed permissions.
|
|
36
|
+
|
|
37
|
+
## High/Low Watermarks
|
|
38
|
+
|
|
39
|
+
```rust
|
|
40
|
+
use core::num::NonZeroU32;
|
|
41
|
+
use flow_machines::{Signal, Watermarks};
|
|
42
|
+
|
|
43
|
+
# fn example() -> Option<()> {
|
|
44
|
+
let mut control = Watermarks::new(2, NonZeroU32::new(5)?)?;
|
|
45
|
+
assert_eq!(control.observe(5), Signal::Pause);
|
|
46
|
+
assert_eq!(control.observe(4), Signal::Unchanged);
|
|
47
|
+
assert_eq!(control.observe(2), Signal::Resume);
|
|
48
|
+
# Some(())
|
|
49
|
+
# }
|
|
50
|
+
# assert_eq!(example(), Some(()));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`low < high` is required. A running producer pauses at `occupancy >= high`;
|
|
54
|
+
a paused producer resumes at `occupancy <= low`. The gap prevents oscillation.
|
|
55
|
+
Signals are edges: repeated observations in the same state return `Unchanged`.
|
|
56
|
+
Newly attached producers should inspect `state()` as well as future signals.
|
|
57
|
+
|
|
58
|
+
Watermarks do not enforce capacity. Combine them with credits: producers should
|
|
59
|
+
honor the signals, while credit acquisition remains the hard admission boundary.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'flow_machines/version'
|
|
4
|
+
|
|
5
|
+
# One implementation: Ruby delegates every policy decision to the Rust core.
|
|
6
|
+
# A missing extension is an installation error, not a request for a fallback.
|
|
7
|
+
require 'flow_machines_native/flow_machines_native'
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Native policy decisions only. The caller owns queues and enforces the results.
|
|
2
|
+
module FlowMachines
|
|
3
|
+
VERSION: String
|
|
4
|
+
|
|
5
|
+
type signal = :pause | :resume | :unchanged
|
|
6
|
+
type state = :running | :paused
|
|
7
|
+
|
|
8
|
+
# Aggregate accounting, not a semaphore or a per-work-item permit ledger.
|
|
9
|
+
class CreditWindow
|
|
10
|
+
def self.new: (capacity: Integer) -> instance
|
|
11
|
+
def try_acquire: (Integer units) -> bool
|
|
12
|
+
def release: (Integer units) -> bool
|
|
13
|
+
def capacity: () -> Integer
|
|
14
|
+
def in_flight: () -> Integer
|
|
15
|
+
def available: () -> Integer
|
|
16
|
+
def inspect: () -> String
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Starts running; pauses at occupancy >= high and resumes at occupancy <= low.
|
|
20
|
+
class Watermarks
|
|
21
|
+
def self.new: (low: Integer, high: Integer) -> instance
|
|
22
|
+
def observe: (Integer occupancy) -> signal
|
|
23
|
+
def state: () -> state
|
|
24
|
+
def paused?: () -> bool
|
|
25
|
+
def inspect: () -> String
|
|
26
|
+
end
|
|
27
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: flow_machines
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.4
|
|
5
|
+
platform: arm64-darwin
|
|
6
|
+
authors:
|
|
7
|
+
- Abdelkader Boudih
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A native binding to the allocation-free flow-machines Rust core. Tracks
|
|
14
|
+
aggregate capacity and high/low-watermark producer signals; the caller owns queues,
|
|
15
|
+
credit ownership, and enforcement.
|
|
16
|
+
email:
|
|
17
|
+
- oss@seuros.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE.txt
|
|
24
|
+
- README.md
|
|
25
|
+
- ext/flow_machines_native/core/CHANGELOG.md
|
|
26
|
+
- ext/flow_machines_native/core/README.md
|
|
27
|
+
- ext/flow_machines_native/extconf.rb
|
|
28
|
+
- lib/flow_machines.rb
|
|
29
|
+
- lib/flow_machines/version.rb
|
|
30
|
+
- lib/flow_machines_native/flow_machines_native.bundle
|
|
31
|
+
- sig/flow_machines.rbs
|
|
32
|
+
homepage: https://github.com/seuros/flow_machines
|
|
33
|
+
licenses:
|
|
34
|
+
- MIT
|
|
35
|
+
metadata:
|
|
36
|
+
source_code_uri: https://github.com/seuros/flow_machines
|
|
37
|
+
bug_tracker_uri: https://github.com/seuros/flow_machines/issues
|
|
38
|
+
rubygems_mfa_required: 'true'
|
|
39
|
+
cargo_crate_name: flow_machines_native
|
|
40
|
+
cargo_manifest_path: ext/flow_machines_native/ffi/Cargo.toml
|
|
41
|
+
post_install_message:
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '4.0'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 4.1.dev
|
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '0'
|
|
58
|
+
requirements: []
|
|
59
|
+
rubygems_version: 3.5.23
|
|
60
|
+
signing_key:
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Capacity credits and backpressure decisions for Ruby
|
|
63
|
+
test_files: []
|