rogue_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: 1f287ac34cb1902fa6f38bc699d4ebb9771187ca55bd2ca7677dee75f8da2a67
4
+ data.tar.gz: f4f1ae7a1ed9eb00bc352f7089aac1b6b34b1728e8800680edad99f715dc7b88
5
+ SHA512:
6
+ metadata.gz: f10f9af0ca3c6143e9e46ad0b30387f437fb23195378e8aef054ff7f15892628c1559da5e4a17de7da8097e242ef4e49f3c595351795949841a3fa514a719d21
7
+ data.tar.gz: 3ef750f424079c48a3d531bd0aae538025d12984e60378c1bccd08b5cd93a62175d1f00b21135dc29f2cdfca6069a6119de4feff2d335b5d43acc813d414fac7
data/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## [0.1.1](https://github.com/seuros/rogue_machines/compare/rogue_machines-v0.1.0...rogue_machines/v0.1.1) (2026-11-01)
4
+
5
+
6
+ ### Features
7
+
8
+ * fault injection, disarmed by default, with an expiry it cannot outlive ([e6cfe89](https://github.com/seuros/rogue_machines/commit/e6cfe8999947dbfb048eaf15a13cb2d4dbb03943))
9
+ * pass a rubygems OTP through the environment ([4f60306](https://github.com/seuros/rogue_machines/commit/4f60306bf35aa8fc7336432c0cdd13b61004796b))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * collect the built gems whatever depth the download puts them at ([a470d97](https://github.com/seuros/rogue_machines/commit/a470d97586dad75365425ff5fdde68a982863262))
15
+ * the release tag carries the gem name, and the gate uses the aggregate output ([1c980cd](https://github.com/seuros/rogue_machines/commit/1c980cd0a36bdce2913e1a74252f90987183f0f7))
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 = "rogue-machines"
231
+ version = "0.2.0"
232
+
233
+ [[package]]
234
+ name = "rogue_machines_native"
235
+ version = "0.1.1"
236
+ dependencies = [
237
+ "magnus",
238
+ "rogue-machines",
239
+ ]
240
+
241
+ [[package]]
242
+ name = "rustc-hash"
243
+ version = "2.1.3"
244
+ source = "registry+https://github.com/rust-lang/crates.io-index"
245
+ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
246
+
247
+ [[package]]
248
+ name = "seq-macro"
249
+ version = "0.3.6"
250
+ source = "registry+https://github.com/rust-lang/crates.io-index"
251
+ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
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/rogue_machines_native/core", "ext/rogue_machines_native/ffi"]
3
+ resolver = "3"
4
+
5
+ [workspace.package]
6
+ edition = "2024"
7
+ rust-version = "1.99"
8
+
9
+ [workspace.dependencies]
10
+ rogue-machines = { path = "ext/rogue_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,211 @@
1
+ # RogueMachines 💥
2
+
3
+ > **Controlled sabotage for the fleet.** The ship that breaks your systems on
4
+ > purpose, so the void doesn't get to do it first.
5
+
6
+ Every other vessel in this fleet exists to survive a failure. None of them can
7
+ produce one. A breaker that has never opened, a retry budget that has never
8
+ been spent and a deadline that has never been hit are three untested claims
9
+ flying in formation. **AMS Gray** arms the failure that tests them.
10
+
11
+ ---
12
+
13
+ ## 🚀 Launch Sequence
14
+
15
+ ```bash
16
+ # Add to your ship's Gemfile
17
+ gem 'rogue_machines'
18
+ ```
19
+
20
+ Requires **Ruby 4.0+** and a Rust toolchain, or a platform gem.
21
+
22
+ ---
23
+
24
+ ## 🎯 Mission Brief
25
+
26
+ ```ruby
27
+ require 'rogue_machines'
28
+
29
+ RogueMachines.arm(:payments, fault: :raise, share: 0.01, expires_in: 300)
30
+
31
+ RogueMachines.run(:payments) do
32
+ Payments.charge(order)
33
+ end
34
+ ```
35
+
36
+ One call in a hundred now fails for the next five minutes, and the breaker
37
+ wrapped around it finally gets to prove it works.
38
+
39
+ ---
40
+
41
+ ## 🛡️ Safety Interlocks
42
+
43
+ **Weapons are cold until you arm them.** A process that has armed nothing
44
+ injects nothing, and `run` says so by validating the name and reading one word
45
+ through one native call.
46
+
47
+ ```ruby
48
+ RogueMachines.armed? # => false
49
+ RogueMachines.run(:payments) { Payments.charge(order) } # the real call
50
+ ```
51
+
52
+ It allocates nothing: zero objects per call for a Symbol, a String or a frozen
53
+ String name. The overhead is about 160 ns, most of it the name check rather
54
+ than the native read. That is why this ships as a runtime dependency rather
55
+ than a test-only one. The interlock sits on every call to every protected
56
+ dependency, so it has to be cheap enough to leave in.
57
+
58
+ Nothing is armed by loading the gem, by an environment variable or by a config
59
+ file. Arming is an explicit call naming a target and a share of traffic.
60
+
61
+ ---
62
+
63
+ ## ⏱️ Every Experiment Self-Destructs
64
+
65
+ `expires_in:` is **required**. There is no way to arm an experiment that
66
+ outlives the officer who armed it, and no constructor in the Rust core that
67
+ takes a fault without a reading it expires at.
68
+
69
+ ```ruby
70
+ RogueMachines.arm(:payments, fault: :raise, share: 1.0, expires_in: 60)
71
+ RogueMachines.armed # => [:payments]
72
+
73
+ # sixty seconds later, with nobody touching anything
74
+ RogueMachines.armed # => []
75
+ RogueMachines.armed? # => false
76
+ ```
77
+
78
+ An expiry is **observed, not scheduled**: nothing retires an experiment until
79
+ something takes a reading. The first `run`, `armed` or `armed?` at or after the
80
+ expiry retires it, and once retired a clock that steps backwards cannot bring
81
+ it back.
82
+
83
+ An experiment lasts at most twenty-four hours. Past that it is configuration,
84
+ not an experiment, and it must be armed again deliberately.
85
+
86
+ ---
87
+
88
+ ## 💥 The Armory
89
+
90
+ ```ruby
91
+ # Fail.
92
+ RogueMachines.arm(:payments, fault: :raise, share: 0.01, expires_in: 300)
93
+
94
+ # Be slow, then make the real call.
95
+ RogueMachines.arm(:search, fault: :delay, delay: 2, share: 0.5, expires_in: 300)
96
+
97
+ # Succeed, with a substitute.
98
+ RogueMachines.arm(:catalogue, fault: :garble, garbled: [], share: 0.1, expires_in: 300)
99
+ ```
100
+
101
+ A `:raise` raises `RogueMachines::Injected`. A `:garble` returns `garbled:`
102
+ without running the block, which defaults to `nil` so a forgotten substitute
103
+ cannot quietly return the real value.
104
+
105
+ **There is no weapon that holds a caller forever.** A dependency that times out
106
+ is a `:delay` longer than the caller's own budget, which leaves the caller's
107
+ timeout in charge of when it gives up. The delay is spent against the ambient
108
+ `deadline_machines` budget, so a stall inside a deadline trips that deadline
109
+ instead of sleeping past it. Because a deadline is opt-in per call site, a
110
+ delay is also capped at five minutes. That is not a substitute for a deadline;
111
+ it bounds what a mistyped argument can do to a caller that has none.
112
+
113
+ ---
114
+
115
+ ## 🎲 Targeting
116
+
117
+ `share:` is a fraction of calls between 0 and 1, and the draw deciding each
118
+ call comes from `RogueMachines.random`. Replace it to replay an exact run:
119
+
120
+ ```ruby
121
+ RogueMachines.random = Random.new(1234)
122
+ ```
123
+
124
+ A share is half open at its own boundary and expressed in millionths, so 0.0
125
+ reaches nothing and 1.0 reaches everything. It is a threshold from zero rather
126
+ than an interval: 0.1 and 0.9 both cover a low draw, so two experiments cannot
127
+ divide one stream of calls between them. A share below one millionth is
128
+ refused rather than armed as a silent no-op.
129
+
130
+ ---
131
+
132
+ ## 🕰️ The Ship's Chronometer
133
+
134
+ `RogueMachines.clock` delegates to `DeadlineMachines.clock`, so a test can
135
+ expire an experiment without sleeping:
136
+
137
+ ```ruby
138
+ RogueMachines.clock = DeadlineMachines::TestClock.new
139
+ RogueMachines.arm(:payments, fault: :raise, share: 1.0, expires_in: 60)
140
+ RogueMachines.clock.advance(60)
141
+ RogueMachines.armed? # => false
142
+ ```
143
+
144
+ This is the one place `RogueMachines` consumes a primitive from the fleet
145
+ rather than tripping it.
146
+
147
+ **Install the chronometer through `RogueMachines.clock=`.** Doing so disarms
148
+ everything first, because every armed expiry was computed against the outgoing
149
+ clock and an expiry is only meaningful against the clock it came from.
150
+
151
+ It is safe to do under load. A reading is taken before any lock, so that a
152
+ caller's clock never runs inside one, which means a call can be part way
153
+ through when the clock is replaced. Each reading therefore carries the
154
+ generation it belongs to, and a call arriving with a superseded one is refused
155
+ rather than acted on: arming raises and nothing is armed, a decision makes the
156
+ real call, and a query reports nothing. A clock being replaced faster than a
157
+ reading can be taken is answered the same way rather than retried forever, and
158
+ a clock returning something that is not a non-negative Integer of nanoseconds
159
+ raises an error naming the clock.
160
+
161
+ Replacing `DeadlineMachines.clock` underneath is reflected, since the
162
+ delegation is not memoised, but it cannot disarm anything. A clock whose epoch
163
+ is lower than the old one then makes an armed experiment look as though its
164
+ expiry is still far away. Set `RogueMachines.clock` and the question does not
165
+ arise.
166
+
167
+ A reading is also taken before the lock when arming, so the expiry of a new
168
+ experiment is measured from slightly before it is armed. With a real monotonic
169
+ clock that is microseconds. Nothing infers that a reading came from a
170
+ different clock: with several threads taking their own readings, a reading
171
+ older than the arming is ordinary interleaving, and treating it as evidence of
172
+ a clock change retires experiments that were just armed.
173
+
174
+ ---
175
+
176
+ ## 🔩 The Native Extension Is Required
177
+
178
+ There is no pure-Ruby fallback and no switch to turn one on. `require
179
+ 'rogue_machines'` loads the extension and fails if it is not there. The gem is
180
+ a binding to the `rogue-machines` crate in `ext/rogue_machines_native/core`,
181
+ which has zero dependencies and builds `no_std` for wasm and for a
182
+ microcontroller. The binding asks for its `std` feature, so the crate as this
183
+ gem consumes it is not `no_std`.
184
+
185
+ ---
186
+
187
+ ## 🚫 Rules of Engagement
188
+
189
+ - **No breaker, limiter, retry or budget.** It trips those. `breaker_machines`,
190
+ `throttle_machines`, `chrono_machines` and `shed_machines` own them.
191
+ - **No clock of its own.** Every reading is nanoseconds the caller passes in,
192
+ which is why the core builds for bare metal.
193
+ - **No randomness of its own.** The draw is the caller's.
194
+ - **No capacity beyond 32 experiments**, because the armed set is one 32-bit
195
+ word and the interlock on the hot path is a single load of it.
196
+
197
+ ---
198
+
199
+ ## 📡 Credits
200
+
201
+ **AMS Gray**, NCC-0401, *Gray*-class raider.
202
+
203
+ Named for Jim Gray's *Why Do Computers Stop and What Can Be Done About It?*
204
+ (1985), which argued that the way to find out whether a system tolerates
205
+ faults is to give it some.
206
+
207
+ ---
208
+
209
+ ## License
210
+
211
+ MIT
@@ -0,0 +1,24 @@
1
+ [package]
2
+ name = "rogue-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 = "Fault injection for chaos engineering - armed experiments that make a dependency fail, stall or lie, no_std with zero dependencies"
9
+ repository = "https://github.com/seuros/rogue_machines"
10
+ keywords = ["fault-injection", "chaos-engineering", "resilience", "testing", "no-std"]
11
+ categories = ["no-std", "development-tools::testing"]
12
+ readme = "README.md"
13
+
14
+ [lib]
15
+ name = "rogue_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,78 @@
1
+ //! Which slots are armed, in one word.
2
+
3
+ use core::sync::atomic::{AtomicU32, Ordering};
4
+
5
+ use crate::EXPERIMENTS;
6
+
7
+ /// A slot index a [`Rogue`](crate::Rogue) can hold.
8
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
9
+ pub struct Slot(usize);
10
+
11
+ impl Slot {
12
+ /// Rejects an index at or above [`EXPERIMENTS`].
13
+ #[must_use]
14
+ pub const fn new(index: usize) -> Option<Self> {
15
+ if index < EXPERIMENTS {
16
+ Some(Self(index))
17
+ } else {
18
+ None
19
+ }
20
+ }
21
+
22
+ #[must_use]
23
+ pub const fn index(self) -> usize {
24
+ self.0
25
+ }
26
+
27
+ const fn bit(self) -> u32 {
28
+ 1_u32 << self.0
29
+ }
30
+ }
31
+
32
+ /// One bit per slot, read on every protected call and written only when an
33
+ /// experiment is armed, disarmed or found expired.
34
+ ///
35
+ /// Every operation is `Relaxed`. That is sufficient only because the
36
+ /// experiment table is written behind `&mut self`, so the mask never has to
37
+ /// publish it. Giving `Rogue::arm` interior mutability would break that.
38
+ #[derive(Debug, Default)]
39
+ pub struct Armed(AtomicU32);
40
+
41
+ impl Armed {
42
+ #[must_use]
43
+ pub const fn new() -> Self {
44
+ Self(AtomicU32::new(0))
45
+ }
46
+
47
+ /// Whether any slot is armed.
48
+ #[must_use]
49
+ pub fn any(&self) -> bool {
50
+ self.0.load(Ordering::Relaxed) != 0
51
+ }
52
+
53
+ #[must_use]
54
+ pub fn holds(&self, slot: Slot) -> bool {
55
+ self.0.load(Ordering::Relaxed) & slot.bit() != 0
56
+ }
57
+
58
+ pub fn hold(&self, slot: Slot) {
59
+ self.0.fetch_or(slot.bit(), Ordering::Relaxed);
60
+ }
61
+
62
+ pub fn release(&self, slot: Slot) {
63
+ self.0.fetch_and(!slot.bit(), Ordering::Relaxed);
64
+ }
65
+
66
+ pub fn release_all(&self) {
67
+ self.0.store(0, Ordering::Relaxed);
68
+ }
69
+
70
+ /// How many slots are armed.
71
+ #[must_use]
72
+ pub fn count(&self) -> u32 {
73
+ self.0.load(Ordering::Relaxed).count_ones()
74
+ }
75
+ }
76
+
77
+ #[cfg(test)]
78
+ mod tests;