arbiter_machines 0.1.5

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: 631441a58f585ccf0a154daffdc7791851adbc9705f49eb5a9693a6fc7e11532
4
+ data.tar.gz: 0ca24642601f50c6692a6d6afd22ff1aecbbf73cf4b9111e21c576a2be1920be
5
+ SHA512:
6
+ metadata.gz: d7047d5fcdb4ae35ee82c5202deca7c654d3dcf2d2b5caa4d3fd6429a619cb816e67b836bbed6cf4f65560673c366885f7ecce449c584a185ede1274853403f2
7
+ data.tar.gz: 62d9c925acb317f45209402825cfeb1a646cea095e65ef2c68336cd1745711d2f16f3ef005fa3a66623dbbef2470bc0c62d684716a90f5374c568957ac01d9f9
data/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## [0.1.5](https://github.com/seuros/arbiter_machines/compare/arbiter_machines/v0.1.4...arbiter_machines/v0.1.5) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * share RArray count conversion in arbiter ffi ([0a026aa](https://github.com/seuros/arbiter_machines/commit/0a026aac2e17efecc2546bb8b9532b89f516e7f0))
9
+
10
+ ## [0.1.4](https://github.com/seuros/arbiter_machines/compare/arbiter_machines/v0.1.3...arbiter_machines/v0.1.4) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * crates.io keywords and categories for the core ([10c8cad](https://github.com/seuros/arbiter_machines/commit/10c8cad0f35cb6c44dbe0e5573275f2267afc306))
16
+
17
+ ## [0.1.3](https://github.com/seuros/arbiter_machines/compare/arbiter_machines/v0.1.2...arbiter_machines/v0.1.3) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * keep test sources out of the packaged gem and crate ([513f507](https://github.com/seuros/arbiter_machines/commit/513f5074a2058f23742ee47b2289cd8b8872d6d3))
23
+
24
+ ## [0.1.2](https://github.com/seuros/arbiter_machines/compare/arbiter_machines/v0.1.1...arbiter_machines/v0.1.2) (2026-11-06)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * cross builds no longer compile for the host ([feeb4f0](https://github.com/seuros/arbiter_machines/commit/feeb4f09e5d0b20b81a3fffb35562594b80e2e0a))
30
+
31
+ ## [0.1.1](https://github.com/seuros/arbiter_machines/compare/arbiter_machines-v0.1.0...arbiter_machines/v0.1.1) (2026-11-06)
32
+
33
+
34
+ ### Features
35
+
36
+ * initial extraction ([0ce828b](https://github.com/seuros/arbiter_machines/commit/0ce828bfacd5c9ef7482136b73d7be18339e19b9))
data/Cargo.toml ADDED
@@ -0,0 +1,21 @@
1
+ [workspace]
2
+ members = ["ext/arbiter_machines_native/core", "ext/arbiter_machines_native/ffi"]
3
+ default-members = ["ext/arbiter_machines_native/core"]
4
+ resolver = "3"
5
+ [workspace.dependencies]
6
+ arbiter-machines = { path = "ext/arbiter_machines_native/core" }
7
+ magnus = { version = "0.9.3", default-features = false }
8
+ rb-sys = { version = "0.9.130", default-features = false, features = ["stable-api-compiled-fallback"] }
9
+ [workspace.package]
10
+ edition = "2024"
11
+ rust-version = "1.99"
12
+ license = "MIT"
13
+ [workspace.lints.rust]
14
+ unsafe_code = "deny"
15
+ unused_must_use = "warn"
16
+ [workspace.lints.clippy]
17
+ all = { level = "warn", priority = -1 }
18
+ pedantic = { level = "warn", priority = -1 }
19
+ nursery = { level = "warn", priority = -1 }
20
+ unwrap_used = "deny"
21
+ expect_used = "deny"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
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 all
13
+ 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 THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # ArbiterMachines
2
+
3
+ > When the slow job holds the key the urgent job needs, more workers won't help. 🛰️
4
+
5
+ A priority arbiter for Ruby. You declare your tasks with their priorities and your resources with their ceilings. The `Arbiter` tells you who may take what, who has to wait, who just inherited someone else's urgency, and who is about to deadlock. Your code still takes and releases the actual locks. The arbiter only ever answers one question: is this a good idea right now?
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'arbiter_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ # Two tasks: a janitor at priority 1, a payment at priority 10.
15
+ # One resource: the ledger lock, ceiling 10.
16
+ arbiter = ArbiterMachines::Arbiter.new(
17
+ priorities: [1, 10],
18
+ ceilings: [10],
19
+ protocol: :inheritance
20
+ )
21
+
22
+ arbiter.acquire(task: 0, resource: 0) # => [:granted, nil] janitor takes the ledger
23
+ arbiter.acquire(task: 1, resource: 0) # => [:blocked, 0] payment waits on resource 0
24
+ arbiter.effective_priorities # => [10, 10] janitor now runs as urgent
25
+
26
+ arbiter.release(task: 0, resource: 0) # => true janitor done, wait cleared
27
+ arbiter.acquire(task: 1, resource: 0) # => [:granted, nil] payment retries and gets in
28
+ ```
29
+
30
+ Tasks and resources are positional indices into the arrays you passed in. Larger number means more urgent.
31
+
32
+ ## A Message to the Resistance
33
+
34
+ So you gave the nightly cleanup job priority 1, the payment processor priority 10, and a pile of report builders priority 5. Sensible. Then one night the cleanup job took the ledger lock for a quick vacuum, the payment job arrived and blocked on it, and twelve report builders showed up and ran. All of them. Before the cleanup job got another slice of CPU.
35
+
36
+ The payment job, your most urgent work, sat behind priority-5 reports because a priority-1 job was holding its lock and nobody told the scheduler that the priority-1 job was suddenly very important.
37
+
38
+ That's priority inversion. It put a rover on Mars into a reboot loop in 1997, and it's been quietly doing the same thing to job queues ever since. The fix has been known just as long: while the janitor holds something the payment job needs, the janitor *is* the payment job. It inherits the urgency, finishes fast, hands the lock over.
39
+
40
+ This gem does the accounting for that. Who holds what, who waits on whom, what everybody's priority really is right now, and whether the next acquire closes a loop nobody can escape from.
41
+
42
+ ## Chapter 1: Stardate 2025.186, the AMS Dijkstra
43
+
44
+ > **AMS Dijkstra**, NCC-4201, *Dijkstra*-class traffic-control vessel.
45
+ > One airlock. Two waiting crews. No circular holding pattern.
46
+
47
+ The vessel doesn't open airlocks. It keeps the chart: which crew is in which lock, which crew is waiting at which door, and which crew's urgency has been lent to the crew blocking it. Before any crew is cleared to wait, the chart is checked for a loop. A loop is never charted, because a charted loop is a ship nobody leaves.
48
+
49
+ Somewhere on the bridge, a junior officer suggests:
50
+
51
+ ```ruby
52
+ def process_payment
53
+ ledger_lock.synchronize do # the janitor has this, and the janitor is slow
54
+ charge_card
55
+ end
56
+ end
57
+ ```
58
+
59
+ "That," says the traffic controller, "is how urgent work waits for the floor to be mopped."
60
+
61
+ ## The Flight Plan
62
+
63
+ Every resource is either free or held. Every task is either running, holding, or waiting at exactly one door.
64
+
65
+ ```mermaid
66
+ stateDiagram-v2
67
+ [*] --> free
68
+ free --> held: acquire, granted
69
+ held --> held: holder acquires again, already_owned
70
+ held --> held: other task acquires, blocked (holder inherits urgency)
71
+ held --> free: holder releases, waiters cleared to retry
72
+ held --> held: acquire would close a wait loop, deadlock (nothing changes)
73
+ free --> [*]
74
+ ```
75
+
76
+ And the night the janitor held the ledger:
77
+
78
+ ```mermaid
79
+ sequenceDiagram
80
+ participant J as Janitor (task 0, prio 1)
81
+ participant A as Arbiter
82
+ participant P as Payment (task 1, prio 10)
83
+
84
+ J->>A: acquire(task 0, resource 0)
85
+ A-->>J: granted
86
+ Note over J: starts vacuuming the ledger
87
+
88
+ P->>A: acquire(task 1, resource 0)
89
+ A-->>P: blocked on resource 0
90
+ Note over A: effective priorities now [10, 10]
91
+ Note over J: runs as priority 10, no report builder gets ahead
92
+
93
+ J->>A: release(task 0, resource 0)
94
+ A-->>J: true
95
+ Note over A: effective priorities back to [1, 10], wait cleared
96
+
97
+ P->>A: acquire(task 1, resource 0)
98
+ A-->>P: granted
99
+ ```
100
+
101
+ ## How Arbitration Works
102
+
103
+ ### Acquire
104
+
105
+ `acquire` returns a two-element array: a status and, when blocked, the resource index you're waiting on.
106
+
107
+ ```ruby
108
+ arbiter = ArbiterMachines::Arbiter.new(
109
+ priorities: [1, 5, 10], ceilings: [10, 10], protocol: :inheritance
110
+ )
111
+
112
+ arbiter.acquire(task: 0, resource: 0) # => [:granted, nil]
113
+ arbiter.acquire(task: 0, resource: 0) # => [:already_owned, nil]
114
+ arbiter.acquire(task: 1, resource: 1) # => [:granted, nil]
115
+ arbiter.acquire(task: 1, resource: 0) # => [:blocked, 0]
116
+ ```
117
+
118
+ Task 1 now holds resource 1 and is waiting on resource 0, which task 0 holds.
119
+
120
+ ### Inheritance is transitive
121
+
122
+ Urgency flows up the chain. If a third task waits on task 1, task 1 gets its priority, and so does task 0 behind it.
123
+
124
+ ```ruby
125
+ arbiter.acquire(task: 2, resource: 1) # => [:blocked, 1]
126
+ arbiter.effective_priorities # => [10, 10, 10]
127
+ ```
128
+
129
+ Task 2, priority 10, waits on task 1. Task 1 waits on task 0. Everybody in the chain runs at 10 until the chain clears.
130
+
131
+ ### Release and retry
132
+
133
+ Releasing clears the waits on that resource. It does not hand the resource to anyone. The waiters call `acquire` again, and the arbiter decides fresh.
134
+
135
+ ```ruby
136
+ arbiter.release(task: 0, resource: 0) # => true
137
+ arbiter.effective_priorities # => [1, 10, 10] task 1 still blocks task 2
138
+ arbiter.acquire(task: 1, resource: 0) # => [:granted, nil]
139
+ ```
140
+
141
+ Release by a task that doesn't hold the resource returns `false` and changes nothing.
142
+
143
+ ### Stop waiting
144
+
145
+ A task that gives up on a door calls `cancel_wait`. Its held resources stay held. Only the wait is dropped, and the urgency it was lending goes with it.
146
+
147
+ ```ruby
148
+ arbiter = ArbiterMachines::Arbiter.new(
149
+ priorities: [1, 5, 10], ceilings: [10, 10], protocol: :inheritance
150
+ )
151
+ arbiter.acquire(task: 0, resource: 0)
152
+ arbiter.acquire(task: 2, resource: 0) # => [:blocked, 0]
153
+ arbiter.effective_priorities # => [10, 5, 10]
154
+
155
+ arbiter.cancel_wait(task: 2) # => true
156
+ arbiter.effective_priorities # => [1, 5, 10]
157
+ ```
158
+
159
+ ### Deadlock is refused, not reported after the fact
160
+
161
+ Two tasks each holding what the other wants is a loop. The arbiter sees it before charting the second wait and returns `:deadlock`. Nothing about the current state changes, so you can back out cleanly.
162
+
163
+ ```ruby
164
+ arbiter = ArbiterMachines::Arbiter.new(
165
+ priorities: [1, 5, 10], ceilings: [10, 10], protocol: :inheritance
166
+ )
167
+ arbiter.acquire(task: 0, resource: 0)
168
+ arbiter.acquire(task: 1, resource: 1)
169
+ arbiter.acquire(task: 0, resource: 1) # => [:blocked, 1]
170
+ arbiter.acquire(task: 1, resource: 0) # => [:deadlock, nil]
171
+ arbiter.effective_priorities # => [1, 5, 10] untouched
172
+
173
+ arbiter.cancel_wait(task: 0) # => true, task 0 backs off
174
+ arbiter.acquire(task: 1, resource: 0) # => [:blocked, 0] now it's a plain wait
175
+ ```
176
+
177
+ ### The other protocol: immediate ceiling
178
+
179
+ With `:immediate_ceiling`, a task jumps to the resource's ceiling the moment it takes the resource, before anyone has to wait. Nobody with a priority at or below the ceiling can get in behind it.
180
+
181
+ ```ruby
182
+ arbiter = ArbiterMachines::Arbiter.new(
183
+ priorities: [1, 5, 10], ceilings: [8, 10], protocol: :immediate_ceiling
184
+ )
185
+ arbiter.acquire(task: 0, resource: 0) # => [:granted, nil]
186
+ arbiter.effective_priorities # => [8, 5, 10] task 0 raised to the ceiling
187
+ arbiter.acquire(task: 1, resource: 1) # => [:blocked, 0] priority 5 can't clear a ceiling of 8
188
+ arbiter.acquire(task: 2, resource: 1) # => [:granted, nil] priority 10 can
189
+ ```
190
+
191
+ A ceiling has to be at least as high as every task that will ever take the resource. Task 2 at priority 10 asking for resource 0 with ceiling 8 is a misdeclaration, and the arbiter says so:
192
+
193
+ ```ruby
194
+ arbiter.acquire(task: 2, resource: 0) # => [:invalid, nil]
195
+ ```
196
+
197
+ ## The Rules of the Vessel
198
+
199
+ - **Up to 8 tasks and 8 resources**, chosen independently by the lengths of `priorities:` and `ceilings:`. Empty or longer arrays raise `ArgumentError`.
200
+ - **Priorities, ceilings, and indices are non-negative Integers** below `2**32`. Floats and strings raise `TypeError`, out-of-range raises `RangeError`.
201
+ - **Out-of-range task or resource indices** in `acquire` return `[:invalid, nil]`. In `cancel_wait` they return `false`. Nothing raises for a bad index, only for a bad type.
202
+ - **`effective_priorities` is a fresh Array** every call. Mutate it all you like, the arbiter doesn't care.
203
+ - **Frozen means frozen.** `freeze` the arbiter and `acquire`, `release`, and `cancel_wait` raise `FrozenError`. Reads still work.
204
+ - **Ceilings are always required**, even under `:inheritance`, where they're not consulted.
205
+
206
+ ## What You Think You're Doing vs Reality
207
+
208
+ ### You Think: "Priority 10 means it runs first."
209
+ ### Reality: Priority 10 means it waits first, behind whoever holds its lock.
210
+
211
+ A priority is a promise about scheduling, not about locks. The moment a high-priority task blocks on a lock held by a low-priority one, every medium task in the system is effectively more urgent than your most urgent work. Inheritance is what makes the number mean something again.
212
+
213
+ ## What's Yours
214
+
215
+ The arbiter never touches a mutex, a thread, or a scheduler. When it says `:granted`, you take the lock. When it says `:blocked`, you wait however you wait, then ask again after the holder releases. When it reports an effective priority, you're the one who has to tell your scheduler. Keep one arbiter per group of resources, and keep its calls serialized.
216
+
217
+ ## License
218
+
219
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
220
+
221
+ **Remember: Nobody can hear the payment job scream while the janitor mops at priority 1.**
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [0.2.2](https://github.com/seuros/arbiter_machines/compare/arbiter-machines-v0.2.1...arbiter-machines-v0.2.2) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([10c8cad](https://github.com/seuros/arbiter_machines/commit/10c8cad0f35cb6c44dbe0e5573275f2267afc306))
9
+
10
+ ## [0.2.1](https://github.com/seuros/arbiter_machines/compare/arbiter-machines-v0.2.0...arbiter-machines-v0.2.1) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([513f507](https://github.com/seuros/arbiter_machines/commit/513f5074a2058f23742ee47b2289cd8b8872d6d3))
16
+
17
+ ## [0.2.0](https://github.com/seuros/arbiter_machines/compare/arbiter-machines-v0.1.0...arbiter-machines-v0.2.0) (2026-11-06)
18
+
19
+
20
+ ### Features
21
+
22
+ * initial extraction ([0ce828b](https://github.com/seuros/arbiter_machines/commit/0ce828bfacd5c9ef7482136b73d7be18339e19b9))
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "arbiter-machines"
3
+ version = "0.2.2"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ license.workspace = true
7
+ description = "Bounded ownership, priority inheritance, ceilings, and wait-cycle detection"
8
+ readme = "README.md"
9
+ exclude = ["src/**/tests.rs", "tests/"]
10
+ keywords = ["priority-inheritance", "priority-ceiling", "deadlock", "arbitration", "no-std"]
11
+ categories = ["no-std", "algorithms", "concurrency"]
12
+ [lints]
13
+ workspace = true
@@ -0,0 +1,20 @@
1
+ # arbiter-machines
2
+
3
+ > **AMS Dijkstra**, NCC-4201, *Dijkstra*-class traffic-control vessel.
4
+
5
+ Engine-room flight manual: coordinate airlock ownership without a circular wait.
6
+
7
+ Allocation-free `no_std` ownership and wait-for decisions for fixed tasks/resources.
8
+ Larger numeric priority means higher priority. One outstanding wait per task;
9
+ nonrecursive ownership. Releases wake affected waiters to retry but never silently
10
+ transfer ownership.
11
+
12
+ Inheritance propagates priorities transitively to owners. Immediate-ceiling mode
13
+ raises owners to held-resource ceilings and blocks acquisition unless effective
14
+ priority exceeds every ceiling held by another task. Each configured resource
15
+ ceiling must bound all tasks permitted to use it; requests exceeding it reject.
16
+ The core detects prospective wait-for cycles before inserting a wait.
17
+
18
+ These decisions require an executor honoring priorities and ownership. No OS
19
+ priority manipulation, lock implementation, waiter-queue fairness, preemption
20
+ driver, multi-resource atomic acquisition, or Ruby scheduling guarantee.
@@ -0,0 +1,150 @@
1
+ #![no_std]
2
+ #![doc = include_str!("../README.md")]
3
+
4
+ /// Resource priority protocol.
5
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
6
+ #[non_exhaustive]
7
+ pub enum Protocol {
8
+ /// Transitive inheritance from blocked waiters.
9
+ Inheritance,
10
+ /// Immediate ceilings plus system-ceiling admission.
11
+ ImmediateCeiling,
12
+ }
13
+ /// Acquisition decision; no underlying lock is manipulated.
14
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
15
+ #[non_exhaustive]
16
+ pub enum Acquisition {
17
+ /// Caller may acquire the resource.
18
+ Granted,
19
+ /// Caller owns it; recursion depth is not tracked.
20
+ AlreadyOwned,
21
+ /// Caller must wait on this blocking resource index.
22
+ Blocked(usize),
23
+ /// This wait would create a cycle; old state is retained.
24
+ Deadlock,
25
+ /// Invalid index or declared ceiling.
26
+ Invalid,
27
+ }
28
+ /// Fixed task/resource ownership and wait-for graph.
29
+ #[derive(Debug, Clone, PartialEq, Eq)]
30
+ pub struct Arbiter<const T: usize, const R: usize> {
31
+ base: [u32; T],
32
+ ceilings: [u32; R],
33
+ protocol: Protocol,
34
+ owners: [Option<usize>; R],
35
+ waiting: [Option<usize>; T],
36
+ }
37
+ impl<const T: usize, const R: usize> Arbiter<T, R> {
38
+ /// Configure base priorities and resource ceilings.
39
+ #[must_use]
40
+ pub const fn new(base: [u32; T], ceilings: [u32; R], protocol: Protocol) -> Self {
41
+ Self {
42
+ base,
43
+ ceilings,
44
+ protocol,
45
+ owners: [None; R],
46
+ waiting: [None; T],
47
+ }
48
+ }
49
+ /// Acquire or record one wait; the caller enforces the decision.
50
+ #[must_use]
51
+ pub fn acquire(&mut self, task: usize, resource: usize) -> Acquisition {
52
+ if task >= T || resource >= R {
53
+ return Acquisition::Invalid;
54
+ }
55
+ if self.protocol == Protocol::ImmediateCeiling && self.base[task] > self.ceilings[resource]
56
+ {
57
+ return Acquisition::Invalid;
58
+ }
59
+ if self.owners[resource] == Some(task) {
60
+ return Acquisition::AlreadyOwned;
61
+ }
62
+ let blocker = if self.owners[resource].is_some() {
63
+ Some(resource)
64
+ } else if self.protocol == Protocol::ImmediateCeiling {
65
+ let effective = self.effective_priorities()[task];
66
+ self.owners
67
+ .iter()
68
+ .enumerate()
69
+ .find(|&(index, owner)| {
70
+ owner.is_some_and(|owner| owner != task) && effective <= self.ceilings[index]
71
+ })
72
+ .map(|(index, _)| index)
73
+ } else {
74
+ None
75
+ };
76
+ let Some(blocker) = blocker else {
77
+ self.owners[resource] = Some(task);
78
+ self.waiting[task] = None;
79
+ return Acquisition::Granted;
80
+ };
81
+ let mut holder = self.owners[blocker];
82
+ for _ in 0..T {
83
+ let Some(owner) = holder else {
84
+ break;
85
+ };
86
+ if owner == task {
87
+ return Acquisition::Deadlock;
88
+ }
89
+ holder = self.waiting[owner].and_then(|resource| self.owners[resource]);
90
+ }
91
+ self.waiting[task] = Some(blocker);
92
+ Acquisition::Blocked(blocker)
93
+ }
94
+ /// Release owned state and clear affected waits for explicit retries.
95
+ #[must_use]
96
+ pub fn release(&mut self, task: usize, resource: usize) -> bool {
97
+ let Some(owner) = self.owners.get_mut(resource) else {
98
+ return false;
99
+ };
100
+ if *owner != Some(task) {
101
+ return false;
102
+ }
103
+ *owner = None;
104
+ for wait in &mut self.waiting {
105
+ if *wait == Some(resource) {
106
+ *wait = None;
107
+ }
108
+ }
109
+ true
110
+ }
111
+ /// Cancel a wait without releasing owned resources.
112
+ #[must_use]
113
+ pub fn cancel_wait(&mut self, task: usize) -> bool {
114
+ let Some(wait) = self.waiting.get_mut(task) else {
115
+ return false;
116
+ };
117
+ *wait = None;
118
+ true
119
+ }
120
+ /// Effective priorities for the runtime executor.
121
+ #[must_use]
122
+ pub fn effective_priorities(&self) -> [u32; T] {
123
+ let mut effective = self.base;
124
+ if self.protocol == Protocol::ImmediateCeiling {
125
+ for (resource, owner) in self.owners.iter().enumerate() {
126
+ if let Some(owner) = owner {
127
+ effective[*owner] = effective[*owner].max(self.ceilings[resource]);
128
+ }
129
+ }
130
+ } else {
131
+ for _ in 0..T {
132
+ let mut changed = false;
133
+ for (task, resource) in self.waiting.iter().enumerate() {
134
+ if let Some(owner) = resource.and_then(|resource| self.owners[resource])
135
+ && effective[owner] < effective[task]
136
+ {
137
+ effective[owner] = effective[task];
138
+ changed = true;
139
+ }
140
+ }
141
+ if !changed {
142
+ break;
143
+ }
144
+ }
145
+ }
146
+ effective
147
+ }
148
+ }
149
+ #[cfg(test)]
150
+ mod tests;
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "arbiter_machines_native"
3
+ version = "0.1.3"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ license.workspace = true
7
+ description = "Native Ruby bindings for bounded priority arbitration"
8
+ publish = false
9
+
10
+ [lib]
11
+ crate-type = ["cdylib"]
12
+ test = false
13
+ doctest = false
14
+
15
+ [dependencies]
16
+ arbiter-machines.workspace = true
17
+ magnus.workspace = true
18
+ rb-sys.workspace = true
19
+
20
+ [lints]
21
+ workspace = true
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+ require 'pathname'
5
+ require 'rb_sys/mkmf'
6
+
7
+ create_rust_makefile('arbiter_machines_native/arbiter_machines_native') do |extension|
8
+ extension.ext_dir = Pathname(__dir__).relative_path_from(Pathname(Dir.pwd)).to_s
9
+ extension.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
10
+ end
11
+
12
+ makefile = File.join(Dir.pwd, 'Makefile')
13
+ File.write(makefile, File.read(makefile).gsub(
14
+ /^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/,
15
+ "RB_SYS_CARGO_MANIFEST_DIR ?= #{File.expand_path(__dir__)}"
16
+ ))
@@ -0,0 +1,182 @@
1
+ //! Erase exact task/resource const-generic counts without adding participants.
2
+
3
+ use crate::Count;
4
+ use arbiter_machines::{Acquisition, Arbiter, Protocol};
5
+ use magnus::{
6
+ Error, RArray, RHash, RModule, Ruby, Symbol, Value, function, method,
7
+ prelude::*,
8
+ scan_args::{get_kwargs, scan_args},
9
+ typed_data::Obj,
10
+ };
11
+ use std::cell::RefCell;
12
+
13
+ trait Decisions: Send {
14
+ fn acquire(&mut self, task: usize, resource: usize) -> Acquisition;
15
+ fn release(&mut self, task: usize, resource: usize) -> bool;
16
+ fn cancel_wait(&mut self, task: usize) -> bool;
17
+ fn effective_priorities(&self) -> Vec<u32>;
18
+ }
19
+
20
+ impl<const T: usize, const R: usize> Decisions for Arbiter<T, R> {
21
+ fn acquire(&mut self, task: usize, resource: usize) -> Acquisition {
22
+ self.acquire(task, resource)
23
+ }
24
+ fn release(&mut self, task: usize, resource: usize) -> bool {
25
+ self.release(task, resource)
26
+ }
27
+ fn cancel_wait(&mut self, task: usize) -> bool {
28
+ self.cancel_wait(task)
29
+ }
30
+ fn effective_priorities(&self) -> Vec<u32> {
31
+ self.effective_priorities().to_vec()
32
+ }
33
+ }
34
+
35
+ fn resources<const T: usize>(
36
+ ruby: &Ruby,
37
+ base: [u32; T],
38
+ ceilings: &[u32],
39
+ protocol: Protocol,
40
+ ) -> Result<Box<dyn Decisions>, Error> {
41
+ macro_rules! sized {
42
+ ($($n:literal),*) => {
43
+ match ceilings.len() {
44
+ $($n => Ok(Box::new(Arbiter::<T, $n>::new(
45
+ base,
46
+ std::array::from_fn(|i| ceilings[i]),
47
+ protocol,
48
+ )) as Box<dyn Decisions>),)*
49
+ _ => Err(Error::new(ruby.exception_arg_error(), "resource count must be 1..8")),
50
+ }
51
+ };
52
+ }
53
+ sized!(1, 2, 3, 4, 5, 6, 7, 8)
54
+ }
55
+
56
+ /// Every element of `array` as a non-negative `Integer` count.
57
+ fn counts(array: RArray) -> Result<Vec<u32>, Error> {
58
+ array
59
+ .into_iter()
60
+ .map(|v| Count::try_convert(v).map(|v| v.0))
61
+ .collect()
62
+ }
63
+
64
+ #[magnus::wrap(class = "ArbiterMachines::Arbiter", free_immediately, size)]
65
+ struct Wrapper {
66
+ core: RefCell<Box<dyn Decisions>>,
67
+ tasks: usize,
68
+ resources: usize,
69
+ }
70
+
71
+ impl Wrapper {
72
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
73
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
74
+ let (priorities, ceilings, protocol) = get_kwargs::<_, (RArray, RArray, Symbol), (), ()>(
75
+ args.keywords,
76
+ &["priorities", "ceilings", "protocol"],
77
+ &[],
78
+ )?
79
+ .required;
80
+ let protocol = match protocol.name()?.as_ref() {
81
+ "inheritance" => Protocol::Inheritance,
82
+ "immediate_ceiling" => Protocol::ImmediateCeiling,
83
+ _ => {
84
+ return Err(Error::new(
85
+ ruby.exception_arg_error(),
86
+ "protocol must be :inheritance or :immediate_ceiling",
87
+ ));
88
+ }
89
+ };
90
+ let base = counts(priorities)?;
91
+ let ceilings = counts(ceilings)?;
92
+ macro_rules! sized {
93
+ ($($n:literal),*) => {
94
+ match base.len() {
95
+ $($n => resources::<$n>(
96
+ ruby,
97
+ std::array::from_fn(|i| base[i]),
98
+ &ceilings,
99
+ protocol,
100
+ )?,)*
101
+ _ => {
102
+ let message = "task count must be 1..8";
103
+ return Err(Error::new(ruby.exception_arg_error(), message));
104
+ }
105
+ }
106
+ };
107
+ }
108
+ let core = sized!(1, 2, 3, 4, 5, 6, 7, 8);
109
+ Ok(Self {
110
+ core: RefCell::new(core),
111
+ tasks: base.len(),
112
+ resources: ceilings.len(),
113
+ })
114
+ }
115
+
116
+ fn indices(args: &[Value]) -> Result<(usize, usize), Error> {
117
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
118
+ let (task, resource) =
119
+ get_kwargs::<_, (Count, Count), (), ()>(args.keywords, &["task", "resource"], &[])?
120
+ .required;
121
+ Ok((task.0 as usize, resource.0 as usize))
122
+ }
123
+
124
+ fn acquire(
125
+ ruby: &Ruby,
126
+ rb_self: Obj<Self>,
127
+ args: &[Value],
128
+ ) -> Result<(Symbol, Option<usize>), Error> {
129
+ rb_self.check_frozen()?;
130
+ let (task, resource) = Self::indices(args)?;
131
+ let decision = rb_self.core.borrow_mut().acquire(task, resource);
132
+ let (status, blocker) = match decision {
133
+ Acquisition::Granted => ("granted", None),
134
+ Acquisition::AlreadyOwned => ("already_owned", None),
135
+ Acquisition::Blocked(index) => ("blocked", Some(index)),
136
+ Acquisition::Deadlock => ("deadlock", None),
137
+ _ => ("invalid", None),
138
+ };
139
+ // Ruby allocation only after the RefCell borrow has ended.
140
+ Ok((ruby.to_symbol(status), blocker))
141
+ }
142
+
143
+ fn release(rb_self: Obj<Self>, args: &[Value]) -> Result<bool, Error> {
144
+ rb_self.check_frozen()?;
145
+ let (task, resource) = Self::indices(args)?;
146
+ let released = rb_self.core.borrow_mut().release(task, resource);
147
+ Ok(released)
148
+ }
149
+
150
+ fn cancel_wait(rb_self: Obj<Self>, args: &[Value]) -> Result<bool, Error> {
151
+ rb_self.check_frozen()?;
152
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
153
+ let (task,) = get_kwargs::<_, (Count,), (), ()>(args.keywords, &["task"], &[])?.required;
154
+ let canceled = rb_self.core.borrow_mut().cancel_wait(task.0 as usize);
155
+ Ok(canceled)
156
+ }
157
+
158
+ fn effective_priorities(&self) -> Vec<u32> {
159
+ self.core.borrow().effective_priorities()
160
+ }
161
+
162
+ const fn task_count(&self) -> usize {
163
+ self.tasks
164
+ }
165
+ const fn resource_count(&self) -> usize {
166
+ self.resources
167
+ }
168
+ }
169
+
170
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
171
+ let class = module.define_class("Arbiter", ruby.class_object())?;
172
+ class.define_singleton_method("new", function!(Wrapper::new, -1))?;
173
+ class.define_method("acquire", method!(Wrapper::acquire, -1))?;
174
+ class.define_method("release", method!(Wrapper::release, -1))?;
175
+ class.define_method("cancel_wait", method!(Wrapper::cancel_wait, -1))?;
176
+ class.define_method(
177
+ "effective_priorities",
178
+ method!(Wrapper::effective_priorities, 0),
179
+ )?;
180
+ class.define_method("task_count", method!(Wrapper::task_count, 0))?;
181
+ class.define_method("resource_count", method!(Wrapper::resource_count, 0))
182
+ }
@@ -0,0 +1,6 @@
1
+ use magnus::{Error, Ruby};
2
+
3
+ #[magnus::init]
4
+ fn init(ruby: &Ruby) -> Result<(), Error> {
5
+ crate::arbiter::define(ruby, ruby.define_module("ArbiterMachines")?)
6
+ }
@@ -0,0 +1,21 @@
1
+ //! Thin host adapters for priority inheritance and immediate ceiling decisions.
2
+
3
+ mod arbiter;
4
+ mod init;
5
+
6
+ use magnus::{Error, Integer, Ruby, TryConvert, Value};
7
+
8
+ #[derive(Clone, Copy)]
9
+ pub(crate) struct Count(pub(crate) u32);
10
+
11
+ impl TryConvert for Count {
12
+ fn try_convert(value: Value) -> Result<Self, Error> {
13
+ if Integer::from_value(value).is_none() {
14
+ return Err(Error::new(
15
+ Ruby::get_with(value).exception_type_error(),
16
+ "expected an Integer",
17
+ ));
18
+ }
19
+ u32::try_convert(value).map(Self)
20
+ }
21
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ArbiterMachines
4
+ VERSION = '0.1.5'
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'arbiter_machines/version'
4
+ require 'arbiter_machines_native/arbiter_machines_native'
@@ -0,0 +1,12 @@
1
+ module ArbiterMachines
2
+ VERSION: String
3
+ class Arbiter
4
+ def self.new: (priorities: Array[Integer], ceilings: Array[Integer], protocol: :inheritance | :immediate_ceiling) -> instance
5
+ def acquire: (task: Integer, resource: Integer) -> [Symbol, Integer?]
6
+ def release: (task: Integer, resource: Integer) -> bool
7
+ def cancel_wait: (task: Integer) -> bool
8
+ def effective_priorities: () -> Array[Integer]
9
+ def task_count: () -> Integer
10
+ def resource_count: () -> Integer
11
+ end
12
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: arbiter_machines
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Abdelkader Boudih
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.9'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 0.9.130
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '0.9'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 0.9.130
32
+ description: Thin Ruby adapter to the standalone allocation-free arbiter-machines
33
+ Rust core. Initial unstable local implementation; caller enforces all decisions.
34
+ email:
35
+ - oss@seuros.com
36
+ executables: []
37
+ extensions:
38
+ - ext/arbiter_machines_native/extconf.rb
39
+ extra_rdoc_files: []
40
+ files:
41
+ - CHANGELOG.md
42
+ - Cargo.toml
43
+ - LICENSE.txt
44
+ - README.md
45
+ - ext/arbiter_machines_native/core/CHANGELOG.md
46
+ - ext/arbiter_machines_native/core/Cargo.toml
47
+ - ext/arbiter_machines_native/core/README.md
48
+ - ext/arbiter_machines_native/core/src/lib.rs
49
+ - ext/arbiter_machines_native/extconf.rb
50
+ - ext/arbiter_machines_native/ffi/Cargo.toml
51
+ - ext/arbiter_machines_native/ffi/extconf.rb
52
+ - ext/arbiter_machines_native/ffi/src/arbiter.rs
53
+ - ext/arbiter_machines_native/ffi/src/init.rs
54
+ - ext/arbiter_machines_native/ffi/src/lib.rs
55
+ - lib/arbiter_machines.rb
56
+ - lib/arbiter_machines/version.rb
57
+ - sig/arbiter_machines.rbs
58
+ homepage: https://github.com/seuros/arbiter_machines
59
+ licenses:
60
+ - MIT
61
+ metadata:
62
+ source_code_uri: https://github.com/seuros/arbiter_machines
63
+ bug_tracker_uri: https://github.com/seuros/arbiter_machines/issues
64
+ rubygems_mfa_required: 'true'
65
+ cargo_crate_name: arbiter_machines_native
66
+ cargo_manifest_path: ext/arbiter_machines_native/ffi/Cargo.toml
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 4.0.0
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 4.0.20
82
+ specification_version: 4
83
+ summary: Native bounded priority inheritance and resource ceiling decisions
84
+ test_files: []