flow_machines 0.1.4-x86_64-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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d03519b4f84f989fb2a3619debbddff9a0c1e090203952433028748a6a5b4abc
4
+ data.tar.gz: '08a6e7b9686aaa4de3377d746a4fa9a007e97699f408a21bc36e769ddada0f9c'
5
+ SHA512:
6
+ metadata.gz: c7bf3cfe5d2fda1c0080e8b1ee229821a91cde688c7dbb251f941446648d31d487102aaf21aec7e53dc337486ad58ae7b5f3b07841c592dfaa6d843408173ad1
7
+ data.tar.gz: 74e28bf464ffc363484a012af5d299e918b790d5c2bac6b63db3fbd1c920576e67218ee336ef8514b674d673ff408bd84a77285f86e04f2393631d37badf2388
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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FlowMachines
4
+ VERSION = '0.1.4'
5
+ end
@@ -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'
@@ -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: x86_64-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: []