election_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: aa1c7ec743a4e361a4179082de0fe1b71d27bdc9e090256a4cfd559539dad8ef
4
+ data.tar.gz: bbecd89e4cfe1d6b64997deecf118f12c54c802dcbb39c4882a442420b6b9c76
5
+ SHA512:
6
+ metadata.gz: 55483fd1e0158ff1a70e6c889ab4f5687502e33c651b77a3017a81ca93caf372fd4dbc25be1eee6679aed80d7ef99881725eb7b689bedb39ea9f4c3dca9d3a09
7
+ data.tar.gz: 37295fe37b35c5b95fb74b83aaf7b7bb8cc727c969422c0b1a62fe369df5ac42d0e9f5f3799ba9e8d8ce817908c63b6ce7a74e23aa9971444cb13190450cc38c
data/CHANGELOG.md ADDED
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## [0.1.5](https://github.com/seuros/election_machines/compare/election_machines/v0.1.4...election_machines/v0.1.5) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * extract Wrapper::advance for election revision handling ([09bdd65](https://github.com/seuros/election_machines/commit/09bdd65226a7780bfe980bf19e5145ee2c2f1da0))
9
+
10
+ ## [0.1.4](https://github.com/seuros/election_machines/compare/election_machines/v0.1.3...election_machines/v0.1.4) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * require lease-machines 0.3 ([d99d292](https://github.com/seuros/election_machines/commit/d99d2928ba0dd42e3013eb2d13e9a527387703ee))
16
+
17
+ ## [0.1.3](https://github.com/seuros/election_machines/compare/election_machines/v0.1.2...election_machines/v0.1.3) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * crates.io keywords and categories for the core ([8c6d66c](https://github.com/seuros/election_machines/commit/8c6d66cc9ff19907ad68dc4043c9e421a66037ad))
23
+
24
+ ## [0.1.2](https://github.com/seuros/election_machines/compare/election_machines/v0.1.1...election_machines/v0.1.2) (2026-11-06)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * keep test sources out of the packaged gem and crate ([27f00f1](https://github.com/seuros/election_machines/commit/27f00f1331d641a7d324ee7e560324a99f958bfa))
30
+
31
+ ## [0.1.1](https://github.com/seuros/election_machines/compare/election_machines-v0.1.0...election_machines/v0.1.1) (2026-11-06)
32
+
33
+
34
+ ### Features
35
+
36
+ * initial extraction ([a804dd3](https://github.com/seuros/election_machines/commit/a804dd3a3c37b0f96acec918e7242475a63cd0c4))
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 copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ 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,212 @@
1
+ # ElectionMachines
2
+
3
+ > One captain at a time, and silence from the bridge is not a vacancy. 📡
4
+
5
+ Leader election for Ruby, backed by a lease. You hand in the list of workers that showed up, it names one of them captain and gives them a lease with a token. The captain keeps the chair by heartbeating before the lease runs out. Everyone else is told who the incumbent is. Nobody gets the chair by shouting louder, and a captain who stops answering loses it only when the clock says so.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'election_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ election = ElectionMachines::Election.new
15
+
16
+ # Three workers report in. The lowest id takes the chair.
17
+ status, leader, token, expires_at = election.elect([9, 2, 5], clock.now, 30.0)
18
+ # => ["elected", 2, 1, 30.0]
19
+
20
+ # The leader keeps the chair by heartbeating with its token.
21
+ election.heartbeat(2, token, clock.now, 30.0)
22
+ # => ["renewed", 2, 1, ...]
23
+
24
+ # Anyone else asking while the lease is live just learns who's in charge.
25
+ election.elect([5], clock.now, 30.0)
26
+ # => ["incumbent", 2, 1, ...]
27
+ ```
28
+
29
+ Every call returns the same four-element shape: a status string, then the leader, token, and expiry, or three nils when there's nothing to report.
30
+
31
+ ## A Message to the Resistance
32
+
33
+ So you have three schedulers and only one of them should be ticking the cron table. You picked "lowest hostname wins" and it worked for a year. Then the lowest hostname hit a network partition for ninety seconds. The other two couldn't reach it, decided it was dead, and the second-lowest took over. The first one never knew. For ninety seconds you had two captains, and your billing job ran twice.
34
+
35
+ "It stopped answering" and "it's gone" are different facts. The first is what you observed. The second is what you assumed. The gap between them is where the double-charge lives.
36
+
37
+ An election built on a lease closes that gap. The leader holds the chair until a stated time, not until someone stops hearing from it. If it's quiet but the lease is live, the answer to "can I take over?" is still no. When the lease lapses, the next election hands out a new token, and the old captain's token stops working everywhere it's checked.
38
+
39
+ ## Chapter 1: Stardate 2025.186, the AMS LeLann
40
+
41
+ > **AMS LeLann**, NCC-4206, *LeLann*-class command relay.
42
+ > An unanswered hail is not permission to appoint a second captain.
43
+
44
+ The relay doesn't fly the ship. It keeps the roster and the chair. When a crew reports for duty, the relay names the lowest-numbered one captain and stamps a lease. Captains renew by hailing with their token. A crew that hails about an empty chair gets it. A crew that hails about an occupied one is told who sits there, and nothing else.
45
+
46
+ Somewhere on a lower deck, a lieutenant proposes:
47
+
48
+ ```ruby
49
+ def am_i_leader?
50
+ peers.none? { |peer| peer.alive? && peer.id < my_id }
51
+ end
52
+ ```
53
+
54
+ "That," says the relay officer, "is how a ship ends up with two captains during a comms blackout."
55
+
56
+ ## The Flight Plan
57
+
58
+ ```mermaid
59
+ stateDiagram-v2
60
+ [*] --> vacant
61
+ vacant --> held: elect, lowest candidate wins, token n
62
+ held --> held: heartbeat by leader with token, renewed
63
+ held --> held: elect by anyone before expiry, incumbent
64
+ held --> vacant: resign by leader with token, released
65
+ held --> held: lease lapses, next elect names a new leader with token n+1
66
+ ```
67
+
68
+ And the ninety seconds the cron leader went quiet:
69
+
70
+ ```mermaid
71
+ sequenceDiagram
72
+ participant A as Scheduler A (id 1)
73
+ participant E as Election
74
+ participant B as Scheduler B (id 2)
75
+
76
+ A->>E: elect([1, 2], t=0, ttl=30)
77
+ E-->>A: elected, leader 1, token 1, expires 30
78
+ Note over A: ticks the cron table, heartbeats every 10s
79
+ A->>E: heartbeat(1, token 1, t=10, ttl=30)
80
+ E-->>A: renewed, expires 40
81
+
82
+ Note over A: partitioned at t=12, silent
83
+
84
+ B->>E: elect([2], t=20, ttl=30)
85
+ E-->>B: incumbent, leader 1, token 1, expires 40
86
+ Note over B: lease still live. B waits.
87
+
88
+ B->>E: elect([2], t=45, ttl=30)
89
+ E-->>B: elected, leader 2, token 2, expires 75
90
+ Note over B: now ticks the cron table
91
+
92
+ Note over A: partition heals at t=50
93
+ A->>E: heartbeat(1, token 1, t=50, ttl=30)
94
+ E-->>A: WrongOwner
95
+ Note over A: stands down. One captain the whole time.
96
+ ```
97
+
98
+ ## How the Relay Works
99
+
100
+ ### Elect
101
+
102
+ Pass the ids of the workers that are present, the current time, and how long the lease should last. The lowest id wins. The token starts at 1 and only ever goes up.
103
+
104
+ ```ruby
105
+ election = ElectionMachines::Election.new
106
+
107
+ election.elect([9, 2, 5], 0, 10) # => ["elected", 2, 1, 10.0]
108
+ election.lease # => [2, 1, 10.0]
109
+ ```
110
+
111
+ Ask again while that lease is live and you get the incumbent, even if you offer a lower id. Sitting captains aren't unseated by a late arrival.
112
+
113
+ ```ruby
114
+ election.elect([1], 1, 10) # => ["incumbent", 2, 1, 10.0]
115
+ ```
116
+
117
+ An empty roster is `no_candidates`.
118
+
119
+ ### Heartbeat
120
+
121
+ The leader renews by presenting its id and token. The new expiry is counted from `now`.
122
+
123
+ ```ruby
124
+ election = ElectionMachines::Election.new
125
+ election.elect([2], 0, 10)
126
+
127
+ election.heartbeat(2, 1, 3, 10) # => ["renewed", 2, 1, 13.0]
128
+ election.heartbeat(1, 1, 4, 10) # => ["WrongOwner", nil, nil, nil]
129
+ ```
130
+
131
+ A heartbeat from anyone but the leader is refused and changes nothing.
132
+
133
+ ### Resign
134
+
135
+ A leader steps down by presenting its token. The chair is vacant immediately, and the next election fills it.
136
+
137
+ ```ruby
138
+ election.resign(2, 1, 5) # => ["released", nil, nil, nil]
139
+ election.lease # => nil
140
+ election.elect([7, 3], 6, 10) # => ["elected", 3, 2, 16.0]
141
+ ```
142
+
143
+ Token 2. The old token is dead from here on.
144
+
145
+ ### When the leader goes quiet
146
+
147
+ Nothing happens at the moment of expiry. The chair is only re-filled when someone asks. Until then, `lease` still describes the lapsed lease, so you can see who was last in charge.
148
+
149
+ ```ruby
150
+ election = ElectionMachines::Election.new
151
+ election.elect([2], 0, 10)
152
+
153
+ election.heartbeat(2, 1, 11, 10) # => ["Expired", nil, nil, nil]
154
+ election.lease # => [2, 1, 10.0]
155
+
156
+ election.elect([5], 12, 10) # => ["elected", 5, 2, 22.0]
157
+ election.heartbeat(2, 1, 13, 10) # => ["WrongOwner", nil, nil, nil]
158
+ ```
159
+
160
+ The old leader's late heartbeat is rejected with `WrongOwner`. Its token is also stale, and `StaleToken` is what you'd see if it had the right id and the wrong token.
161
+
162
+ ### Time only moves forward
163
+
164
+ You supply `now`. The election remembers the latest it has seen and refuses to go backwards.
165
+
166
+ ```ruby
167
+ election = ElectionMachines::Election.new
168
+ election.elect([2], 5, 10)
169
+ election.elect([2], 0, 10) # => ["ClockRegressed", nil, nil, nil]
170
+ ```
171
+
172
+ Feed it a monotonic clock. Not the wall clock.
173
+
174
+ ## Checkpoints
175
+
176
+ An election can hand you a `Checkpoint` and roll back to it, but only itself, and only to the most recent one.
177
+
178
+ ```ruby
179
+ election = ElectionMachines::Election.new
180
+ election.elect([1], 0, 10)
181
+ saved = election.checkpoint
182
+
183
+ election.heartbeat(1, 1, 1, 10) # => ["renewed", 1, 1, 11.0]
184
+ election.restore!(saved) # => ArgumentError, a transition happened since
185
+ ```
186
+
187
+ Any successful transition, or any successful restore, invalidates every checkpoint taken before it. That rules out rewinding past a decision someone has already acted on. A checkpoint from one election can't be fed to another. This is for testing and in-process simulation, not for surviving a restart.
188
+
189
+ ## The Rules of the Relay
190
+
191
+ - **Ids and tokens are Integers** in `0..2**64-1`. Up to 64 candidates per election. Floats and strings raise `TypeError`, out of range raises `RangeError`, a 65th candidate raises `ArgumentError`.
192
+ - **Times are seconds** as Integer or Float, from a monotonic origin you choose. `ttl` must be positive. Negative, infinite, NaN, or absurdly large values raise `ArgumentError`.
193
+ - **Rejections don't raise.** `WrongOwner`, `StaleToken`, `Expired`, `ClockRegressed`, and `no_candidates` all come back as a status with three nils. Only malformed input raises.
194
+ - **Bad input never half-applies.** Arguments are checked before anything changes.
195
+ - **Frozen means frozen.** `freeze` the election and `elect`, `heartbeat`, `resign`, and `restore!` raise `FrozenError`. `lease` still reads.
196
+
197
+ ## What You Think You're Doing vs Reality
198
+
199
+ ### You Think: "If I can't reach the leader, the leader is down."
200
+ ### Reality: You can't reach the leader. That's all you know.
201
+
202
+ A partition looks identical to a crash from the outside. If your election promotes on silence, every network hiccup is a coup. If it promotes on lease expiry, the worst case is a leaderless gap of one ttl, which is boring, and boring is what you want from the thing that decides who runs billing.
203
+
204
+ ## What's Yours
205
+
206
+ The election decides. It doesn't talk to the network, run timers, or store anything across a restart. Run it in one place, write its decisions down before anyone acts on them, and make the thing the leader controls check the token before every action. A token the resource has already seen beaten is a captain who has already been replaced.
207
+
208
+ ## License
209
+
210
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
211
+
212
+ **Remember: Scheduler A was never dead. It just couldn't hear you say it was.**
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [0.2.2](https://github.com/seuros/election_machines/compare/election-machines-v0.2.1...election-machines-v0.2.2) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([8c6d66c](https://github.com/seuros/election_machines/commit/8c6d66cc9ff19907ad68dc4043c9e421a66037ad))
9
+
10
+ ## [0.2.1](https://github.com/seuros/election_machines/compare/election-machines-v0.2.0...election-machines-v0.2.1) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([27f00f1](https://github.com/seuros/election_machines/commit/27f00f1331d641a7d324ee7e560324a99f958bfa))
16
+
17
+ ## [0.2.0](https://github.com/seuros/election_machines/compare/election-machines-v0.1.0...election-machines-v0.2.0) (2026-11-06)
18
+
19
+
20
+ ### Features
21
+
22
+ * initial extraction ([a804dd3](https://github.com/seuros/election_machines/commit/a804dd3a3c37b0f96acec918e7242475a63cd0c4))
@@ -0,0 +1,17 @@
1
+ # election-machines
2
+
3
+ > **AMS LeLann**, NCC-4206, *LeLann*-class command relay.
4
+
5
+ Engine-room flight manual: model command handovers through one lease authority.
6
+
7
+ `no_std` lease-backed owner nomination under one authoritative history.
8
+ Among participating candidates, the smallest numeric ID is nominated. An
9
+ unexpired incumbent remains leader even if absent from the current nomination
10
+ list; this list is not a consensus membership configuration.
11
+
12
+ All lease-machine persistence, fencing, authentication, and clock requirements
13
+ apply. A fresh election instance is a fresh namespace, not failover recovery.
14
+ Empty candidate lists make no authority transition. Heartbeats renew a lease;
15
+ they are not an asynchronous failure detector. This policy is **not Raft,
16
+ quorum voting, or partition-safe leader election without an external serialized
17
+ durable authority**. Raft elections remain in `consensus-machines`.
@@ -0,0 +1,2 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'ffi/extconf'
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module ElectionMachines
3
+ VERSION = '0.1.5'
4
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'election_machines/version'
3
+ require 'election_machines_native/election_machines_native'
@@ -0,0 +1,15 @@
1
+ module ElectionMachines
2
+ VERSION: String
3
+ type outcome = [String, Integer?, Integer?, Float?]
4
+ class Checkpoint
5
+ end
6
+ class Election
7
+ def self.new: () -> Election
8
+ def elect: (Array[Integer], Integer | Float, Integer | Float) -> outcome
9
+ def heartbeat: (Integer, Integer, Integer | Float, Integer | Float) -> outcome
10
+ def resign: (Integer, Integer, Integer | Float) -> outcome
11
+ def checkpoint: () -> Checkpoint
12
+ def restore!: (Checkpoint) -> bool
13
+ def lease: () -> [Integer, Integer, Float]?
14
+ end
15
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: election_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-06 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: A thin Rust bridge for deterministic nomination and lease renewal. No
14
+ distributed election service.
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/election_machines_native/core/CHANGELOG.md
25
+ - ext/election_machines_native/core/README.md
26
+ - ext/election_machines_native/extconf.rb
27
+ - lib/election_machines.rb
28
+ - lib/election_machines/version.rb
29
+ - lib/election_machines_native/election_machines_native.so
30
+ - sig/election_machines.rbs
31
+ homepage: https://github.com/seuros/election_machines
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ source_code_uri: https://github.com/seuros/election_machines
36
+ bug_tracker_uri: https://github.com/seuros/election_machines/issues
37
+ rubygems_mfa_required: 'true'
38
+ cargo_crate_name: election_machines_native
39
+ cargo_manifest_path: ext/election_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 bounded lease-backed nomination model for Ruby
62
+ test_files: []