shed_machines 0.1.1

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: 061e0fb85399ec23ed73e673a6ca55d406b3c1a775a6c3fade1a84e836bcb615
4
+ data.tar.gz: e02e410d9a5909bf71101a5d5cbe80cfc70e1aa6a42e0a6c3eb6509115078cb5
5
+ SHA512:
6
+ metadata.gz: 274e603824e19a217e2762cec86d6c43b1756ac955b753afb1061b9b5ed4dcd96a745b92c10c96fe6b80d5221fb95be4d3b109ce652d857d76d2557d096c0b78
7
+ data.tar.gz: 6e928923121a47f7c38f963c3f5c09b4e77c408c7b7a6d122f18b1587e54983949c2067cd2d73b3e4d16784bc51b03ae2a5a7bd29967f4685ec6dc40366781ab
data/CHANGELOG.md ADDED
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ ## [0.1.1](https://github.com/seuros/shed_machines/compare/shed_machines-v0.1.0...shed_machines/v0.1.1) (2026-11-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * adaptive concurrency limits and load shedding ([e07f0dc](https://github.com/seuros/shed_machines/commit/e07f0dca6cc3dc65a49757d5eda0b96256310c61))
9
+ * pass a rubygems OTP through the environment ([8a4d15b](https://github.com/seuros/shed_machines/commit/8a4d15bc46fed686dd67da5566b8d5c252de617c))
10
+ * Ruby gem and magnus FFI for the control loops ([b323351](https://github.com/seuros/shed_machines/commit/b323351d11a2ee01782e93cafe769a24e4485ed0))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * a configured CoDel duration goes through the same check as an observed one ([c424040](https://github.com/seuros/shed_machines/commit/c424040c95e419f88d20f06d19cd07bf5fba30f5))
16
+ * a source install can build, and a failed gem push fails the release ([4975e7f](https://github.com/seuros/shed_machines/commit/4975e7fcc39820ec681d79e8d0e3213878c61487))
17
+ * AIMD divided by zero, and both controllers saturated before dividing ([67006bd](https://github.com/seuros/shed_machines/commit/67006bdad51f9caef02594f042f2b5a5c7d34735))
18
+ * avoid re-entrant borrow and u32 clipping in shed control ([d4a1b09](https://github.com/seuros/shed_machines/commit/d4a1b0950e6de32bb2756f5ef183e649ff04d829))
19
+ * collect the built gems whatever depth the download puts them at ([7ffc2db](https://github.com/seuros/shed_machines/commit/7ffc2db96f97a1b30b3992b16d30dc45fd43f216))
20
+ * refuse a fractional call count instead of truncating it to zero ([869c3d8](https://github.com/seuros/shed_machines/commit/869c3d8988ae4e1be5924899466522053fa47981))
21
+ * reject durations and bounds the arithmetic cannot survive ([37edc6f](https://github.com/seuros/shed_machines/commit/37edc6f8849e9fff6192955cc41e910a930b3fcf))
22
+ * ship the core's test files so the packaged crate builds ([9a3e00d](https://github.com/seuros/shed_machines/commit/9a3e00dffffff31bbebfbaf6e9c57e2d21cea43e))
23
+ * the release tag carries the gem name, and the gate uses the aggregate output ([f0937ad](https://github.com/seuros/shed_machines/commit/f0937ad7ecbab770aaa81378c1035f80442291b7))
24
+
25
+ ## 0.1.0
26
+
27
+ - Initial release.
28
+ - `ShedMachines::Aimd`: additive increase per saturated success, multiplicative
29
+ decrease on overload, with a configurable `kept / of` backoff fraction.
30
+ - `ShedMachines::Vegas`: TCP Vegas applied to a concurrency limit, steering by
31
+ the queue estimate drawn from the gap between current and best round-trip
32
+ time, with a configurable `alpha`/`beta` band.
33
+ - `ShedMachines::Codel`: controlled delay, shedding queued work by how long it
34
+ waited once the wait stays above target for a full interval, with sheds that
35
+ come faster as the inverse square root of how many have happened.
36
+ - `ShedMachines::Budget`: client-side adaptive throttling, reporting the
37
+ fraction of the next retry wave to drop rather than making the decision.
38
+ - Durations cross the Ruby boundary as seconds in Floats and are converted once
39
+ to nanoseconds; the control laws themselves are integer-only.
40
+ - `shed-machines` core crate: the four control laws in `no_std` Rust with zero
41
+ dependencies. Builds and lints clean on bare metal.
42
+ - RBS signatures for the whole public API.
data/Cargo.lock ADDED
@@ -0,0 +1,286 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "aho-corasick"
7
+ version = "1.1.5"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
10
+ dependencies = [
11
+ "memchr",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "bindgen"
16
+ version = "0.72.1"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
19
+ dependencies = [
20
+ "bitflags",
21
+ "cexpr",
22
+ "clang-sys",
23
+ "itertools",
24
+ "proc-macro2",
25
+ "quote",
26
+ "regex",
27
+ "rustc-hash",
28
+ "shlex",
29
+ "syn",
30
+ ]
31
+
32
+ [[package]]
33
+ name = "bitflags"
34
+ version = "2.13.2"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
37
+
38
+ [[package]]
39
+ name = "cexpr"
40
+ version = "0.6.0"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
43
+ dependencies = [
44
+ "nom",
45
+ ]
46
+
47
+ [[package]]
48
+ name = "cfg-if"
49
+ version = "1.0.5"
50
+ source = "registry+https://github.com/rust-lang/crates.io-index"
51
+ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
52
+
53
+ [[package]]
54
+ name = "clang-sys"
55
+ version = "1.9.1"
56
+ source = "registry+https://github.com/rust-lang/crates.io-index"
57
+ checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
58
+ dependencies = [
59
+ "glob",
60
+ "libc",
61
+ "libloading",
62
+ ]
63
+
64
+ [[package]]
65
+ name = "either"
66
+ version = "1.19.0"
67
+ source = "registry+https://github.com/rust-lang/crates.io-index"
68
+ checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be"
69
+
70
+ [[package]]
71
+ name = "glob"
72
+ version = "0.3.4"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
75
+
76
+ [[package]]
77
+ name = "itertools"
78
+ version = "0.13.0"
79
+ source = "registry+https://github.com/rust-lang/crates.io-index"
80
+ checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
81
+ dependencies = [
82
+ "either",
83
+ ]
84
+
85
+ [[package]]
86
+ name = "lazy_static"
87
+ version = "1.5.1"
88
+ source = "registry+https://github.com/rust-lang/crates.io-index"
89
+ checksum = "20870f649af7073d53e38067b2a84312175d56ea15217e1b15bc83506ec50afb"
90
+
91
+ [[package]]
92
+ name = "libc"
93
+ version = "0.2.191"
94
+ source = "registry+https://github.com/rust-lang/crates.io-index"
95
+ checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
96
+
97
+ [[package]]
98
+ name = "libloading"
99
+ version = "0.8.9"
100
+ source = "registry+https://github.com/rust-lang/crates.io-index"
101
+ checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
102
+ dependencies = [
103
+ "cfg-if",
104
+ "windows-link",
105
+ ]
106
+
107
+ [[package]]
108
+ name = "magnus"
109
+ version = "0.9.3"
110
+ source = "registry+https://github.com/rust-lang/crates.io-index"
111
+ checksum = "40ef424404d166cbb71c8b8d19c9c589c748c4547c1afa1928c5504299f6c713"
112
+ dependencies = [
113
+ "magnus-macros",
114
+ "rb-sys",
115
+ "rb-sys-env",
116
+ "seq-macro",
117
+ ]
118
+
119
+ [[package]]
120
+ name = "magnus-macros"
121
+ version = "0.9.1"
122
+ source = "registry+https://github.com/rust-lang/crates.io-index"
123
+ checksum = "9dafde2eeb66f090e0f326112323eb8324b12cb32b5d1210e541d3df6b0ce77e"
124
+ dependencies = [
125
+ "proc-macro2",
126
+ "quote",
127
+ "syn",
128
+ ]
129
+
130
+ [[package]]
131
+ name = "memchr"
132
+ version = "2.8.3"
133
+ source = "registry+https://github.com/rust-lang/crates.io-index"
134
+ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
135
+
136
+ [[package]]
137
+ name = "minimal-lexical"
138
+ version = "0.2.1"
139
+ source = "registry+https://github.com/rust-lang/crates.io-index"
140
+ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
141
+
142
+ [[package]]
143
+ name = "nom"
144
+ version = "7.1.3"
145
+ source = "registry+https://github.com/rust-lang/crates.io-index"
146
+ checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
147
+ dependencies = [
148
+ "memchr",
149
+ "minimal-lexical",
150
+ ]
151
+
152
+ [[package]]
153
+ name = "proc-macro2"
154
+ version = "1.0.107"
155
+ source = "registry+https://github.com/rust-lang/crates.io-index"
156
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
157
+ dependencies = [
158
+ "unicode-ident",
159
+ ]
160
+
161
+ [[package]]
162
+ name = "quote"
163
+ version = "1.0.47"
164
+ source = "registry+https://github.com/rust-lang/crates.io-index"
165
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
166
+ dependencies = [
167
+ "proc-macro2",
168
+ ]
169
+
170
+ [[package]]
171
+ name = "rb-sys"
172
+ version = "0.9.130"
173
+ source = "registry+https://github.com/rust-lang/crates.io-index"
174
+ checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
175
+ dependencies = [
176
+ "rb-sys-build",
177
+ ]
178
+
179
+ [[package]]
180
+ name = "rb-sys-build"
181
+ version = "0.9.130"
182
+ source = "registry+https://github.com/rust-lang/crates.io-index"
183
+ checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
184
+ dependencies = [
185
+ "bindgen",
186
+ "lazy_static",
187
+ "proc-macro2",
188
+ "quote",
189
+ "regex",
190
+ "shell-words",
191
+ "syn",
192
+ ]
193
+
194
+ [[package]]
195
+ name = "rb-sys-env"
196
+ version = "0.2.3"
197
+ source = "registry+https://github.com/rust-lang/crates.io-index"
198
+ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
199
+
200
+ [[package]]
201
+ name = "regex"
202
+ version = "1.13.1"
203
+ source = "registry+https://github.com/rust-lang/crates.io-index"
204
+ checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
205
+ dependencies = [
206
+ "aho-corasick",
207
+ "memchr",
208
+ "regex-automata",
209
+ "regex-syntax",
210
+ ]
211
+
212
+ [[package]]
213
+ name = "regex-automata"
214
+ version = "0.4.18"
215
+ source = "registry+https://github.com/rust-lang/crates.io-index"
216
+ checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
217
+ dependencies = [
218
+ "aho-corasick",
219
+ "memchr",
220
+ "regex-syntax",
221
+ ]
222
+
223
+ [[package]]
224
+ name = "regex-syntax"
225
+ version = "0.8.11"
226
+ source = "registry+https://github.com/rust-lang/crates.io-index"
227
+ checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
228
+
229
+ [[package]]
230
+ name = "rustc-hash"
231
+ version = "2.1.3"
232
+ source = "registry+https://github.com/rust-lang/crates.io-index"
233
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
234
+
235
+ [[package]]
236
+ name = "seq-macro"
237
+ version = "0.3.6"
238
+ source = "registry+https://github.com/rust-lang/crates.io-index"
239
+ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
240
+
241
+ [[package]]
242
+ name = "shed-machines"
243
+ version = "0.2.0"
244
+
245
+ [[package]]
246
+ name = "shed_machines_native"
247
+ version = "0.1.1"
248
+ dependencies = [
249
+ "magnus",
250
+ "shed-machines",
251
+ ]
252
+
253
+ [[package]]
254
+ name = "shell-words"
255
+ version = "1.1.1"
256
+ source = "registry+https://github.com/rust-lang/crates.io-index"
257
+ checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
258
+
259
+ [[package]]
260
+ name = "shlex"
261
+ version = "1.3.0"
262
+ source = "registry+https://github.com/rust-lang/crates.io-index"
263
+ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
264
+
265
+ [[package]]
266
+ name = "syn"
267
+ version = "2.0.119"
268
+ source = "registry+https://github.com/rust-lang/crates.io-index"
269
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
270
+ dependencies = [
271
+ "proc-macro2",
272
+ "quote",
273
+ "unicode-ident",
274
+ ]
275
+
276
+ [[package]]
277
+ name = "unicode-ident"
278
+ version = "1.0.27"
279
+ source = "registry+https://github.com/rust-lang/crates.io-index"
280
+ checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563"
281
+
282
+ [[package]]
283
+ name = "windows-link"
284
+ version = "0.2.1"
285
+ source = "registry+https://github.com/rust-lang/crates.io-index"
286
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
data/Cargo.toml ADDED
@@ -0,0 +1,24 @@
1
+ [workspace]
2
+ members = ["ext/shed_machines_native/core", "ext/shed_machines_native/ffi"]
3
+ resolver = "3"
4
+
5
+ [workspace.package]
6
+ edition = "2024"
7
+ rust-version = "1.99"
8
+
9
+ [workspace.dependencies]
10
+ shed-machines = { path = "ext/shed_machines_native/core" }
11
+
12
+ [workspace.lints.rust]
13
+ unsafe_code = "forbid"
14
+ unused_must_use = "warn"
15
+
16
+ [workspace.lints.clippy]
17
+ all = { level = "warn", priority = -1 }
18
+ pedantic = { level = "warn", priority = -1 }
19
+ nursery = { level = "warn", priority = -1 }
20
+ expect_used = "deny"
21
+ unwrap_used = "deny"
22
+ uninlined_format_args = "deny"
23
+ needless_borrow = "deny"
24
+ redundant_clone = "deny"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 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,174 @@
1
+ # ShedMachines 🌊
2
+
3
+ > **Adaptive concurrency limits and load shedding.** The escort that reads the
4
+ > convoy's own telemetry and says how wide the lane is, right now.
5
+
6
+ A fixed bulkhead is a guess that is wrong at every load except the one it was
7
+ tuned for. **AMS Jacobson** does not carry a tuned number. These four control
8
+ loops read what completed calls report and answer one question each: how many
9
+ calls may be in flight, whether this queued piece of work is still worth
10
+ running, and how much of the next retry wave to drop before it leaves the
11
+ process.
12
+
13
+ ---
14
+
15
+ ## 🚀 Launch Sequence
16
+
17
+ ```bash
18
+ # Add to your ship's Gemfile
19
+ gem 'shed_machines'
20
+ ```
21
+
22
+ Requires **Ruby 4.0+** and a Rust toolchain, or a platform gem.
23
+
24
+ ---
25
+
26
+ ## 🎯 Mission Brief
27
+
28
+ ```ruby
29
+ require 'shed_machines'
30
+
31
+ control = ShedMachines::Vegas.new(limit: 10, min: 1, max: 200)
32
+
33
+ control.observe(rtt: 0.004, in_flight: 10, outcome: :ok)
34
+ control.limit # => 11
35
+
36
+ control.observe(rtt: 0.040, in_flight: 11, outcome: :ok)
37
+ control.limit # => 10
38
+ ```
39
+
40
+ The escort advises, it does not fire. Nothing here enforces anything. The
41
+ caller owns the semaphore, the queue and the dice.
42
+
43
+ ---
44
+
45
+ ## 🔩 The Native Extension Is Required
46
+
47
+ There is no pure-Ruby fallback and no switch to turn one on. `require
48
+ 'shed_machines'` loads the extension, and fails if it is not there. A control
49
+ law with two implementations is a control law with two behaviours, and the whole
50
+ point of these four is that they are the same arithmetic everywhere: the gem is
51
+ a binding to the `shed-machines` crate in `ext/shed_machines_native/core`, which
52
+ is `no_std` with zero dependencies and also runs on bare metal. It needs Ruby
53
+ 4.0 and a Rust toolchain, or a platform gem.
54
+
55
+ ---
56
+
57
+ ## 🎚️ Moving a Concurrency Limit
58
+
59
+ `Aimd` grows the limit by one whenever a saturated call succeeds, and keeps
60
+ three quarters of it on any overload. It needs no latency signal.
61
+
62
+ ```ruby
63
+ control = ShedMachines::Aimd.new(limit: 10, min: 1, max: 100)
64
+
65
+ control.observe(rtt: 0.004, in_flight: 10, outcome: :ok) # saturated: grows
66
+ control.limit # => 11
67
+
68
+ control.observe(rtt: 0.004, in_flight: 11, outcome: :overload)
69
+ control.limit # => 8
70
+ ```
71
+
72
+ `Vegas` infers queue depth from the gap between the current round-trip time and
73
+ the best one it has seen, because latency alone cannot tell a slow dependency
74
+ from a deep queue. It grows while fewer than `alpha` calls look queued, shrinks
75
+ above `beta`, and holds in between.
76
+
77
+ ```ruby
78
+ control = ShedMachines::Vegas.new(limit: 10, min: 1, max: 200, alpha: 3, beta: 6)
79
+ control.best_rtt # => nil, until the first completed call
80
+ ```
81
+
82
+ Both take `outcome: :ok` or `outcome: :overload`, where overload means the call
83
+ timed out, was rejected, or otherwise says the dependency is loaded. Anything
84
+ else raises `ArgumentError`. Both clamp a starting limit into `min..max`, which
85
+ default to `LOWEST_FLOOR` and `DEFAULT_CEILING`.
86
+
87
+ ---
88
+
89
+ ## ⏱️ Shedding by Waiting Time
90
+
91
+ Queue length is the wrong signal, since a fast queue can be long and a stalled
92
+ one short. `Codel` watches how long work waited and sheds only once that stays
93
+ above target for a full interval, so a burst passes through and a standing queue
94
+ does not.
95
+
96
+ ```ruby
97
+ queue = ShedMachines::Codel.new # 5 ms target, 100 ms interval
98
+
99
+ queue.admit(sojourn: 0.020, now: 0.000) # => :admit, the burst is tolerated
100
+ queue.admit(sojourn: 0.020, now: 0.150) # => :shed, it is a standing queue
101
+ queue.shedding? # => true
102
+ queue.admit(sojourn: 0.001, now: 0.200) # => :admit, and the state clears
103
+ ```
104
+
105
+ Successive sheds come faster, as the inverse square root of how many have
106
+ happened, so the control law converges instead of oscillating.
107
+
108
+ ---
109
+
110
+ ## 🌀 Not Amplifying an Outage
111
+
112
+ A client that keeps retrying a failing dependency multiplies the load that broke
113
+ it. `Budget` tracks what the client asked for against what was accepted and
114
+ reports what fraction of the next wave to drop.
115
+
116
+ ```ruby
117
+ budget = ShedMachines::Budget.new # each accept earns 2 requests
118
+
119
+ 100.times { budget.requested.accepted }
120
+ budget.shed_probability # => 0.0
121
+
122
+ 300.times { budget.requested }
123
+ budget.shed_probability # => 0.4987...
124
+
125
+ budget.decay # halves both counts
126
+ ```
127
+
128
+ `shed_probability` is a fraction, not a decision, so nothing here needs a random
129
+ number generator.
130
+
131
+ ---
132
+
133
+ ## 📐 Seconds In, Nanoseconds Inside
134
+
135
+ Every duration crosses this boundary as seconds in a `Float`, and is converted
136
+ once to nanoseconds. The control laws are integer-only on purpose: a limit, a
137
+ queue estimate and a backoff are counts, and counting in floats invites a
138
+ limiter that is 10.999999 calls wide.
139
+
140
+ ---
141
+
142
+ ## 📊 Constants on the Instrument Panel
143
+
144
+ The Ruby constants mirror the crate's, with the same names and values.
145
+
146
+ | Constant | Value |
147
+ | ---------------------------------- | ----------- |
148
+ | `ShedMachines::LOWEST_FLOOR` | 1 |
149
+ | `ShedMachines::DEFAULT_CEILING` | 1000 |
150
+ | `Aimd::DEFAULT_BACKOFF_KEPT` | 3 |
151
+ | `Aimd::DEFAULT_BACKOFF_OF` | 4 |
152
+ | `Vegas::DEFAULT_ALPHA` | 3 |
153
+ | `Vegas::DEFAULT_BETA` | 6 |
154
+ | `Vegas::OVERLOAD_DIVISOR` | 2 |
155
+ | `Codel::DEFAULT_TARGET_NS` | 5_000_000 |
156
+ | `Codel::DEFAULT_INTERVAL_NS` | 100_000_000 |
157
+ | `Budget::DEFAULT_MULTIPLIER` | 2.0 |
158
+
159
+ ---
160
+
161
+ ## 📡 Credits
162
+
163
+ **AMS Jacobson**, NCC-2012, *Jacobson*-class escort.
164
+
165
+ Named for Kathleen Nichols and Van Jacobson, *Controlling Queue Delay* (2012),
166
+ the paper that introduced controlled delay, CoDel, and argued that a queue
167
+ should be judged by how long work waits in it rather than by how much work is
168
+ in it.
169
+
170
+ ---
171
+
172
+ ## License
173
+
174
+ MIT.
@@ -0,0 +1,24 @@
1
+ [package]
2
+ name = "shed-machines"
3
+ version = "0.2.0"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ authors = ["Abdelkader Boudih <oss@seuros.com>"]
7
+ license = "MIT"
8
+ description = "Adaptive concurrency limits and load shedding - AIMD, Vegas, CoDel and retry budgets, no_std with zero dependencies"
9
+ repository = "https://github.com/seuros/shed_machines"
10
+ keywords = ["load-shedding", "concurrency", "codel", "backpressure", "no-std"]
11
+ categories = ["no-std", "concurrency"]
12
+ readme = "README.md"
13
+
14
+ [lib]
15
+ name = "shed_machines"
16
+ crate-type = ["lib"]
17
+
18
+ [features]
19
+ default = ["std"]
20
+ std = ["alloc"]
21
+ alloc = []
22
+
23
+ [lints]
24
+ workspace = true
@@ -0,0 +1,89 @@
1
+ //! Additive increase, multiplicative decrease.
2
+
3
+ use crate::{Bounds, Controller, Outcome, Sample};
4
+
5
+ /// Numerator of the fraction kept after an overload.
6
+ pub const DEFAULT_BACKOFF_KEPT: u32 = 3;
7
+
8
+ /// Denominator of the fraction kept after an overload. Three quarters backs
9
+ /// off faster than a dependency usually recovers, without the halving that
10
+ /// makes a limit oscillate.
11
+ pub const DEFAULT_BACKOFF_OF: u32 = 4;
12
+
13
+ /// Calls added per saturated success. One is what makes this additive.
14
+ const INCREASE: u32 = 1;
15
+
16
+ /// Grows the limit by one whenever a saturated call succeeds, and cuts it by a
17
+ /// factor on any overload. The limit only grows while the caller is actually
18
+ /// using it, so an idle client does not drift upward.
19
+ ///
20
+ /// ```
21
+ /// use shed_machines::{Aimd, Bounds, Controller, Sample};
22
+ ///
23
+ /// let mut control = Aimd::new(Bounds::new(1, 100), 10);
24
+ ///
25
+ /// control.observe(Sample::ok(1_000, 10));
26
+ /// assert_eq!(control.limit(), 11);
27
+ ///
28
+ /// control.observe(Sample::overload(1_000, 11));
29
+ /// assert_eq!(control.limit(), 8);
30
+ /// ```
31
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
32
+ pub struct Aimd {
33
+ bounds: Bounds,
34
+ limit: u32,
35
+ kept: u32,
36
+ of: u32,
37
+ }
38
+
39
+ impl Aimd {
40
+ #[must_use]
41
+ pub const fn new(bounds: Bounds, limit: u32) -> Self {
42
+ Self {
43
+ bounds,
44
+ limit: bounds.clamp(limit),
45
+ kept: DEFAULT_BACKOFF_KEPT,
46
+ of: DEFAULT_BACKOFF_OF,
47
+ }
48
+ }
49
+
50
+ /// Keep `kept / of` of the limit after an overload. See
51
+ /// [`DEFAULT_BACKOFF_KEPT`].
52
+ #[must_use]
53
+ pub const fn backoff(mut self, kept: u32, of: u32) -> Self {
54
+ self.kept = kept;
55
+ self.of = of;
56
+ self
57
+ }
58
+ }
59
+
60
+ impl Aimd {
61
+ /// A zero denominator keeps the limit rather than dividing by zero, and the
62
+ /// product is widened so a representable quotient is not lost to saturation.
63
+ fn kept_limit(&self) -> u32 {
64
+ if self.of == 0 {
65
+ return self.limit;
66
+ }
67
+ let scaled = u64::from(self.limit) * u64::from(self.kept) / u64::from(self.of);
68
+ u32::try_from(scaled).unwrap_or(u32::MAX)
69
+ }
70
+ }
71
+
72
+ impl Controller for Aimd {
73
+ fn observe(&mut self, sample: Sample) {
74
+ self.limit = match sample.outcome {
75
+ Outcome::Overload => self.bounds.clamp(self.kept_limit()),
76
+ Outcome::Ok if sample.in_flight >= self.limit => {
77
+ self.bounds.clamp(self.limit.saturating_add(INCREASE))
78
+ }
79
+ Outcome::Ok => self.limit,
80
+ };
81
+ }
82
+
83
+ fn limit(&self) -> u32 {
84
+ self.limit
85
+ }
86
+ }
87
+
88
+ #[cfg(test)]
89
+ mod tests;