fair_machines 0.1.5-x86_64-linux

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: c3c4faee52a632fb98e06653ec81b1c60b779479aeadc28bf6dde0fa8622762e
4
+ data.tar.gz: cea172d1a0cb2679bbd9ef3db6defbc2d62983082c282c5c688a4bcefc3fca34
5
+ SHA512:
6
+ metadata.gz: 58e781a081e6f3afe3a874cd07224a52cfd8be2ec1cb3cf8598c3d9910e7aa9581ca7c14dc0fc68ce019d2a4e3bc641b14933bcf9cf2944772620a1e2f4264e7
7
+ data.tar.gz: 797826760ab020f80c912e2cdfebcc255de233dd5df05e8643811eb718568e3c8b15f8d15f9018683bd355944b622a1569da6d54d4ad5a537d7f6d6f34fd85ec
data/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## [0.1.5](https://github.com/seuros/fair_machines/compare/fair_machines/v0.1.4...fair_machines/v0.1.5) (2026-11-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * add KeyedFair hashed-lane budgets ([33d8f62](https://github.com/seuros/fair_machines/commit/33d8f62d6c8f8e8ad8a9bc759dffbdfa8b7a2706))
9
+
10
+ ## [0.1.4](https://github.com/seuros/fair_machines/compare/fair_machines/v0.1.3...fair_machines/v0.1.4) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * share positive count parsing in tenants ffi ([85f970b](https://github.com/seuros/fair_machines/commit/85f970b7e7002248032c25fc36ad03b1b96ca56c))
16
+
17
+ ## [0.1.3](https://github.com/seuros/fair_machines/compare/fair_machines/v0.1.2...fair_machines/v0.1.3) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * crates.io keywords and categories for the core ([c9e496f](https://github.com/seuros/fair_machines/commit/c9e496fca380c51529e965ce4c8158f5210ed258))
23
+
24
+ ## [0.1.2](https://github.com/seuros/fair_machines/compare/fair_machines/v0.1.1...fair_machines/v0.1.2) (2026-11-06)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * keep test sources out of the packaged gem and crate ([58a408b](https://github.com/seuros/fair_machines/commit/58a408ba6727d04d8517a6b76c47b90e381203e3))
30
+
31
+ ## [0.1.1](https://github.com/seuros/fair_machines/compare/fair_machines-v0.1.0...fair_machines/v0.1.1) (2026-11-06)
32
+
33
+
34
+ ### Features
35
+
36
+ * initial extraction ([3c25da7](https://github.com/seuros/fair_machines/commit/3c25da7ef06236f290cef43e29ce5519cbf69aff))
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,216 @@
1
+ # FairMachines
2
+
3
+ > One tenant with ten thousand jobs should not make every other tenant wait ten thousand jobs. ⚖️
4
+
5
+ Cost-aware fair scheduling for Ruby. You give each tenant a budget for how much work it may have queued and a share of each dispatch round. `TenantFair` tells you whose job goes next, charges the tenant for its cost, and refuses to admit work past the budget. The loud tenant still gets served. It just doesn't get served *instead of* everyone else.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'fair_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ # Two tenants. Each may have 100 units of work queued; each gets 2 units per round.
15
+ fair = FairMachines::TenantFair.new(capacities: [100, 100], quanta: [2, 2])
16
+
17
+ # Enterprise tenant enqueues ten PDF exports at cost 5. Small tenant queues five emails at cost 1.
18
+ 10.times { fair.admit(tenant: 0, cost: 5) }
19
+ 5.times { fair.admit(tenant: 1, cost: 1) }
20
+
21
+ # Each round, tell the scheduler the cost at the head of every tenant's queue.
22
+ # It answers with the tenant whose head runs now. You pop that job and run it.
23
+ fair.select([5, 1]) # => 1 the email goes first
24
+ fair.select([5, 1]) # => 1
25
+ fair.select([5, 1]) # => 1
26
+ fair.select([5, 1]) # => 1
27
+ fair.select([5, 1]) # => 0 the PDF has saved up enough share
28
+ ```
29
+
30
+ Tenants are indices into the arrays you passed in. Heads are the cost of each tenant's next job, or `nil` when that tenant has nothing waiting.
31
+
32
+ ## A Message to the Resistance
33
+
34
+ So you built the job queue. Priorities, retries, a dashboard. Then your biggest customer clicked "export everything" and ten thousand PDF jobs landed in the queue at once, and for the next forty minutes nobody else's password reset email went out.
35
+
36
+ You added a second queue for the big customer. Then a third for the next big customer. Then a priority flag that every customer's jobs somehow ended up setting. None of it fixes the actual problem, which is that the queue has no idea what a tenant is, let alone how much of the worker pool that tenant has already consumed.
37
+
38
+ Fairness is two numbers per tenant. How much work may this tenant have waiting at once, and how big a slice of each round does it get. With those, a ten-thousand-job export gets a fixed share of the workers and the password reset goes out in seconds, because the small tenant's slice is sitting there unused.
39
+
40
+ This gem keeps those two numbers honest. You keep the queues.
41
+
42
+ ## Chapter 1: Stardate 2025.186, the AMS Shreedhar
43
+
44
+ > **AMS Shreedhar**, NCC-4207, *Shreedhar*-class convoy escort.
45
+ > The flagship does not get to spend every tenant's fuel allowance.
46
+
47
+ The escort doesn't fly the convoy. It holds the fuel ledger: how much each ship may have on request at once, and how much is pumped to each per cycle. A ship with a loud captain and a long manifest still draws from its own allowance, charged by the actual mass of what it asked for. A dinghy asking for one pallet gets its pallet on the next cycle.
48
+
49
+ Somewhere on the bridge, a junior officer suggests:
50
+
51
+ ```ruby
52
+ def next_job
53
+ queue.shift # it's a queue, first come first served, that's fair, right?
54
+ end
55
+ ```
56
+
57
+ "That," says the convoy master, "is how a dinghy starves behind a freighter."
58
+
59
+ ## The Flight Plan
60
+
61
+ Each tenant is either idle or has cost queued. Admission moves cost in, dispatch and cancellation move it out, and a full budget turns new work away.
62
+
63
+ ```mermaid
64
+ stateDiagram-v2
65
+ [*] --> idle
66
+ idle --> queued: admit within budget
67
+ queued --> queued: admit within budget
68
+ queued --> queued: admit over budget, refused
69
+ queued --> queued: select, head cost charged
70
+ queued --> idle: last head dispatched or cancelled
71
+ queued --> queued: cancel, cost returned
72
+ idle --> [*]
73
+ ```
74
+
75
+ And the afternoon the enterprise tenant clicked "export everything":
76
+
77
+ ```mermaid
78
+ sequenceDiagram
79
+ participant E as Enterprise (tenant 0)
80
+ participant F as TenantFair
81
+ participant S as Small shop (tenant 1)
82
+ participant W as Workers
83
+
84
+ E->>F: admit cost 5, ten times
85
+ F-->>E: true, true, true...
86
+ S->>F: admit cost 1
87
+ F-->>S: true
88
+
89
+ W->>F: select([5, 1])
90
+ F-->>W: tenant 1
91
+ Note over W: password reset goes out
92
+
93
+ W->>F: select([5, nil])
94
+ F-->>W: tenant 0
95
+ Note over W: first PDF renders
96
+
97
+ S->>F: admit cost 1
98
+ W->>F: select([5, 1])
99
+ F-->>W: tenant 1
100
+ Note over S: still seconds, not forty minutes
101
+ ```
102
+
103
+ ## How the Ledger Works
104
+
105
+ ### Admit
106
+
107
+ Before a job is enqueued, ask whether the tenant has room for its cost. The answer is a plain boolean, and `false` means the job doesn't go on the queue.
108
+
109
+ ```ruby
110
+ fair = FairMachines::TenantFair.new(capacities: [10, 5], quanta: [3, 1])
111
+
112
+ fair.admit(tenant: 1, cost: 5) # => true budget 5, now full
113
+ fair.admit(tenant: 1, cost: 1) # => false over budget
114
+ fair.admit(tenant: 2, cost: 1) # => false no such tenant
115
+ fair.tenant_count # => 2
116
+ ```
117
+
118
+ Over-budget is a refusal, not an error. Decide what to do with the job: reject it, hold it, tell the tenant.
119
+
120
+ ### Select
121
+
122
+ Each round, pass the head cost of every tenant's queue. You get back the tenant whose head runs now, or `nil` when nobody is eligible. The chosen tenant is charged that cost against its budget, so the room comes back as the work drains.
123
+
124
+ ```ruby
125
+ fair = FairMachines::TenantFair.new(capacities: [10, 5], quanta: [3, 1])
126
+ fair.admit(tenant: 0, cost: 4)
127
+ fair.admit(tenant: 1, cost: 1)
128
+
129
+ fair.select([4, 1]) # => 1
130
+ fair.select([4, nil]) # => 0
131
+ fair.select([4, nil]) # => nil tenant 0 has nothing left queued
132
+ fair.admit(tenant: 0, cost: 7) # => true the dispatched 4 freed its room
133
+ ```
134
+
135
+ Heads have to be truthful. A head that costs more than the tenant has admitted is not eligible and `select` returns `nil` for it.
136
+
137
+ ### Share is by cost, not by count
138
+
139
+ Quanta are how much cost each tenant may dispatch per round, with leftover carried to the next round. A tenant whose head is expensive waits until it has saved up. A tenant with cheap jobs runs one every round.
140
+
141
+ ```ruby
142
+ fair = FairMachines::TenantFair.new(capacities: [50, 50], quanta: [2, 2])
143
+ fair.admit(tenant: 0, cost: 40) # heavy jobs at cost 4
144
+ fair.admit(tenant: 1, cost: 10) # light jobs at cost 1
145
+
146
+ Array.new(10) { fair.select([4, 1]) }
147
+ # => [1, 1, 0, 1, 1, 1, 1, 0, 1, 1]
148
+ ```
149
+
150
+ Same quanta, same share of the pool, and the light tenant still dispatches four jobs for every one heavy job, because the heavy job costs four times as much.
151
+
152
+ Give one tenant a bigger quantum and it gets a bigger share of the pool outright:
153
+
154
+ ```ruby
155
+ fair = FairMachines::TenantFair.new(capacities: [20, 20], quanta: [3, 1])
156
+ fair.admit(tenant: 0, cost: 20)
157
+ fair.admit(tenant: 1, cost: 20)
158
+
159
+ Array.new(8) { fair.select([1, 1]) }
160
+ # => [0, 0, 0, 1, 0, 0, 0, 1]
161
+ ```
162
+
163
+ ### Cancel
164
+
165
+ A job that leaves the queue without being dispatched has to give its cost back, or the tenant's budget leaks until nothing is admitted.
166
+
167
+ ```ruby
168
+ fair = FairMachines::TenantFair.new(capacities: [10, 5], quanta: [3, 1])
169
+ fair.admit(tenant: 0, cost: 4)
170
+
171
+ fair.cancel(tenant: 0, cost: 4) # => true
172
+ fair.cancel(tenant: 0, cost: 1) # => false nothing left to cancel
173
+ fair.select([4, nil]) # => nil the queue is empty, and so is the ledger
174
+ ```
175
+
176
+ Cancelling more than was admitted is refused, and the ledger is untouched.
177
+
178
+ ### Too many tenants to name: `KeyedFair`
179
+
180
+ Sixteen tenant slots is a crew roster. Orgs, API keys and IP prefixes are a starfield. `KeyedFair` hashes each key onto a fixed set of lanes, every lane with its own budget and no queue. A loud org fills its lane and gets `nil`; the other lanes don't notice. Keys that hash to the same lane share its budget, so pick enough lanes that a collision is rare.
181
+
182
+ ```ruby
183
+ fair = FairMachines::KeyedFair.new(lanes: 64, capacity: 8)
184
+
185
+ lane = fair.admit(hash: org_id.hash, cost: 1) # => 17, or nil when that lane is full
186
+ # ... do the work ...
187
+ fair.release(lane:, cost: 1) # => true
188
+ ```
189
+
190
+ `lanes:` is a power of two from 1 to 256. Any Integer works as `hash:`, negatives included, so `key.hash` goes straight in.
191
+
192
+ ## The Rules of the Escort
193
+
194
+ - **1 to 16 tenants**, set by the length of `capacities:`. `quanta:` must be the same length. Empty, longer, or mismatched arrays raise `ArgumentError`.
195
+ - **Capacities, quanta, and costs are positive Integers** below `2**32`. Zero is `ArgumentError`, negative or too large is `RangeError`, floats and strings are `TypeError`. Nothing is rounded for you.
196
+ - **Heads are exactly `tenant_count` entries**, each a positive Integer or `nil`. The wrong length or a zero raises `ArgumentError`, and a float raises `TypeError`, before anything changes.
197
+ - **Bad tenant indices don't raise.** `admit` and `cancel` with an unknown tenant return `false`.
198
+ - **Frozen means frozen.** `freeze` the scheduler and `admit`, `cancel`, and `select` raise `FrozenError`. `tenant_count` still answers.
199
+ - **A tenant that drains to empty loses its saved-up share.** Leftover quantum doesn't accumulate for a tenant with nothing queued.
200
+
201
+ ## What You Think You're Doing vs Reality
202
+
203
+ ### You Think: "Round robin between tenants. Everybody gets a turn."
204
+ ### Reality: A turn is not a share. One tenant's turn is a thumbnail, another's is a 40-page PDF.
205
+
206
+ Taking turns by job count hands the pool to whoever submits the heaviest jobs. Fairness has to be measured in what a job costs, which is why every call here carries a cost and why the scheduler charges it before it hands out the next turn.
207
+
208
+ ## What's Yours
209
+
210
+ The scheduler is a ledger, not a queue. You keep a queue per tenant, call `admit` before you push, hand `select` the cost at the head of each queue, and pop the job from the tenant it names. Your cost estimates have to be honest, because the ledger believes them. And when a job dies on the floor without running, call `cancel`, or the tenant pays for it forever.
211
+
212
+ ## License
213
+
214
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
215
+
216
+ **Remember: The export job will finish. The question is whether the password reset has to wait for it.**
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.3.0](https://github.com/seuros/fair_machines/compare/fair-machines-v0.2.2...fair-machines-v0.3.0) (2026-11-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * add KeyedFair hashed-lane budgets ([33d8f62](https://github.com/seuros/fair_machines/commit/33d8f62d6c8f8e8ad8a9bc759dffbdfa8b7a2706))
9
+
10
+ ## [0.2.2](https://github.com/seuros/fair_machines/compare/fair-machines-v0.2.1...fair-machines-v0.2.2) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * crates.io keywords and categories for the core ([c9e496f](https://github.com/seuros/fair_machines/commit/c9e496fca380c51529e965ce4c8158f5210ed258))
16
+
17
+ ## [0.2.1](https://github.com/seuros/fair_machines/compare/fair-machines-v0.2.0...fair-machines-v0.2.1) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * keep test sources out of the packaged gem and crate ([58a408b](https://github.com/seuros/fair_machines/commit/58a408ba6727d04d8517a6b76c47b90e381203e3))
23
+
24
+ ## [0.2.0](https://github.com/seuros/fair_machines/compare/fair-machines-v0.1.0...fair-machines-v0.2.0) (2026-11-06)
25
+
26
+
27
+ ### Features
28
+
29
+ * initial extraction ([3c25da7](https://github.com/seuros/fair_machines/commit/3c25da7ef06236f290cef43e29ce5519cbf69aff))
@@ -0,0 +1,21 @@
1
+ # fair-machines
2
+
3
+ > **AMS Shreedhar**, NCC-4207, *Shreedhar*-class convoy escort.
4
+
5
+ Engine-room flight manual: share departure capacity without starving smaller crews.
6
+
7
+ Allocation-free `no_std` tenant isolation: independent cost-credit budgets for
8
+ admission, with DRR selection delegated to `priority-machines`. This is not a
9
+ second implementation of DRR. Admission accounts queued cost; selection returns
10
+ that cost to the tenant budget. Running work after selection is not bounded here.
11
+
12
+ Adapters own queues, present truthful head costs, and serialize admission,
13
+ selection, and cancellation. Aggregate credits do not identify individual jobs.
14
+ `cancel` returns admitted cost; when a tenant drains its DRR visit is reset.
15
+ Fairness assumes continuously backlogged tenants and meaningful cost estimates.
16
+ `KeyedFair<LANES>` covers an open tenant set: caller-hashed keys map onto fixed
17
+ lanes with independent credit budgets and no scheduler. Keys sharing a lane share
18
+ its budget.
19
+
20
+ Tenant hierarchies, per-flow WFQ, runtime preemption, and strict latency/starvation
21
+ bounds are not implemented.
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ffi/extconf'
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FairMachines
4
+ VERSION = '0.1.5'
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'fair_machines/version'
4
+ require 'fair_machines_native/fair_machines_native'
@@ -0,0 +1,18 @@
1
+ module FairMachines
2
+ VERSION: String
3
+ class TenantFair
4
+ def self.new: (capacities: Array[Integer], quanta: Array[Integer]) -> instance
5
+ def admit: (tenant: Integer, cost: Integer) -> bool
6
+ def cancel: (tenant: Integer, cost: Integer) -> bool
7
+ def select: (Array[Integer?] heads) -> Integer?
8
+ def tenant_count: () -> Integer
9
+ end
10
+ class KeyedFair
11
+ def self.new: (lanes: Integer, capacity: Integer) -> instance
12
+ def admit: (hash: Integer, cost: Integer) -> Integer?
13
+ def release: (lane: Integer, cost: Integer) -> bool
14
+ def in_flight: (Integer lane) -> Integer
15
+ def lane: (Integer hash) -> Integer
16
+ def lane_count: () -> Integer
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fair_machines
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: x86_64-linux
6
+ authors:
7
+ - Abdelkader Boudih
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-11-07 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Initial unstable Ruby adapter to the no_std fair-machines Rust core.
14
+ Local source builds require unpublished path-only flow and priority siblings.
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/fair_machines_native/core/CHANGELOG.md
25
+ - ext/fair_machines_native/core/README.md
26
+ - ext/fair_machines_native/extconf.rb
27
+ - lib/fair_machines.rb
28
+ - lib/fair_machines/version.rb
29
+ - lib/fair_machines_native/fair_machines_native.so
30
+ - sig/fair_machines.rbs
31
+ homepage: https://github.com/seuros/fair_machines
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ source_code_uri: https://github.com/seuros/fair_machines
36
+ bug_tracker_uri: https://github.com/seuros/fair_machines/issues
37
+ rubygems_mfa_required: 'true'
38
+ cargo_crate_name: fair_machines_native
39
+ cargo_manifest_path: ext/fair_machines_native/ffi/Cargo.toml
40
+ post_install_message:
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '4.0'
49
+ - - "<"
50
+ - !ruby/object:Gem::Version
51
+ version: 4.1.dev
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.5.23
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Native per-tenant admission budgets and deficit fairness
62
+ test_files: []