batch_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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b025f47eeb70434a02fd8e60cb54e40fd85d896ee7aa9b0d339a85431a065ee9
4
+ data.tar.gz: ab4add0214d8f2e8f2ad2395ea3c23d698e864f1b3c938fc77f2502185bd7e72
5
+ SHA512:
6
+ metadata.gz: d7ad31e137d1522835281ae35105b9fa7b2e1669e1e1f3fe432b5b7d73f017ee341f051796ea620f6d4f1a61e5317ea5a0dbe030110d3342042fe9b1ab83bbcb
7
+ data.tar.gz: 2c80d8c18535ae0ea8e40b9b797e5fd905733164df4f180fab201057e58f7fb25d4033713d67e80225c48059f8855f5be2326ff2e522dcfce62d94786d072c20
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.1.4](https://github.com/seuros/batch_machines/compare/batch_machines/v0.1.3...batch_machines/v0.1.4) (2026-11-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * expose the pending deadline and weighted pushes ([9044fa3](https://github.com/seuros/batch_machines/commit/9044fa323859bcd6fb6cf9a7b5c884bf503fedc4))
9
+
10
+ ## [0.1.3](https://github.com/seuros/batch_machines/compare/batch_machines/v0.1.2...batch_machines/v0.1.3) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * crates.io keywords and categories for the core ([b150571](https://github.com/seuros/batch_machines/commit/b150571563871f7af013b716402aa169688deaf2))
16
+
17
+ ## [0.1.2](https://github.com/seuros/batch_machines/compare/batch_machines/v0.1.1...batch_machines/v0.1.2) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * keep test sources out of the packaged gem and crate ([2287c10](https://github.com/seuros/batch_machines/commit/2287c1089fefd9154740078a6977598f47033d0e))
23
+
24
+ ## [0.1.1](https://github.com/seuros/batch_machines/compare/batch_machines-v0.1.0...batch_machines/v0.1.1) (2026-11-06)
25
+
26
+
27
+ ### Features
28
+
29
+ * initial extraction ([da62e4b](https://github.com/seuros/batch_machines/commit/da62e4bb16f3d1eaecc93ebf2a739f86fcfee932))
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,219 @@
1
+ # BatchMachines
2
+
3
+ > Fewer launches, fuller holds, and nobody left sitting on the pad. 🚚
4
+
5
+ A batching policy for Ruby. You tell it how big a batch may get and how long the oldest item may wait. Every time something arrives you `push` the clock, and it tells you whether to keep buffering or send now. On your timer you `poll`, and it tells you if the oldest item has waited long enough. When you've sent, you `flush`. It never sees your payloads. It only ever answers one question: is now the time?
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'batch_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ # Up to 500 rows per insert, and no row waits longer than 50ms.
15
+ batch = BatchMachines::Batch.new(500, 50_000_000)
16
+ rows = []
17
+
18
+ def ingest(batch, rows, row)
19
+ rows << row
20
+ case batch.push(now_ns)
21
+ when 'size', 'deadline' then insert_all(batch, rows)
22
+ end
23
+ end
24
+
25
+ def tick(batch, rows)
26
+ insert_all(batch, rows) if batch.poll(now_ns)
27
+ end
28
+
29
+ def insert_all(batch, rows)
30
+ Row.insert_all(rows)
31
+ rows.clear
32
+ batch.flush(now_ns)
33
+ end
34
+ ```
35
+
36
+ Time is Integer nanoseconds from a monotonic clock. `now_ns` is whatever you use for that, such as `Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)`.
37
+
38
+ ## A Message to the Resistance
39
+
40
+ So the event stream got busy and you were doing one INSERT per event. The database said no. You switched to batches of 500, and throughput went up tenfold. Then support opened a ticket: the last 37 events of every day show up the next morning, because 37 is less than 500 and nothing ever told them to go.
41
+
42
+ You added a timer. Now there's a batch size *and* a deadline, and a ticket about which one wins when both fire in the same tick, and another about the deadline resetting every time an item arrives so a steady trickle never flushes at all.
43
+
44
+ This is Nagle's problem from 1984 with the names changed. Group small things into bigger things, but never make the first small thing wait forever. The bookkeeping for that is three numbers: how many are waiting, how many you want before sending, and when the oldest one arrived. This gem keeps those three numbers and makes the call. The sending is still yours.
45
+
46
+ ## Chapter 1: Stardate 2025.186, the AMS Nagle
47
+
48
+ > **AMS Nagle**, NCC-4202, *Nagle*-class cargo consolidator.
49
+ > Fewer launches. Fuller holds. No forgotten departure deadline.
50
+
51
+ The consolidator doesn't fly. It watches the loading bay. Every pod that rolls in is logged, and the moment the hold reaches target size, or the first pod has sat on the deck for its full wait, the launch order goes out. One launch per pod burns the convoy's fuel. One pod waiting a week burns the mission.
52
+
53
+ Somewhere in the bay, a loading officer proposes:
54
+
55
+ ```ruby
56
+ def enqueue(row)
57
+ buffer << row
58
+ flush_all if buffer.size >= 500
59
+ end
60
+ ```
61
+
62
+ "That," says the consolidator, "is how thirty-seven pods spend the night on the deck."
63
+
64
+ ## The Flight Plan
65
+
66
+ Three things can be true of the hold: it has room and no reason to launch, it has reached its target, or the first pod has waited too long. Deadline beats size when both are true.
67
+
68
+ ```mermaid
69
+ stateDiagram-v2
70
+ [*] --> empty
71
+ empty --> buffering: push, buffered (oldest timestamp set)
72
+ buffering --> buffering: push, buffered
73
+ buffering --> size_ready: push or poll, pending >= target
74
+ buffering --> deadline_ready: push or poll, oldest waited max_wait
75
+ size_ready --> deadline_ready: oldest waited max_wait
76
+ size_ready --> size_ready: push accepted (until capacity, then full)
77
+ size_ready --> empty: flush
78
+ deadline_ready --> empty: flush
79
+ buffering --> empty: flush
80
+ ```
81
+
82
+ And a slow evening, with a trickle that never reaches 500:
83
+
84
+ ```mermaid
85
+ sequenceDiagram
86
+ participant App
87
+ participant Batch
88
+ participant DB
89
+
90
+ App->>Batch: push(t=0ms)
91
+ Batch-->>App: buffered
92
+ App->>Batch: push(t=20ms)
93
+ Batch-->>App: buffered
94
+ App->>Batch: push(t=45ms)
95
+ Batch-->>App: buffered
96
+ Note over Batch: oldest arrived at 0ms, max wait 50ms
97
+
98
+ App->>Batch: poll(t=50ms)
99
+ Batch-->>App: deadline
100
+ App->>DB: INSERT 3 rows
101
+ App->>Batch: flush(t=51ms)
102
+ Batch-->>App: 3
103
+ Note over Batch: hold empty, no deadline pending
104
+ ```
105
+
106
+ ## How the Hold Works
107
+
108
+ ### Push
109
+
110
+ Every arrival is a `push` with the current time. The return value is a String telling you what to do.
111
+
112
+ ```ruby
113
+ batch = BatchMachines::Batch.new(2, 10)
114
+
115
+ batch.push(1) # => "buffered" room left, no reason to send
116
+ batch.push(2) # => "size" target reached, send now
117
+ batch.push(3) # => "full" capacity reached, this one was NOT accepted
118
+ batch.pending # => 2
119
+ ```
120
+
121
+ `"buffered"`, `"size"`, and `"deadline"` all mean the item was counted. `"full"` means it wasn't, and you need to flush before it can be. Keep your own list of payloads alongside, and only append when the push was accepted.
122
+
123
+ ### Poll
124
+
125
+ Nothing arrives, time passes. `poll` asks the same question without adding anything. It answers `nil` when there's nothing to do.
126
+
127
+ ```ruby
128
+ batch = BatchMachines::Batch.new(100, 50_000_000)
129
+ batch.push(1_000)
130
+ batch.push(40_000_000)
131
+
132
+ batch.poll(50_000_999) # => nil oldest has waited 49.999999ms
133
+ batch.poll(50_001_000) # => "deadline" oldest has waited 50ms
134
+ ```
135
+
136
+ The deadline belongs to the oldest item and never moves. A new arrival doesn't push it back, and neither does changing the target.
137
+
138
+ ### Flush
139
+
140
+ When you've sent, say so. `flush` returns how many were waiting and empties the hold.
141
+
142
+ ```ruby
143
+ batch.flush(50_001_000) # => 2
144
+ batch.flush(50_001_000) # => 0
145
+ batch.poll(60_000_000) # => nil
146
+ ```
147
+
148
+ Flushing an empty hold is fine and returns zero.
149
+
150
+ ### Deadline wins
151
+
152
+ When an arrival both fills the target and lands at the deadline, you hear about the deadline. The older reason takes precedence.
153
+
154
+ ```ruby
155
+ batch = BatchMachines::Batch.new(2, 10)
156
+ batch.push(0) # => "buffered"
157
+ batch.push(10) # => "deadline"
158
+ ```
159
+
160
+ ### Adapting the target
161
+
162
+ The target size starts at capacity. If you'd rather it follow conditions downstream, such as insert latency, feed `adapt` an error signal and it moves the target for you, clamped to `1..capacity`. Out of the box it's a proportional controller with gain 1, so the target simply follows the number you give it. The second argument is how much time passed since the last call, in nanoseconds.
163
+
164
+ ```ruby
165
+ batch = BatchMachines::Batch.new(100, 50_000_000)
166
+ batch.target # => 100
167
+
168
+ batch.adapt(10, 1_000_000_000) # => 10
169
+ batch.adapt(-3, 1_000_000_000) # => 1 never below one
170
+ batch.adapt(1000, 1_000_000_000) # => 100 never above capacity
171
+ ```
172
+
173
+ Need memory? `configure_pid(p, i, d)` sets the gains. With only an integral gain the target climbs a little more each call:
174
+
175
+ ```ruby
176
+ batch = BatchMachines::Batch.new(100, 10)
177
+ batch.configure_pid(0, 1.0, 0)
178
+
179
+ batch.adapt(5.0, 1_000_000_000) # => 5
180
+ batch.adapt(5.0, 1_000_000_000) # => 10
181
+ batch.adapt(5.0, 1_000_000_000) # => 15
182
+ ```
183
+
184
+ Lowering the target doesn't drop anything already in the hold. Items past the target still count, and the next push or poll says `"size"` until you flush.
185
+
186
+ ```ruby
187
+ batch = BatchMachines::Batch.new(5, 10)
188
+ batch.push(0)
189
+ batch.adapt(2.0, 1_000_000_000) # => 2
190
+ batch.push(1) # => "size"
191
+ batch.push(2) # => "size" accepted, still under capacity 5
192
+ batch.pending # => 3
193
+ ```
194
+
195
+ ## The Rules of the Consolidator
196
+
197
+ - **Capacity is an Integer** in `1...2**32`, and `max_wait_ns` is a positive Integer. Zero for either raises `ArgumentError`.
198
+ - **Time is unsigned 64-bit Integer nanoseconds.** Floats and strings raise `TypeError`, negative or overflow raises `RangeError`.
199
+ - **Time only moves forward.** A `push` with an earlier clock than the last one returns `"rejected"` and counts nothing. `poll` and `flush` with an earlier clock return `nil` and change nothing.
200
+ - **Gains are finite and non-negative.** Infinite, NaN, or negative gains raise `ArgumentError`. An error of infinity or NaN raises too, and `adapt` with zero elapsed raises.
201
+ - **A blown-up controller returns `nil`** and leaves the target where it was, rather than handing you a nonsense size.
202
+ - **Frozen means frozen.** `freeze` the batch and `push`, `poll`, `flush`, `adapt`, and `configure_pid` all raise `FrozenError`.
203
+
204
+ ## What You Think You're Doing vs Reality
205
+
206
+ ### You Think: "Batch of 500 or 50ms, whichever comes first. Easy."
207
+ ### Reality: Whichever comes first, measured from when?
208
+
209
+ Measured from the newest item and a steady trickle never flushes. Measured from the last flush and the first item after a quiet hour waits the full 50ms plus however long the hour was. Measured from the oldest item, with the deadline pinned the moment it lands, and everything behaves. That last one is the only correct answer and it's the only one this gem knows how to give.
210
+
211
+ ## What's Yours
212
+
213
+ The batch counts. It doesn't hold your rows, doesn't run a timer, and doesn't call the database. You keep the payloads in a list next to it, you call `poll` from whatever tick you already have, and you call `flush` after the send actually succeeded, not before. If the send fails, don't flush. The hold still knows how many are waiting and when the oldest arrived.
214
+
215
+ ## License
216
+
217
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
218
+
219
+ **Remember: The last thirty-seven events of the day are still events. Someone has to tell them to go.**
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.3.0](https://github.com/seuros/batch_machines/compare/batch-machines-v0.2.2...batch-machines-v0.3.0) (2026-11-06)
4
+
5
+
6
+ ### Features
7
+
8
+ * expose the pending deadline and weighted pushes ([9044fa3](https://github.com/seuros/batch_machines/commit/9044fa323859bcd6fb6cf9a7b5c884bf503fedc4))
9
+
10
+ ## [0.2.2](https://github.com/seuros/batch_machines/compare/batch-machines-v0.2.1...batch-machines-v0.2.2) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * crates.io keywords and categories for the core ([b150571](https://github.com/seuros/batch_machines/commit/b150571563871f7af013b716402aa169688deaf2))
16
+
17
+ ## [0.2.1](https://github.com/seuros/batch_machines/compare/batch-machines-v0.2.0...batch-machines-v0.2.1) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * keep test sources out of the packaged gem and crate ([2287c10](https://github.com/seuros/batch_machines/commit/2287c1089fefd9154740078a6977598f47033d0e))
23
+
24
+ ## [0.2.0](https://github.com/seuros/batch_machines/compare/batch-machines-v0.1.0...batch-machines-v0.2.0) (2026-11-06)
25
+
26
+
27
+ ### Features
28
+
29
+ * initial extraction ([da62e4b](https://github.com/seuros/batch_machines/commit/da62e4bb16f3d1eaecc93ebf2a739f86fcfee932))
@@ -0,0 +1,21 @@
1
+ # batch-machines
2
+
3
+ > **AMS Nagle**, NCC-4202, *Nagle*-class cargo consolidator.
4
+
5
+ Engine-room flight manual: fill the cargo hold without missing the launch window.
6
+
7
+ Allocation-free `no_std` size/deadline batch decisions. The first item establishes
8
+ a fixed maximum-wait deadline; later arrivals and target adaptation never move
9
+ that deadline. Poll with explicit monotonic time, or read `deadline()` and sleep
10
+ until it: it is the absolute time at which `poll` first reports `Deadline`, and
11
+ `None` while nothing is pending. A flush recommendation does not flush payloads:
12
+ the adapter must execute/acknowledge the flush, which also clears the deadline.
13
+
14
+ Capacity is a budget of cost units. `push` spends one unit per item;
15
+ `push_weighted` spends the item's cost, so a byte budget flushes at a byte
16
+ target. An item costing more than the capacity is never admitted. Flow credits
17
+ bound the pending cost. PID output can adapt the target cost, truncated toward
18
+ zero and clamped to `[1, capacity]`. Error definition, gains,
19
+ measurement, and downstream flush concurrency are caller policies. This cannot
20
+ guarantee an end-to-end latency budget if timers/execution are delayed. No I/O,
21
+ timer driver, payload storage, partial flush, or Ruby binding is included.
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'ffi/extconf'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module BatchMachines
3
+ VERSION = '0.1.4'
4
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'batch_machines/version'
3
+ require 'batch_machines_native/batch_machines_native'
@@ -0,0 +1,13 @@
1
+ module BatchMachines
2
+ VERSION: String
3
+ class Batch
4
+ def self.new: (Integer capacity, Integer max_wait_ns) -> instance
5
+ def configure_pid: (Float | Integer p, Float | Integer i, Float | Integer d) -> nil
6
+ def push: (Integer now_ns) -> String
7
+ def poll: (Integer now_ns) -> String?
8
+ def flush: (Integer now_ns) -> Integer?
9
+ def adapt: (Float | Integer error, Integer elapsed_ns) -> Integer?
10
+ def pending: () -> Integer
11
+ def target: () -> Integer
12
+ end
13
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: batch_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: Initial native batch policy model. Caller owns payloads and execution.
14
+ Local sibling Rust checkouts required.
15
+ email:
16
+ - oss@seuros.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - CHANGELOG.md
22
+ - LICENSE.txt
23
+ - README.md
24
+ - ext/batch_machines_native/core/CHANGELOG.md
25
+ - ext/batch_machines_native/core/README.md
26
+ - ext/batch_machines_native/extconf.rb
27
+ - lib/batch_machines.rb
28
+ - lib/batch_machines/version.rb
29
+ - lib/batch_machines_native/batch_machines_native.bundle
30
+ - sig/batch_machines.rbs
31
+ homepage: https://github.com/seuros/batch_machines
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ source_code_uri: https://github.com/seuros/batch_machines
36
+ rubygems_mfa_required: 'true'
37
+ cargo_crate_name: batch_machines_native
38
+ cargo_manifest_path: ext/batch_machines_native/ffi/Cargo.toml
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: 4.1.dev
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.5.23
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Native size/deadline batch decisions with PID adaptation
61
+ test_files: []