priority_machines 0.1.5-x86_64-linux-musl
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 +7 -0
- data/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +184 -0
- data/ext/priority_machines_native/core/CHANGELOG.md +29 -0
- data/ext/priority_machines_native/core/LICENSE +21 -0
- data/ext/priority_machines_native/core/README.md +125 -0
- data/ext/priority_machines_native/extconf.rb +3 -0
- data/lib/priority_machines/version.rb +5 -0
- data/lib/priority_machines.rb +4 -0
- data/lib/priority_machines_native/priority_machines_native.so +0 -0
- data/sig/priority_machines.rbs +18 -0
- metadata +63 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bafce6c8f7f5ab703eb5f82e5777f6611a030f4da3b0525a1add498b44a55658
|
|
4
|
+
data.tar.gz: 7aef1746e644089f9d227eb1f4a9aac9d444c142b00502f5399046cfc604fade
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6418fb49e6e3fbebdd00f9434794ffa65c5f37268656ca0c1f42cda637b0cb60fb5366fafcc456878de9c6455c7b422e078d870efe422b50a8243bf417320adc
|
|
7
|
+
data.tar.gz: 64a56bf675274d8e5cc2d6f8af4bc6f2d7fff1e4a819fa737ba7ae976b3a25d009efe03d8cb0ecd83bc5ed3ae4782906700a780e81cb5d478d3b8648023f6f58
|
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/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,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.
|
|
Binary file
|
|
@@ -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,63 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: priority_machines
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.5
|
|
5
|
+
platform: x86_64-linux-musl
|
|
6
|
+
authors:
|
|
7
|
+
- Abdelkader Boudih
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-11-06 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Thin Ruby binding to the standalone allocation-free priority-machines
|
|
14
|
+
Rust core. Initial, unstable local implementation; caller owns queues and execution.
|
|
15
|
+
email:
|
|
16
|
+
- oss@seuros.com
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- CHANGELOG.md
|
|
22
|
+
- LICENSE.txt
|
|
23
|
+
- README.md
|
|
24
|
+
- ext/priority_machines_native/core/CHANGELOG.md
|
|
25
|
+
- ext/priority_machines_native/core/LICENSE
|
|
26
|
+
- ext/priority_machines_native/core/README.md
|
|
27
|
+
- ext/priority_machines_native/extconf.rb
|
|
28
|
+
- lib/priority_machines.rb
|
|
29
|
+
- lib/priority_machines/version.rb
|
|
30
|
+
- lib/priority_machines_native/priority_machines_native.so
|
|
31
|
+
- sig/priority_machines.rbs
|
|
32
|
+
homepage: https://github.com/seuros/priority_machines
|
|
33
|
+
licenses:
|
|
34
|
+
- MIT
|
|
35
|
+
metadata:
|
|
36
|
+
source_code_uri: https://github.com/seuros/priority_machines
|
|
37
|
+
bug_tracker_uri: https://github.com/seuros/priority_machines/issues
|
|
38
|
+
rubygems_mfa_required: 'true'
|
|
39
|
+
cargo_crate_name: priority_machines_native
|
|
40
|
+
cargo_manifest_path: ext/priority_machines_native/ffi/Cargo.toml
|
|
41
|
+
post_install_message:
|
|
42
|
+
rdoc_options: []
|
|
43
|
+
require_paths:
|
|
44
|
+
- lib
|
|
45
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '4.0'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: 4.1.dev
|
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: 3.3.22
|
|
58
|
+
requirements: []
|
|
59
|
+
rubygems_version: 3.5.23
|
|
60
|
+
signing_key:
|
|
61
|
+
specification_version: 4
|
|
62
|
+
summary: Native deadline, weighted and deficit scheduling policies
|
|
63
|
+
test_files: []
|