priority_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: 336b8504ae6121373ba3e2b2b4e9b3750c3479796a6c06cd3da16c74daee171b
4
+ data.tar.gz: 0c5d3ec5bf92de2252dff7494e4adffd3395f507bab800845211e27377792104
5
+ SHA512:
6
+ metadata.gz: 1eac9da160ac88331dc341f9731b706be2a4e991407d32dea9f7ab6943fdcac70b366feeed1cfc199fb7687f670e1a289ef5131a9001a183cbb27a8a0d63052d
7
+ data.tar.gz: d1ecc552bf3b4b08f985e2e24ab66e9b622d5092420857067cadc93cd8fe107c90e3d34cdb6ecbb77beb12908beaae8e625658df9ddcfc06591206d738a4c3ae
data/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## [0.1.5](https://github.com/seuros/priority_machines/compare/priority_machines/v0.1.4...priority_machines/v0.1.5) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([1d389af](https://github.com/seuros/priority_machines/commit/1d389af51eacb70fe595028c381c68cb9d556f51))
9
+
10
+ ## [0.1.4](https://github.com/seuros/priority_machines/compare/priority_machines/v0.1.3...priority_machines/v0.1.4) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([37e58c0](https://github.com/seuros/priority_machines/commit/37e58c0575a088807db99d38ea820d87dde956b8))
16
+
17
+ ## [0.1.3](https://github.com/seuros/priority_machines/compare/priority_machines/v0.1.2...priority_machines/v0.1.3) (2026-11-06)
18
+
19
+
20
+ ### Features
21
+
22
+ * plain round robin scheduler in the core ([315e567](https://github.com/seuros/priority_machines/commit/315e5677ab93d677c71847233abcaa2bad5a7f4e))
23
+
24
+ ## [0.1.2](https://github.com/seuros/priority_machines/compare/priority_machines/v0.1.1...priority_machines/v0.1.2) (2026-11-06)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * cross builds no longer compile for the host ([dcf3aac](https://github.com/seuros/priority_machines/commit/dcf3aaccd145677feb6f61ea2fbd3d7e7f3e120a))
30
+
31
+ ## [0.1.1](https://github.com/seuros/priority_machines/compare/priority_machines-v0.1.0...priority_machines/v0.1.1) (2026-11-06)
32
+
33
+
34
+ ### Features
35
+
36
+ * initial extraction ([ea040c3](https://github.com/seuros/priority_machines/commit/ea040c3e486d5b77a9eb4c68a8ac493399df8323))
data/Cargo.toml ADDED
@@ -0,0 +1,26 @@
1
+ [workspace]
2
+ members = ["ext/priority_machines_native/core", "ext/priority_machines_native/ffi"]
3
+ default-members = ["ext/priority_machines_native/core"]
4
+ resolver = "3"
5
+
6
+ [workspace.dependencies]
7
+ priority-machines = { path = "ext/priority_machines_native/core" }
8
+ magnus = { version = "0.9.3", default-features = false }
9
+ rb-sys = { version = "0.9.130", default-features = false, features = ["stable-api-compiled-fallback"] }
10
+
11
+ [workspace.package]
12
+ edition = "2024"
13
+ rust-version = "1.99"
14
+ license = "MIT"
15
+
16
+ [workspace.lints.rust]
17
+ unsafe_code = "deny"
18
+ unused_must_use = "warn"
19
+ missing_docs = "warn"
20
+
21
+ [workspace.lints.clippy]
22
+ all = { level = "warn", priority = -1 }
23
+ pedantic = { level = "warn", priority = -1 }
24
+ nursery = { level = "warn", priority = -1 }
25
+ unwrap_used = "deny"
26
+ 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,184 @@
1
+ # PriorityMachines
2
+
3
+ > Everyone's job is urgent. Someone still has to pick who goes next. 🚀
4
+
5
+ Three ways to answer "which lane now?" for Ruby. Weighted round robin hands out turns by weight. Deficit round robin hands out turns by cost, so a lane full of heavy jobs can't hog the worker. Earliest deadline first picks whoever is closest to being late. You keep the queues. The scheduler just points.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'priority_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ # Three tenants, three queues. Enterprise gets three turns for every one the others get.
15
+ turns = PriorityMachines::WeightedRoundRobin.new(weights: [3, 1, 1])
16
+
17
+ loop do
18
+ ready = queues.map { |q| !q.empty? } # [true, false, true]
19
+ lane = turns.select(ready) or break # => 0, 0, 0, 2, 0, 0, 0, 2 ...
20
+ run(queues[lane].shift)
21
+ end
22
+ ```
23
+
24
+ Weighted by cost instead of count? Tell it what the next job in each lane will cost:
25
+
26
+ ```ruby
27
+ credit = PriorityMachines::DeficitRoundRobin.new(quanta: [4, 2])
28
+ credit.select([3, 1]) # => 0, lane 0 has 4 credits and its head costs 3
29
+ ```
30
+
31
+ Racing a clock? Give it the deadlines and it points at the nearest one:
32
+
33
+ ```ruby
34
+ PriorityMachines.earliest_deadline([nil, 2.5, 1.0]) # => 2
35
+ ```
36
+
37
+ ## A Message to the Resistance
38
+
39
+ So you have one worker pool and three customers. The big one uploads a 40 GB CSV every night. The small ones upload a contact form. Your queue is FIFO, which is a polite way of saying the contact forms wait behind 40 GB of someone else's rows.
40
+
41
+ You tried priority levels. Now the big customer's jobs are all "high" too, because their account manager asked nicely.
42
+
43
+ The problem was never the ordering. It's that "next" has no memory. Round robin fixes count: everyone gets a turn. But a turn that costs a million rows is not the same as a turn that costs one, so you need a scheduler that remembers who already spent their share. And when the question is "who is about to miss an SLA," neither of those is the answer; the deadline is.
44
+
45
+ This gem is those three answers, with nothing else attached. No threads, no queues, no clock. You ask, it points at a lane.
46
+
47
+ ## Chapter 1: Stardate 2025.186, the AMS Liu
48
+
49
+ > **AMS Liu**, NCC-4215, *Liu*-class dispatch escort.
50
+ > Every shuttle says its launch is urgent. Someone still has to order the queue.
51
+
52
+ The escort doesn't fly anyone anywhere. It stands on the pad with a clipboard. Three bays, three pilots all claiming priority. The escort knows how many launches each bay was promised, how much fuel each launch burns, and which flight plan runs out of window first. It points. The bay launches. It points again.
53
+
54
+ On the pad, a dispatcher mutters:
55
+
56
+ ```ruby
57
+ def next_job
58
+ jobs.sort_by { |j| [-j.priority, j.enqueued_at] }.first
59
+ end
60
+ ```
61
+
62
+ "That," says the escort, "is why the hangar is full of contact forms."
63
+
64
+ ## The Flight Plan
65
+
66
+ Each scheduler keeps a little state per lane and moves it on every `select`.
67
+
68
+ ```mermaid
69
+ stateDiagram-v2
70
+ [*] --> idle
71
+ idle --> pointing: select(lanes)
72
+ pointing --> idle: lane chosen, its share spent
73
+ pointing --> idle: nothing ready, nil
74
+ idle --> idle: deactivate(lane), unspent share discarded
75
+ ```
76
+
77
+ The night the big customer's import finally waited its turn:
78
+
79
+ ```mermaid
80
+ sequenceDiagram
81
+ participant W as Worker
82
+ participant S as DeficitRoundRobin quanta [4, 2]
83
+ participant B as Bulk import lane
84
+ participant F as Contact form lane
85
+
86
+ W->>S: select([10, 1])
87
+ Note over S: bulk has 4 credits, needs 10. Form has 2, needs 1.
88
+ S-->>W: 1
89
+ W->>F: run one form
90
+ W->>S: select([10, 1])
91
+ S-->>W: 1
92
+ W->>F: run one form
93
+ W->>S: select([10, 1])
94
+ S-->>W: 1
95
+ W->>S: select([10, 1])
96
+ S-->>W: 1
97
+ W->>S: select([10, 1])
98
+ Note over S: bulk has saved up 12 credits now.
99
+ S-->>W: 0
100
+ W->>B: run the import
101
+ ```
102
+
103
+ ## Weighted Round Robin
104
+
105
+ One turn per weight unit, round after round. Pass an array of booleans saying which lanes have something waiting.
106
+
107
+ ```ruby
108
+ turns = PriorityMachines::WeightedRoundRobin.new(weights: [3, 1])
109
+ Array.new(8) { turns.select([true, true]) } # => [0, 0, 0, 1, 0, 0, 0, 1]
110
+ ```
111
+
112
+ A lane that isn't ready is skipped, not saved up:
113
+
114
+ ```ruby
115
+ turns = PriorityMachines::WeightedRoundRobin.new(weights: [3, 1])
116
+ Array.new(4) { turns.select([false, true]) } # => [1, 1, 1, 1]
117
+ ```
118
+
119
+ Nothing ready anywhere returns `nil`. Weights are positive Integers, one to sixteen lanes.
120
+
121
+ ## Deficit Round Robin
122
+
123
+ Each lane earns `quanta[lane]` credits per round. A lane is served when its credit covers the cost of its head job, and leftover credit carries forward. Pass the cost of the head job per lane, or `nil` for an empty lane.
124
+
125
+ ```ruby
126
+ credit = PriorityMachines::DeficitRoundRobin.new(quanta: [3, 1])
127
+ Array.new(8) { credit.select([1, 1]) } # => [0, 0, 0, 1, 0, 0, 0, 1]
128
+ ```
129
+
130
+ Same shape as weighted when every job costs one. Now make lane 0's jobs cost three:
131
+
132
+ ```ruby
133
+ credit = PriorityMachines::DeficitRoundRobin.new(quanta: [4, 2])
134
+ Array.new(6) { credit.select([3, 1]) } # => [0, 1, 1, 0, 1, 1]
135
+ ```
136
+
137
+ Lane 0 gets one heavy job per round. Lane 1 gets two light ones. Nobody starves, and the heavy lane can't buy extra turns by being heavy.
138
+
139
+ If a lane drains, throw away what it had saved so it doesn't come back with a windfall:
140
+
141
+ ```ruby
142
+ credit.deactivate(0) # => true
143
+ credit.deactivate(99) # => false, no such lane
144
+ ```
145
+
146
+ An empty lane is `nil`; costs are positive Integers.
147
+
148
+ ## Earliest Deadline First
149
+
150
+ No state at all. Hand it one deadline per lane, as seconds on whatever monotonic clock you use, with `nil` for lanes that have nothing. Get back the lane with the nearest deadline, lowest index on a tie.
151
+
152
+ ```ruby
153
+ PriorityMachines.earliest_deadline([10, nil, 0.25]) # => 2
154
+ PriorityMachines.earliest_deadline([5, 5, 1]) # => 2
155
+ PriorityMachines.earliest_deadline([nil, nil]) # => nil
156
+ ```
157
+
158
+ Deadlines in the past still count. A job that is already late is the most urgent one you have.
159
+
160
+ ## The Rules of the Escort
161
+
162
+ - **Lanes are one to sixteen.** Weights and quanta arrays set the lane count; every later call must pass an array of exactly that length or it raises `ArgumentError`.
163
+ - **Weights, quanta and costs are positive Integers.** Floats raise `TypeError`. Zero raises `ArgumentError`. Negative or above 32 bits raises `RangeError`.
164
+ - **Ready flags are real booleans.** Weighted round robin rejects anything that isn't `true` or `false`.
165
+ - **Deadlines are finite, non-negative** Integers or Floats, or `nil`. NaN, infinity, and negatives raise `ArgumentError`.
166
+ - **Bad input changes nothing.** The whole call is checked before any lane state moves.
167
+ - **Frozen means frozen.** `freeze` a scheduler and `select` and `deactivate` raise `FrozenError`. `lane_count` still answers.
168
+
169
+ ## What You Think You're Doing vs Reality
170
+
171
+ ### You Think: "I'll give the big customer a lower priority number."
172
+ ### Reality: Priority is a label. Fairness is a budget.
173
+
174
+ A priority sorts the queue once. It says nothing about how much of the worker the top entry is allowed to consume before someone else gets a go. A budget does, and it remembers what was already spent. That's the difference between "high priority" and "three turns per round."
175
+
176
+ ## What's Yours
177
+
178
+ The scheduler answers one question per call and remembers only what it needs for the next one. Your queues hold the jobs. Your code pops from the lane it points at and runs the job. Your clock supplies the deadlines. If several workers share one scheduler, wrap it in a lock. If you want a lane to stop earning credit, tell it with `deactivate`.
179
+
180
+ ## License
181
+
182
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
183
+
184
+ **Remember: The contact form was never low priority. It was just standing behind forty gigabytes.**
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.3.2](https://github.com/seuros/priority_machines/compare/priority-machines-v0.3.1...priority-machines-v0.3.2) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([1d389af](https://github.com/seuros/priority_machines/commit/1d389af51eacb70fe595028c381c68cb9d556f51))
9
+
10
+ ## [0.3.1](https://github.com/seuros/priority_machines/compare/priority-machines-v0.3.0...priority-machines-v0.3.1) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([37e58c0](https://github.com/seuros/priority_machines/commit/37e58c0575a088807db99d38ea820d87dde956b8))
16
+
17
+ ## [0.3.0](https://github.com/seuros/priority_machines/compare/priority-machines-v0.2.0...priority-machines-v0.3.0) (2026-11-06)
18
+
19
+
20
+ ### Features
21
+
22
+ * plain round robin scheduler in the core ([315e567](https://github.com/seuros/priority_machines/commit/315e5677ab93d677c71847233abcaa2bad5a7f4e))
23
+
24
+ ## [0.2.0](https://github.com/seuros/priority_machines/compare/priority-machines-v0.1.0...priority-machines-v0.2.0) (2026-11-06)
25
+
26
+
27
+ ### Features
28
+
29
+ * initial extraction ([ea040c3](https://github.com/seuros/priority_machines/commit/ea040c3e486d5b77a9eb4c68a8ac493399df8323))
@@ -0,0 +1,13 @@
1
+ [package]
2
+ name = "priority-machines"
3
+ version = "0.3.2"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ license.workspace = true
7
+ description = "Allocation-free EDF, round robin, weighted round robin, and deficit round robin decisions"
8
+ keywords = ["scheduling", "no-std", "priority", "round-robin", "edf"]
9
+ categories = ["no-std", "algorithms"]
10
+ readme = "README.md"
11
+ exclude = ["src/**/tests.rs", "tests/"]
12
+ [lints]
13
+ workspace = true
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 PriorityMachines contributors
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.
@@ -0,0 +1,125 @@
1
+ # priority-machines
2
+
3
+ > **AMS Liu**, NCC-4215, *Liu*-class dispatch escort.
4
+
5
+ Engine-room flight manual: order departures by deadline, weight, or accounted cost.
6
+
7
+ Allocation-free scheduling policies. No threads, I/O, wall clock, queue storage,
8
+ allocator, or dependencies. The crate is always `no_std`.
9
+
10
+ The caller supplies a view of eligible work; the scheduler returns an index.
11
+ A successful selection **consumes a scheduling turn or cost credit**. Serialize
12
+ selection with dequeue/admission in your adapter: this is not a reservation or
13
+ an execution guarantee. Filter blocked and cancelled work before selecting.
14
+
15
+ ## Earliest Deadline First
16
+
17
+ ```rust
18
+ use core::time::Duration;
19
+ use priority_machines::earliest_deadline;
20
+
21
+ let deadlines = [
22
+ Some(Duration::from_millis(30)),
23
+ None,
24
+ Some(Duration::from_millis(10)),
25
+ ];
26
+ assert_eq!(earliest_deadline(&deadlines), Some(2));
27
+ ```
28
+
29
+ Supply absolute deadlines as durations from one common monotonic origin.
30
+ `None` means ineligible, not "best effort with no deadline". Equal deadlines
31
+ choose the lowest index; order the input by arrival if FIFO ties are required.
32
+ Overdue deadlines remain eligible: cancellation and deadline enforcement belong
33
+ to the caller. This policy makes no preemption or schedulability guarantee.
34
+
35
+ ## Round Robin
36
+
37
+ ```rust
38
+ use priority_machines::RoundRobin;
39
+
40
+ let mut scheduler = RoundRobin::new();
41
+ assert_eq!(scheduler.select(&[false, true, true]), Some(1));
42
+ assert_eq!(scheduler.select(&[false, true, true]), Some(2));
43
+ assert_eq!(scheduler.select(&[true, true]), Some(0));
44
+ assert_eq!(scheduler.select(&[]), None);
45
+ ```
46
+
47
+ The unweighted case with no fixed lane count. The `ready` slice sets the lanes
48
+ on every call and the cursor wraps, so lanes may appear and vanish between
49
+ calls. One `usize` of state.
50
+
51
+ ## Weighted Round Robin
52
+
53
+ ```rust
54
+ use core::num::NonZeroU32;
55
+ use priority_machines::WeightedRoundRobin;
56
+
57
+ # fn example() -> Option<()> {
58
+ let mut scheduler = WeightedRoundRobin::new([
59
+ NonZeroU32::new(2)?,
60
+ NonZeroU32::MIN,
61
+ ]);
62
+ assert_eq!(
63
+ core::array::from_fn::<_, 6, _>(|_| scheduler.select(&[true, true])),
64
+ [Some(0), Some(0), Some(1), Some(0), Some(0), Some(1)],
65
+ );
66
+ # Some(())
67
+ # }
68
+ # assert_eq!(example(), Some(()));
69
+ ```
70
+
71
+ This is **burst WRR**, not smooth WRR. Each continuously ready lane gets its
72
+ positive weight in consecutive turns per cycle. It shares dispatch counts, not
73
+ bytes or CPU time. Unready lanes forfeit unused turns; idle lanes cannot bank
74
+ turns. Zero lanes are supported and select nothing.
75
+
76
+ ## Deficit Round Robin
77
+
78
+ ```rust
79
+ use core::num::NonZeroU32;
80
+ use priority_machines::DeficitRoundRobin;
81
+
82
+ # fn example() -> Option<()> {
83
+ let mut scheduler = DeficitRoundRobin::new([
84
+ NonZeroU32::new(4)?,
85
+ NonZeroU32::new(2)?,
86
+ ]);
87
+ let heads = [NonZeroU32::new(2), NonZeroU32::new(2)];
88
+ assert_eq!(
89
+ core::array::from_fn::<_, 6, _>(|_| scheduler.select(&heads)),
90
+ [Some(0), Some(0), Some(1), Some(0), Some(0), Some(1)],
91
+ );
92
+ # Some(())
93
+ # }
94
+ # assert_eq!(example(), Some(()));
95
+ ```
96
+
97
+ Each lane receives a positive quantum per visit. A head consumes its positive
98
+ cost from that lane's deficit. Remaining credit can serve more heads in the same
99
+ visit; insufficient credit carries into the next round. Quanta and head costs
100
+ must use the same unit, such as bytes or estimated work units.
101
+
102
+ Pass the current head cost for each lane, or `None` for an empty lane, on every
103
+ selection. Update your queues after dispatch. When a lane drains or is removed,
104
+ call `deactivate(lane)` **before refilling it** so old credit cannot leak into a
105
+ new busy period. Empty lanes observed during selection also lose their credit.
106
+ WRR exposes the same lifecycle hook to relinquish unused turns immediately.
107
+
108
+ Costs larger than a quantum still make progress. Entire unproductive rounds are
109
+ skipped arithmetically: selection uses at most two lane scans, rather than
110
+ looping once per unit of a large cost. Credits use `u64` internally for `u32`
111
+ quanta/costs. Idle credit is not accumulated.
112
+
113
+ Fairness is in accounted cost over continuously backlogged lanes, not in job
114
+ counts or actual execution time. A bad cost estimate produces a bad resource
115
+ share; this core does not measure or preempt work.
116
+
117
+ ## Integration Contract
118
+
119
+ - Keep lane indices stable for the lifetime of a scheduler.
120
+ - Weights, quanta, and lane count are fixed at construction. Construct a fresh
121
+ scheduler when changing them.
122
+ - Selection is deterministic for the same initial state and input sequence.
123
+ - Snapshot views and dispatch must be coordinated by the caller; the core adds
124
+ no locking.
125
+ - No serialization format or Ruby binding is promised by this initial version.
@@ -0,0 +1,111 @@
1
+ use core::num::NonZeroU32;
2
+
3
+ /// Deficit round robin over fixed, caller-owned lanes with explicit head costs.
4
+ ///
5
+ /// Positive quanta and costs use the same unit. Empty lanes lose their credit.
6
+ /// Each successful selection charges one head; update the queue view before
7
+ /// selecting again. This policy measures accounted work, not actual CPU time.
8
+ #[derive(Debug, Clone, PartialEq, Eq)]
9
+ pub struct DeficitRoundRobin<const N: usize> {
10
+ quanta: [NonZeroU32; N],
11
+ deficits: [u64; N],
12
+ cursor: usize,
13
+ needs_quantum: bool,
14
+ }
15
+
16
+ impl<const N: usize> DeficitRoundRobin<N> {
17
+ /// Construct a scheduler with positive quanta and no accumulated credit.
18
+ #[must_use]
19
+ pub const fn new(quanta: [NonZeroU32; N]) -> Self {
20
+ Self {
21
+ quanta,
22
+ deficits: [0; N],
23
+ cursor: 0,
24
+ needs_quantum: true,
25
+ }
26
+ }
27
+
28
+ /// Charge and select the next affordable head.
29
+ ///
30
+ /// `None` marks an empty lane. Returns `None` exactly when all lanes are
31
+ /// empty. Expensive heads accumulate enough credit via skipped rounds in
32
+ /// O(N) time, using at most two selection scans.
33
+ pub fn select(&mut self, heads: &[Option<NonZeroU32>; N]) -> Option<usize> {
34
+ // Observe every empty lane, even if selection stops before visiting it.
35
+ for (deficit, head) in self.deficits.iter_mut().zip(heads) {
36
+ if head.is_none() {
37
+ *deficit = 0;
38
+ }
39
+ }
40
+ if let Some(lane) = self.select_round(heads) {
41
+ return Some(lane);
42
+ }
43
+
44
+ // Every active head is now unaffordable and every lane has been visited.
45
+ // Skip only whole rounds in which no head could have been dispatched.
46
+ let rounds = heads
47
+ .iter()
48
+ .enumerate()
49
+ .filter_map(|(lane, head)| {
50
+ head.map(|cost| {
51
+ (u64::from(cost.get()) - self.deficits[lane])
52
+ .div_ceil(u64::from(self.quanta[lane].get()))
53
+ })
54
+ })
55
+ .min()?;
56
+ let skipped = rounds - 1;
57
+ for (lane, head) in heads.iter().enumerate() {
58
+ if head.is_some() {
59
+ self.deficits[lane] += skipped * u64::from(self.quanta[lane].get());
60
+ }
61
+ }
62
+ self.select_round(heads)
63
+ }
64
+
65
+ /// Discard a lane's credit and end its current visit, if any.
66
+ ///
67
+ /// Call when a queue drains or is removed, before refilling that lane.
68
+ /// Returns `false` for an out-of-range index, without changing state.
69
+ pub const fn deactivate(&mut self, lane: usize) -> bool {
70
+ if lane >= N {
71
+ return false;
72
+ }
73
+ self.deficits[lane] = 0;
74
+ if lane == self.cursor {
75
+ self.advance();
76
+ }
77
+ true
78
+ }
79
+
80
+ fn select_round(&mut self, heads: &[Option<NonZeroU32>; N]) -> Option<usize> {
81
+ for _ in 0..N {
82
+ let lane = self.cursor;
83
+ if let Some(cost) = heads[lane] {
84
+ if self.needs_quantum {
85
+ // An unsuccessful visit leaves deficit < cost <= u32::MAX.
86
+ // Adding a u32 quantum therefore cannot overflow u64.
87
+ self.deficits[lane] += u64::from(self.quanta[lane].get());
88
+ self.needs_quantum = false;
89
+ }
90
+ if self.deficits[lane] >= u64::from(cost.get()) {
91
+ self.deficits[lane] -= u64::from(cost.get());
92
+ return Some(lane);
93
+ }
94
+ }
95
+ self.advance();
96
+ }
97
+ None
98
+ }
99
+
100
+ const fn advance(&mut self) {
101
+ self.cursor = if self.cursor + 1 == N {
102
+ 0
103
+ } else {
104
+ self.cursor + 1
105
+ };
106
+ self.needs_quantum = true;
107
+ }
108
+ }
109
+
110
+ #[cfg(test)]
111
+ mod tests;
@@ -0,0 +1,19 @@
1
+ use core::time::Duration;
2
+
3
+ /// Select the lowest-index eligible item with the earliest deadline.
4
+ ///
5
+ /// Deadlines must share one monotonic origin. `None` marks an ineligible item.
6
+ /// Expired items are not automatically dropped. Returns `None` if no item is
7
+ /// eligible, including when the input is empty. Runs in O(n) time.
8
+ #[must_use]
9
+ pub fn earliest_deadline(deadlines: &[Option<Duration>]) -> Option<usize> {
10
+ deadlines
11
+ .iter()
12
+ .enumerate()
13
+ .filter_map(|(index, deadline)| deadline.map(|value| (index, value)))
14
+ .min_by_key(|&(_, deadline)| deadline)
15
+ .map(|(index, _)| index)
16
+ }
17
+
18
+ #[cfg(test)]
19
+ mod tests;
@@ -0,0 +1,12 @@
1
+ #![no_std]
2
+ #![doc = include_str!("../README.md")]
3
+
4
+ mod deficit;
5
+ mod edf;
6
+ mod round_robin;
7
+ mod weighted;
8
+
9
+ pub use deficit::DeficitRoundRobin;
10
+ pub use edf::earliest_deadline;
11
+ pub use round_robin::RoundRobin;
12
+ pub use weighted::WeightedRoundRobin;
@@ -0,0 +1,47 @@
1
+ /// Plain round robin over however many lanes the caller has right now.
2
+ ///
3
+ /// The lane count is the length of the `ready` slice on each call, so lanes may
4
+ /// be added or removed between calls; the cursor wraps. All lanes weigh the
5
+ /// same. The caller owns queue storage and execution.
6
+ #[derive(Debug, Clone, Default, PartialEq, Eq)]
7
+ pub struct RoundRobin {
8
+ cursor: usize,
9
+ }
10
+
11
+ impl RoundRobin {
12
+ /// Construct a scheduler initially visiting lane zero.
13
+ #[must_use]
14
+ pub const fn new() -> Self {
15
+ Self { cursor: 0 }
16
+ }
17
+
18
+ /// The next ready lane at or after the cursor; the cursor moves past it.
19
+ ///
20
+ /// Returns `None` exactly when no lane is ready. O(N) worst case.
21
+ pub const fn select(&mut self, ready: &[bool]) -> Option<usize> {
22
+ let lanes = ready.len();
23
+ if lanes == 0 {
24
+ return None;
25
+ }
26
+ let mut lane = self.cursor % lanes;
27
+ let mut visited = 0;
28
+ while visited < lanes {
29
+ if ready[lane] {
30
+ self.cursor = if lane + 1 == lanes { 0 } else { lane + 1 };
31
+ return Some(lane);
32
+ }
33
+ lane = if lane + 1 == lanes { 0 } else { lane + 1 };
34
+ visited += 1;
35
+ }
36
+ None
37
+ }
38
+
39
+ /// Where the next search starts.
40
+ #[must_use]
41
+ pub const fn cursor(&self) -> usize {
42
+ self.cursor
43
+ }
44
+ }
45
+
46
+ #[cfg(test)]
47
+ mod tests;
@@ -0,0 +1,69 @@
1
+ use core::num::NonZeroU32;
2
+
3
+ /// Burst weighted round robin over a fixed set of caller-owned lanes.
4
+ ///
5
+ /// Weights count dispatches, not work cost. Empty lanes forfeit their remaining
6
+ /// turns. The caller owns queue storage and execution.
7
+ #[derive(Debug, Clone, PartialEq, Eq)]
8
+ pub struct WeightedRoundRobin<const N: usize> {
9
+ weights: [NonZeroU32; N],
10
+ cursor: usize,
11
+ remaining: u32,
12
+ }
13
+
14
+ impl<const N: usize> WeightedRoundRobin<N> {
15
+ /// Construct a scheduler with positive weights, initially visiting lane zero.
16
+ #[must_use]
17
+ pub const fn new(weights: [NonZeroU32; N]) -> Self {
18
+ Self {
19
+ remaining: if N == 0 { 0 } else { weights[0].get() },
20
+ weights,
21
+ cursor: 0,
22
+ }
23
+ }
24
+
25
+ /// Consume one turn from the next ready lane.
26
+ ///
27
+ /// Returns `None` exactly when no lane is ready. Runs in O(N) worst-case
28
+ /// time; repeated dispatches from the current lane take O(1).
29
+ pub fn select(&mut self, ready: &[bool; N]) -> Option<usize> {
30
+ for _ in 0..N {
31
+ let lane = self.cursor;
32
+ if ready[lane] {
33
+ self.remaining -= 1;
34
+ if self.remaining == 0 {
35
+ self.advance();
36
+ }
37
+ return Some(lane);
38
+ }
39
+ self.advance();
40
+ }
41
+ None
42
+ }
43
+
44
+ /// Relinquish unused turns if this lane is currently being visited.
45
+ ///
46
+ /// Call when a queue drains or is removed, before reusing that lane.
47
+ /// Returns `false` for an out-of-range index, without changing state.
48
+ pub const fn deactivate(&mut self, lane: usize) -> bool {
49
+ if lane >= N {
50
+ return false;
51
+ }
52
+ if lane == self.cursor {
53
+ self.advance();
54
+ }
55
+ true
56
+ }
57
+
58
+ const fn advance(&mut self) {
59
+ self.cursor = if self.cursor + 1 == N {
60
+ 0
61
+ } else {
62
+ self.cursor + 1
63
+ };
64
+ self.remaining = self.weights[self.cursor].get();
65
+ }
66
+ }
67
+
68
+ #[cfg(test)]
69
+ 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 = "priority_machines_native"
3
+ version = "0.1.4"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ license.workspace = true
7
+ description = "Thin native Ruby bindings for priority-machines"
8
+ publish = false
9
+
10
+ [lib]
11
+ crate-type = ["cdylib"]
12
+ test = false
13
+ doctest = false
14
+
15
+ [dependencies]
16
+ priority-machines.workspace = true
17
+ magnus.workspace = true
18
+ rb-sys.workspace = true
19
+
20
+ [lints]
21
+ workspace = true
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mkmf'
4
+ require 'pathname'
5
+ require 'rb_sys/mkmf'
6
+
7
+ create_rust_makefile('priority_machines_native/priority_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
+ # rake-compiler builds from tmp/, not from the extension source directory.
13
+ makefile = File.join(Dir.pwd, 'Makefile')
14
+ File.write(makefile, File.read(makefile).gsub(
15
+ /^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/,
16
+ "RB_SYS_CARGO_MANIFEST_DIR ?= #{File.expand_path(__dir__)}"
17
+ ))
@@ -0,0 +1,8 @@
1
+ use magnus::{Error, Ruby, function, prelude::*};
2
+
3
+ #[magnus::init]
4
+ fn init(ruby: &Ruby) -> Result<(), Error> {
5
+ let module = ruby.define_module("PriorityMachines")?;
6
+ module.define_singleton_method("earliest_deadline", function!(crate::earliest_deadline, 1))?;
7
+ crate::scheduler::define(ruby, module)
8
+ }
@@ -0,0 +1,62 @@
1
+ //! Thin Ruby adapters; scheduling decisions belong exclusively to the core.
2
+
3
+ mod init;
4
+ mod scheduler;
5
+
6
+ use std::{num::NonZeroU32, time::Duration};
7
+
8
+ use magnus::{Error, Float, Integer, RArray, Ruby, TryConvert, Value, prelude::*};
9
+
10
+ #[derive(Clone, Copy)]
11
+ pub(crate) struct Count(pub(crate) u32);
12
+
13
+ impl TryConvert for Count {
14
+ fn try_convert(value: Value) -> Result<Self, Error> {
15
+ let ruby = Ruby::get_with(value);
16
+ if Integer::from_value(value).is_none() {
17
+ return Err(Error::new(
18
+ ruby.exception_type_error(),
19
+ "expected an Integer",
20
+ ));
21
+ }
22
+ u32::try_convert(value).map(Self)
23
+ }
24
+ }
25
+
26
+ pub(crate) fn positive(ruby: &Ruby, value: Value) -> Result<NonZeroU32, Error> {
27
+ NonZeroU32::new(Count::try_convert(value)?.0)
28
+ .ok_or_else(|| Error::new(ruby.exception_arg_error(), "expected a positive u32"))
29
+ }
30
+
31
+ fn duration(ruby: &Ruby, value: Value) -> Result<Duration, Error> {
32
+ if Integer::from_value(value).is_some() {
33
+ return u64::try_convert(value).map(Duration::from_secs);
34
+ }
35
+ if Float::from_value(value).is_none() {
36
+ return Err(Error::new(
37
+ ruby.exception_type_error(),
38
+ "seconds must be Integer or Float",
39
+ ));
40
+ }
41
+ Duration::try_from_secs_f64(f64::try_convert(value)?).map_err(|_| {
42
+ Error::new(
43
+ ruby.exception_arg_error(),
44
+ "seconds must be finite, nonnegative and representable",
45
+ )
46
+ })
47
+ }
48
+
49
+ pub(crate) fn earliest_deadline(ruby: &Ruby, deadlines: RArray) -> Result<Option<usize>, Error> {
50
+ // Convert the complete call before entering the core.
51
+ let deadlines = deadlines
52
+ .into_iter()
53
+ .map(|value| {
54
+ if value.is_nil() {
55
+ Ok(None)
56
+ } else {
57
+ duration(ruby, value).map(Some)
58
+ }
59
+ })
60
+ .collect::<Result<Vec<_>, Error>>()?;
61
+ Ok(priority_machines::earliest_deadline(&deadlines))
62
+ }
@@ -0,0 +1,156 @@
1
+ //! Exact const-generic lane counts, erased only at the Ruby boundary.
2
+
3
+ use std::{cell::RefCell, num::NonZeroU32};
4
+
5
+ use magnus::{
6
+ Error, RArray, RHash, RModule, Ruby, Value, function, method,
7
+ prelude::*,
8
+ scan_args::{get_kwargs, scan_args},
9
+ typed_data::Obj,
10
+ };
11
+ use priority_machines::{DeficitRoundRobin, WeightedRoundRobin};
12
+
13
+ use crate::{Count, positive};
14
+
15
+ // This trait erases const-generic sizes, never adds inactive/phantom lanes.
16
+ trait Policy: Send {
17
+ fn select(&mut self, heads: &[Option<NonZeroU32>]) -> Option<usize>;
18
+ fn deactivate(&mut self, lane: usize) -> bool;
19
+ }
20
+
21
+ impl<const N: usize> Policy for WeightedRoundRobin<N> {
22
+ fn select(&mut self, heads: &[Option<NonZeroU32>]) -> Option<usize> {
23
+ self.select(&std::array::from_fn(|index| heads[index].is_some()))
24
+ }
25
+ fn deactivate(&mut self, lane: usize) -> bool {
26
+ self.deactivate(lane)
27
+ }
28
+ }
29
+
30
+ impl<const N: usize> Policy for DeficitRoundRobin<N> {
31
+ fn select(&mut self, heads: &[Option<NonZeroU32>]) -> Option<usize> {
32
+ self.select(&std::array::from_fn(|index| heads[index]))
33
+ }
34
+ fn deactivate(&mut self, lane: usize) -> bool {
35
+ self.deactivate(lane)
36
+ }
37
+ }
38
+
39
+ struct State {
40
+ policy: RefCell<Box<dyn Policy>>,
41
+ lanes: usize,
42
+ }
43
+
44
+ fn make(ruby: &Ruby, values: RArray, deficit: bool) -> Result<State, Error> {
45
+ let values = values
46
+ .into_iter()
47
+ .map(|value| positive(ruby, value))
48
+ .collect::<Result<Vec<_>, _>>()?;
49
+ macro_rules! sized {
50
+ ($($n:literal),*) => {
51
+ match values.len() {
52
+ $($n => {
53
+ let array = std::array::from_fn::<_, $n, _>(|index| values[index]);
54
+ if deficit { Box::new(DeficitRoundRobin::new(array)) as Box<dyn Policy> }
55
+ else { Box::new(WeightedRoundRobin::new(array)) as Box<dyn Policy> }
56
+ },)*
57
+ _ => return Err(Error::new(ruby.exception_arg_error(), "lane count must be 1..16")),
58
+ }
59
+ };
60
+ }
61
+ let policy = sized!(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);
62
+ Ok(State {
63
+ policy: RefCell::new(policy),
64
+ lanes: values.len(),
65
+ })
66
+ }
67
+
68
+ fn inputs(
69
+ ruby: &Ruby,
70
+ state: &State,
71
+ values: RArray,
72
+ deficit: bool,
73
+ ) -> Result<Vec<Option<NonZeroU32>>, Error> {
74
+ if values.len() != state.lanes {
75
+ return Err(Error::new(
76
+ ruby.exception_arg_error(),
77
+ "input length must match lane count",
78
+ ));
79
+ }
80
+ values
81
+ .into_iter()
82
+ .map(|value| {
83
+ if deficit {
84
+ if value.is_nil() {
85
+ Ok(None)
86
+ } else {
87
+ positive(ruby, value).map(Some)
88
+ }
89
+ } else if magnus::value::Qtrue::from_value(value).is_some() {
90
+ Ok(Some(NonZeroU32::MIN))
91
+ } else if magnus::value::Qfalse::from_value(value).is_some() {
92
+ Ok(None)
93
+ } else {
94
+ Err(Error::new(
95
+ ruby.exception_type_error(),
96
+ "ready entries must be true or false",
97
+ ))
98
+ }
99
+ })
100
+ .collect()
101
+ }
102
+
103
+ #[magnus::wrap(class = "PriorityMachines::WeightedRoundRobin", free_immediately, size)]
104
+ struct Weighted(State);
105
+
106
+ #[magnus::wrap(class = "PriorityMachines::DeficitRoundRobin", free_immediately, size)]
107
+ struct Deficit(State);
108
+
109
+ macro_rules! wrapper {
110
+ ($type:ident, $class:literal, $keyword:literal, $deficit:literal) => {
111
+ impl $type {
112
+ fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
113
+ let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
114
+ let (values,) =
115
+ get_kwargs::<_, (RArray,), (), ()>(args.keywords, &[$keyword], &[])?.required;
116
+ make(ruby, values, $deficit).map(Self)
117
+ }
118
+ fn select(
119
+ ruby: &Ruby,
120
+ rb_self: Obj<Self>,
121
+ values: RArray,
122
+ ) -> Result<Option<usize>, Error> {
123
+ rb_self.check_frozen()?;
124
+ let values = inputs(ruby, &rb_self.0, values, $deficit)?;
125
+ let selected = rb_self.0.policy.borrow_mut().select(&values);
126
+ Ok(selected)
127
+ }
128
+ fn deactivate(rb_self: Obj<Self>, lane: Count) -> Result<bool, Error> {
129
+ rb_self.check_frozen()?;
130
+ let changed = rb_self.0.policy.borrow_mut().deactivate(lane.0 as usize);
131
+ Ok(changed)
132
+ }
133
+ const fn lane_count(&self) -> usize {
134
+ self.0.lanes
135
+ }
136
+ }
137
+ };
138
+ }
139
+
140
+ wrapper!(Weighted, "WeightedRoundRobin", "weights", false);
141
+ wrapper!(Deficit, "DeficitRoundRobin", "quanta", true);
142
+
143
+ pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
144
+ macro_rules! register {
145
+ ($type:ident, $name:literal) => {{
146
+ let class = module.define_class($name, ruby.class_object())?;
147
+ class.define_singleton_method("new", function!($type::new, -1))?;
148
+ class.define_method("select", method!($type::select, 1))?;
149
+ class.define_method("deactivate", method!($type::deactivate, 1))?;
150
+ class.define_method("lane_count", method!($type::lane_count, 0))?;
151
+ }};
152
+ }
153
+ register!(Weighted, "WeightedRoundRobin");
154
+ register!(Deficit, "DeficitRoundRobin");
155
+ Ok(())
156
+ }
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PriorityMachines
4
+ VERSION = '0.1.5'
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'priority_machines/version'
4
+ require 'priority_machines_native/priority_machines_native'
@@ -0,0 +1,18 @@
1
+ module PriorityMachines
2
+ VERSION: String
3
+ def self.earliest_deadline: (Array[Integer | Float | nil] deadlines) -> Integer?
4
+
5
+ class WeightedRoundRobin
6
+ def self.new: (weights: Array[Integer]) -> instance
7
+ def select: (Array[bool] ready) -> Integer?
8
+ def deactivate: (Integer lane) -> bool
9
+ def lane_count: () -> Integer
10
+ end
11
+
12
+ class DeficitRoundRobin
13
+ def self.new: (quanta: Array[Integer]) -> instance
14
+ def select: (Array[Integer?] heads) -> Integer?
15
+ def deactivate: (Integer lane) -> bool
16
+ def lane_count: () -> Integer
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: priority_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 binding to the standalone allocation-free priority-machines
33
+ Rust core. Initial, unstable local implementation; caller owns queues and execution.
34
+ email:
35
+ - oss@seuros.com
36
+ executables: []
37
+ extensions:
38
+ - ext/priority_machines_native/extconf.rb
39
+ extra_rdoc_files: []
40
+ files:
41
+ - CHANGELOG.md
42
+ - Cargo.toml
43
+ - LICENSE.txt
44
+ - README.md
45
+ - ext/priority_machines_native/core/CHANGELOG.md
46
+ - ext/priority_machines_native/core/Cargo.toml
47
+ - ext/priority_machines_native/core/LICENSE
48
+ - ext/priority_machines_native/core/README.md
49
+ - ext/priority_machines_native/core/src/deficit.rs
50
+ - ext/priority_machines_native/core/src/edf.rs
51
+ - ext/priority_machines_native/core/src/lib.rs
52
+ - ext/priority_machines_native/core/src/round_robin.rs
53
+ - ext/priority_machines_native/core/src/weighted.rs
54
+ - ext/priority_machines_native/extconf.rb
55
+ - ext/priority_machines_native/ffi/Cargo.toml
56
+ - ext/priority_machines_native/ffi/extconf.rb
57
+ - ext/priority_machines_native/ffi/src/init.rs
58
+ - ext/priority_machines_native/ffi/src/lib.rs
59
+ - ext/priority_machines_native/ffi/src/scheduler.rs
60
+ - lib/priority_machines.rb
61
+ - lib/priority_machines/version.rb
62
+ - sig/priority_machines.rbs
63
+ homepage: https://github.com/seuros/priority_machines
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ source_code_uri: https://github.com/seuros/priority_machines
68
+ bug_tracker_uri: https://github.com/seuros/priority_machines/issues
69
+ rubygems_mfa_required: 'true'
70
+ cargo_crate_name: priority_machines_native
71
+ cargo_manifest_path: ext/priority_machines_native/ffi/Cargo.toml
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 4.0.0
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 4.0.20
87
+ specification_version: 4
88
+ summary: Native deadline, weighted and deficit scheduling policies
89
+ test_files: []