consensus_machines 0.1.4

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: d81e47113f1afec593db633c2afd4454e5cb9b41f9dd7e3cedf38993241751e4
4
+ data.tar.gz: 313fcdb207747f9d86fb8fa824483edc3037bb5fa6e742ce06690d4bb3b760f0
5
+ SHA512:
6
+ metadata.gz: d70d5251244cf6fea69323ae391a5287bd88be3dd5a6a57dc05784f92e5240c212d66d5ff262a0d8c13f135b51e7d952988e963b29f574ba3951fdf18b54a7a5
7
+ data.tar.gz: 5e4b7bb8dd15565e3ac245ea2caa234e7cb5fafed94d6e276004286c3b8713f89aac50e6804599fa150765d5b426aaab4e554372bb71c1cf9c5a9c3c16b612ff
data/CHANGELOG.md ADDED
@@ -0,0 +1,29 @@
1
+ # Changelog
2
+
3
+ ## [0.1.4](https://github.com/seuros/consensus_machines/compare/consensus_machines/v0.1.3...consensus_machines/v0.1.4) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * extract Wrapper::advance for node revision handling ([d09a5b1](https://github.com/seuros/consensus_machines/commit/d09a5b1e6f8f7f093be7f0b3c0ddcf6188417471))
9
+
10
+ ## [0.1.3](https://github.com/seuros/consensus_machines/compare/consensus_machines/v0.1.2...consensus_machines/v0.1.3) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * crates.io keywords and categories for the core ([3eacc56](https://github.com/seuros/consensus_machines/commit/3eacc564584d1a2485c2ce150db6673ffedfc698))
16
+
17
+ ## [0.1.2](https://github.com/seuros/consensus_machines/compare/consensus_machines/v0.1.1...consensus_machines/v0.1.2) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * keep test sources out of the packaged gem and crate ([6ef97ab](https://github.com/seuros/consensus_machines/commit/6ef97abf2cde36bf051e5919c3c32f76c32d989a))
23
+
24
+ ## [0.1.1](https://github.com/seuros/consensus_machines/compare/consensus_machines-v0.1.0...consensus_machines/v0.1.1) (2026-11-06)
25
+
26
+
27
+ ### Features
28
+
29
+ * initial extraction ([385650b](https://github.com/seuros/consensus_machines/commit/385650b7dc025c8d4494eadb1f4c59dcad67aa97))
data/Cargo.toml ADDED
@@ -0,0 +1,23 @@
1
+ [workspace]
2
+ members = ["ext/consensus_machines_native/core", "ext/consensus_machines_native/ffi"]
3
+ default-members = ["ext/consensus_machines_native/core"]
4
+ resolver = "3"
5
+ [workspace.package]
6
+ edition = "2024"
7
+ rust-version = "1.99"
8
+ license = "MIT"
9
+ [workspace.dependencies]
10
+ consensus-machines = { path = "ext/consensus_machines_native/core" }
11
+ magnus = { version = "0.9.3", default-features = false }
12
+ rb-sys = { version = "0.9.130", default-features = false, features = ["stable-api-compiled-fallback"] }
13
+ quorum-machines = ">=0.2"
14
+ [workspace.lints.rust]
15
+ unsafe_code = "deny"
16
+ unused_must_use = "warn"
17
+ missing_docs = "warn"
18
+ [workspace.lints.clippy]
19
+ all = { level = "warn", priority = -1 }
20
+ pedantic = { level = "warn", priority = -1 }
21
+ nursery = { level = "warn", priority = -1 }
22
+ unwrap_used = "deny"
23
+ 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 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,220 @@
1
+ # ConsensusMachines
2
+
3
+ > Three nodes can all claim to be in charge. The log only believes the one with the votes. 🛸
4
+
5
+ A Raft rulebook for Ruby. Each node holds a `Raft` object that knows the current term, who it voted for, what's in its log, and how far that log is committed. You feed it the messages your nodes exchange. It tells you how to answer, when you've become leader, and which entries a majority has safely stored. The wires, the timers, and the disk are yours.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'consensus_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ # Three members with ids 10, 20, 30. This process is member 0 (id 10).
15
+ node = ConsensusMachines::Raft.new([10, 20, 30], 0)
16
+
17
+ # Election timer fired. Start a term and vote for yourself.
18
+ node.begin_election # => [0, 1, 0, 0] send this RequestVote to the peers
19
+ node.receive_vote(1, 1, 1) # => "leader" member 1 said yes: that's a majority of 3
20
+
21
+ # Leader work: append an entry, replicate it, count the acknowledgements.
22
+ node.propose(42) # => 1 log index to replicate
23
+ node.acknowledge(1, 1, 1) # => true member 1 stored index 1 in term 1
24
+ node.committed # => 1 a majority has it; apply it
25
+ ```
26
+
27
+ Members are positional indices into the array you passed in. The second argument is which index you are.
28
+
29
+ ## A Message to the Resistance
30
+
31
+ So you ran two database replicas with a `PRIMARY=true` flag and a health check that promotes the standby when the primary stops answering. Then a switch rebooted, the standby couldn't see the primary for nine seconds, promoted itself, and took writes. The primary, which was fine the whole time, also took writes. Nine seconds later you had two histories and no rule for picking one.
32
+
33
+ The fix is older than your cluster. A node leads only for a numbered term, only after a majority of distinct members voted for it in that term, and a write counts only once a majority has stored it. Two leaders in one term is impossible because nobody votes twice. A leader from an old term gets told the new term number and steps down.
34
+
35
+ That's Raft. The rules are precise and the bookkeeping is unforgiving, and that bookkeeping is this gem. Your code still sends the messages and writes the disk. The gem never lets you count a vote twice or commit a write the cluster didn't keep.
36
+
37
+ ## Chapter 1: Stardate 2025.186, the AMS Ongaro
38
+
39
+ > **AMS Ongaro**, NCC-4204, *Ongaro*-class command vessel.
40
+ > Three bridges can speak. That does not make three orders authoritative.
41
+
42
+ The vessel doesn't broadcast. It keeps the ledger of command: which term it is, which bridge won it, which orders enough bridges have logged. An order from a bridge whose term has lapsed is logged nowhere.
43
+
44
+ Somewhere in the fleet, an ensign proposes:
45
+
46
+ ```ruby
47
+ def primary?
48
+ ENV['PRIMARY'] == 'true' || !ping(other_node)
49
+ end
50
+ ```
51
+
52
+ "That," says the commander, "is how two bridges end up giving orders to the same engine."
53
+
54
+ ## The Flight Plan
55
+
56
+ Every node is in one of three roles.
57
+
58
+ ```mermaid
59
+ stateDiagram-v2
60
+ [*] --> follower
61
+ follower --> candidate: begin_election, new term, votes for self
62
+ candidate --> candidate: receive_vote, not yet a majority
63
+ candidate --> leader: receive_vote, majority of distinct members
64
+ candidate --> follower: a message arrives with a higher term
65
+ leader --> leader: propose, acknowledge, committed advances
66
+ leader --> follower: a message arrives with a higher term
67
+ follower --> follower: request_vote, append_entries
68
+ ```
69
+
70
+ And the night the switch rebooted, on a three-member cluster:
71
+
72
+ ```mermaid
73
+ sequenceDiagram
74
+ participant A as Member 0
75
+ participant B as Member 1
76
+ participant C as Member 2
77
+
78
+ Note over A: election timer fires
79
+ A->>A: begin_election (term 1)
80
+ A->>B: RequestVote term 1
81
+ A->>C: RequestVote term 1
82
+ B->>B: request_vote → [1, true]
83
+ B-->>A: vote granted
84
+ A->>A: receive_vote(1, 1, 1) → "leader"
85
+ Note over A: 2 of 3 is a majority. C's vote is not needed.
86
+
87
+ A->>A: propose(42) → index 1
88
+ A->>B: AppendEntries [[1, 42]]
89
+ A->>C: AppendEntries [[1, 42]]
90
+ B->>B: append_entries → [1, 1]
91
+ B-->>A: stored index 1
92
+ A->>A: acknowledge(1, 1, 1) → true
93
+ Note over A: committed = 1. Apply entry 1.
94
+ A->>C: AppendEntries, committed 1
95
+ C->>C: append_entries → [1, 1], committed 1
96
+ ```
97
+
98
+ ## How Command Works
99
+
100
+ ### Becoming a candidate
101
+
102
+ `begin_election` bumps the term, votes for yourself, and hands you the four numbers the peers need: your member index, the term, and your last log index and term.
103
+
104
+ ```ruby
105
+ node = ConsensusMachines::Raft.new([10, 20, 30, 40, 50], 0)
106
+
107
+ node.begin_election # => [0, 1, 0, 0]
108
+ node.role # => "candidate"
109
+ node.term # => 1
110
+ ```
111
+
112
+ Send that as a RequestVote to every peer.
113
+
114
+ ### Answering a vote request
115
+
116
+ A peer feeds the request into its own node and sends back what it gets. One vote per term, to the first eligible candidate who asked.
117
+
118
+ ```ruby
119
+ peer = ConsensusMachines::Raft.new([10, 20, 30, 40, 50], 1)
120
+
121
+ peer.request_vote(0, 1, 0, 0) # => [1, true] member 0 gets the vote
122
+ peer.request_vote(2, 1, 0, 0) # => [1, false] member 2 asked too late
123
+ ```
124
+
125
+ ### Counting votes
126
+
127
+ `receive_vote` takes the peer's index, the term it voted in, and a ballot: 1 for yes, 0 for no. It returns your role afterwards. Repeats from the same member don't count twice.
128
+
129
+ ```ruby
130
+ node.receive_vote(1, 1, 1) # => "candidate" 1 of 5, plus yourself is 2
131
+ node.receive_vote(1, 1, 1) # => "candidate" same member, same vote
132
+ node.receive_vote(2, 1, 1) # => "leader" 3 distinct of 5
133
+ ```
134
+
135
+ ### Proposing and committing
136
+
137
+ Only a leader can `propose`. It returns the log index to replicate. An entry is committed when a majority of distinct members has acknowledged that index in the current term.
138
+
139
+ ```ruby
140
+ node.propose(99) # => 1
141
+ node.committed # => 0 nobody has it yet but you
142
+
143
+ node.acknowledge(1, 1, 1) # => true
144
+ node.committed # => 0 2 of 5 is not a majority
145
+ node.acknowledge(2, 1, 1) # => true
146
+ node.committed # => 1 3 of 5. Apply it.
147
+ node.entry(1) # => [1, 99]
148
+ ```
149
+
150
+ The thing you propose is an Integer id. Keep the actual payload in your own store, keyed by that id.
151
+
152
+ ### Replicating the log
153
+
154
+ Followers take `append_entries` with the leader's index, term, the index and term of the entry just before the new ones, the leader's commit index, and the entries as `[term, id]` pairs. The reply is the follower's term and the last index it now matches, or `nil` when the prefix didn't line up.
155
+
156
+ ```ruby
157
+ follower = ConsensusMachines::Raft.new([10, 20, 30, 40, 50], 1)
158
+
159
+ follower.append_entries(0, 1, 0, 0, 0, [[1, 99]]) # => [1, 1] stored index 1
160
+ follower.entry(1) # => [1, 99]
161
+ follower.append_entries(0, 1, 1, 2, 0, []) # => [1, nil] prefix mismatch, refused
162
+ follower.append_entries(0, 1, 1, 1, 1, []) # => [1, 1] heartbeat carrying commit 1
163
+ follower.committed # => 1
164
+ ```
165
+
166
+ A `nil` match means the leader has to back up and send from an earlier index.
167
+
168
+ ### Terms only go up
169
+
170
+ Any message carrying a higher term pulls the node into that term and back to follower. A leader from term 1 hearing about term 10 is a follower in term 10.
171
+
172
+ ```ruby
173
+ node.role # => "leader"
174
+ node.receive_vote(1, 10, 0)
175
+ node.term # => 10
176
+ node.role # => "follower"
177
+ ```
178
+
179
+ ### Checkpoints
180
+
181
+ `checkpoint` captures the node as it is right now. `restore!` puts it back, as a follower. A checkpoint is good only for the node that made it and only until that node changes again.
182
+
183
+ ```ruby
184
+ node = ConsensusMachines::Raft.new([10, 20, 30], 0)
185
+ node.begin_election
186
+ saved = node.checkpoint
187
+
188
+ node.restore!(saved) # => true
189
+ node.role # => "follower"
190
+ node.term # => 1
191
+ ```
192
+
193
+ Change the node and the old checkpoint is refused with `ArgumentError`. So is a checkpoint from a different node.
194
+
195
+ ## The Rules of the Vessel
196
+
197
+ - **One to nine members**, all with distinct Integer ids. Your own index must be one of them. A majority is more than half of the real members, never padded.
198
+ - **Ids, terms, indices, and proposals are Integers** in `0..2**64-1`. Floats and strings raise `TypeError`. Negative or oversized raise `RangeError`.
199
+ - **Ballots are 0 or 1.** Anything else is `ArgumentError`.
200
+ - **The log holds 64 entries.** `propose` returns `nil` when it's full, and also when you're not the leader. Nothing is evicted.
201
+ - **A single-member cluster commits on propose.** It is its own majority.
202
+ - **Bad input changes nothing.** Every argument is checked before any state moves. A malformed entries array raises `TypeError` and the node is exactly as it was.
203
+ - **Frozen means frozen.** `freeze` the node and every mutation, including `restore!`, raises `FrozenError`. Reads still work.
204
+
205
+ ## What You Think You're Doing vs Reality
206
+
207
+ ### You Think: "The leader wrote it, so it's written."
208
+ ### Reality: The leader wrote it to the leader.
209
+
210
+ Until a majority has acknowledged an index, that entry lives on one machine that could lose power before the next heartbeat. `committed` is the only number that means "safe to apply". Reading `last` and calling it durable is how you acknowledge a payment the cluster never kept.
211
+
212
+ ## What's Yours
213
+
214
+ The node decides. You deliver. Every `request_vote` reply, every `append_entries` reply, and every acknowledgement you fold in must have been written to disk on the node that produced it before it was sent, or a crash can make the cluster forget a vote it already cast. Run the election timer, the heartbeat, and the retries yourself. Authenticate the messages. And apply entries to your state machine only up to `committed`, in order, exactly once.
215
+
216
+ ## License
217
+
218
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
219
+
220
+ **Remember: The standby was sure it was primary. So was the primary. The log was only sure about one of them.**
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ ## [0.2.2](https://github.com/seuros/consensus_machines/compare/consensus-machines-v0.2.1...consensus-machines-v0.2.2) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * crates.io keywords and categories for the core ([3eacc56](https://github.com/seuros/consensus_machines/commit/3eacc564584d1a2485c2ce150db6673ffedfc698))
9
+
10
+ ## [0.2.1](https://github.com/seuros/consensus_machines/compare/consensus-machines-v0.2.0...consensus-machines-v0.2.1) (2026-11-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * keep test sources out of the packaged gem and crate ([6ef97ab](https://github.com/seuros/consensus_machines/commit/6ef97abf2cde36bf051e5919c3c32f76c32d989a))
16
+
17
+ ## [0.2.0](https://github.com/seuros/consensus_machines/compare/consensus-machines-v0.1.0...consensus-machines-v0.2.0) (2026-11-06)
18
+
19
+
20
+ ### Features
21
+
22
+ * initial extraction ([385650b](https://github.com/seuros/consensus_machines/commit/385650b7dc025c8d4494eadb1f4c59dcad67aa97))
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "consensus-machines"
3
+ version = "0.2.2"
4
+ edition.workspace = true
5
+ rust-version.workspace = true
6
+ license.workspace = true
7
+ description = "Bounded fixed-membership Raft terms, votes, log matching, and commitment model"
8
+ readme = "README.md"
9
+ exclude = ["src/**/tests.rs", "tests/"]
10
+ keywords = ["raft", "consensus", "quorum", "replication", "no-std"]
11
+ categories = ["no-std", "algorithms"]
12
+ [dependencies]
13
+ quorum-machines.workspace = true
14
+ [lints]
15
+ workspace = true
@@ -0,0 +1,53 @@
1
+ # consensus-machines
2
+
3
+ > **AMS Ongaro**, NCC-4204, *Ongaro*-class command vessel.
4
+
5
+ Engine-room flight manual: model a shared command log before wiring the radios.
6
+
7
+ Allocation-free `no_std` **bounded fixed-membership Raft transition model**.
8
+ It models role/term changes, one vote per term, log freshness, append matching,
9
+ uncommitted suffix replacement, distinct-voter elections, replication progress,
10
+ and majority commitment restricted to entries from the leader's current term.
11
+ Command payloads are immutable caller-defined `u64` command identities.
12
+
13
+ Indices are one-based (`0/term 0` is the empty-log sentinel). Peer indices refer
14
+ to one validated static membership. No dynamic membership or joint consensus.
15
+ A full fixed-size log rejects proposals; no compaction/snapshot installation yet.
16
+
17
+ ## Mandatory external safety contract
18
+
19
+ - Serialize node transitions and authenticate/correlate all messages to the same
20
+ cluster, configuration, term, and replication request. Quorum arithmetic alone
21
+ does not prove successful replication.
22
+ - **Durably commit `checkpoint()` after every state transition before exposing
23
+ its replies, broadcasting requests/entries, counting local persistence, or
24
+ applying committed commands.** The API returns proposed in-memory decisions;
25
+ it does not provide a persistence barrier.
26
+ - On storage uncertainty, stop and recover the latest committed checkpoint.
27
+ Never roll back terms, votes, or durable log history.
28
+ - Restore as follower. Keep timers, leader retry/next-index logic, heartbeat
29
+ scheduling, command-data storage, applied-index recovery, and client
30
+ deduplication in a tested runtime. Apply only committed commands, in order,
31
+ with an explicit crash-replay contract.
32
+
33
+ Typed checkpoints are opaque state transfers, not a stable disk/wire format.
34
+ No transport, election timeout randomization, pre-vote, leader lease/read-index,
35
+ linearizable read API, snapshotting, Byzantine protection, production liveness
36
+ claim, or Ruby binding is implemented. Do not deploy this model as a complete
37
+ distributed consensus service.
38
+
39
+ ```rust
40
+ use consensus_machines::{Raft, Role};
41
+ # fn example() -> Option<()> {
42
+ // A local single-member model, not a storage or transport implementation.
43
+ let mut node = Raft::<1, 8>::new([1], 0)?;
44
+ node.begin_election()?;
45
+ assert_eq!(node.role(), Role::Leader);
46
+ assert_eq!(node.propose(42), Some(1));
47
+ assert_eq!(node.committed(), 1);
48
+ let recovered = Raft::restore(node.checkpoint())?;
49
+ assert_eq!(recovered.role(), Role::Follower);
50
+ # Some(())
51
+ # }
52
+ # assert_eq!(example(), Some(()));
53
+ ```