lease_machines 0.1.6-aarch64-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: f58d55d1036fccfec406afa28a9fe41d7377ce663f3681d6b3b2025b808051c1
4
+ data.tar.gz: 3265fb1457c7f2a3f31fdb3288d9c6ddcab6876c80cdfbe55780fdd78e7c8a9b
5
+ SHA512:
6
+ metadata.gz: 18354dad967f824971679f976b311f033ae4d038be40a7ce887f2c6536f55a635600ed367aee68f54eba21517cc5a01af9c86709f7854bf29e903b10acfb42f5
7
+ data.tar.gz: 275b0b17670661c0c76ff4efcc479028800708816c07eaecc19010e6091c80d19274dacd8f9c8ba242cea11f825df61f24d782865a8e0884cdf3d0350b4f4075
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ ## [0.1.6](https://github.com/seuros/lease_machines/compare/lease_machines/v0.1.5...lease_machines/v0.1.6) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * share lease renew/release checks in authority ([3c44626](https://github.com/seuros/lease_machines/commit/3c446267e004228b03df335e577f35b19b0c645f))
9
+
10
+ ## [0.1.5](https://github.com/seuros/lease_machines/compare/lease_machines/v0.1.4...lease_machines/v0.1.5) (2026-11-06)
11
+
12
+
13
+ ### Features
14
+
15
+ * rebuild authorities from storage and model the holder side ([5fe2199](https://github.com/seuros/lease_machines/commit/5fe2199af3efca8666f5015511649c802d95b77f))
16
+
17
+ ## [0.1.4](https://github.com/seuros/lease_machines/compare/lease_machines/v0.1.3...lease_machines/v0.1.4) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * crates.io keywords and categories for the core ([c4f147f](https://github.com/seuros/lease_machines/commit/c4f147f9cb2d69bac209d2d1c50e851f5137ff80))
23
+
24
+ ## [0.1.3](https://github.com/seuros/lease_machines/compare/lease_machines/v0.1.2...lease_machines/v0.1.3) (2026-11-06)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * keep test sources out of the packaged gem and crate ([6f0b979](https://github.com/seuros/lease_machines/commit/6f0b979351f1fe456d7ae922203f1ec69274d70d))
30
+
31
+ ## [0.1.2](https://github.com/seuros/lease_machines/compare/lease_machines/v0.1.1...lease_machines/v0.1.2) (2026-11-06)
32
+
33
+
34
+ ### Bug Fixes
35
+
36
+ * cross builds no longer compile for the host ([e7571a9](https://github.com/seuros/lease_machines/commit/e7571a988886534052055c05b31d97a686aaeca7))
37
+
38
+ ## [0.1.1](https://github.com/seuros/lease_machines/compare/lease_machines-v0.1.0...lease_machines/v0.1.1) (2026-11-06)
39
+
40
+
41
+ ### Features
42
+
43
+ * initial extraction ([243e8f6](https://github.com/seuros/lease_machines/commit/243e8f642f63cc2dd61a61524de0f45cd858ab96))
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # LeaseMachines
2
+
3
+ > Expiry tells the next worker it may start. The fence tells the last one it must stop. 🔑
4
+
5
+ Time-limited ownership for Ruby. One `Authority` decides who holds a resource and until when. Every grant comes with a token that only ever goes up. A `Fence` at the resource turns away any token older than the last one it saw. That's how a worker that fell asleep holding the key finds out, on waking, that it's no longer the captain.
6
+
7
+ ## Quick Start
8
+
9
+ ```bash
10
+ gem 'lease_machines'
11
+ ```
12
+
13
+ ```ruby
14
+ authority = LeaseMachines::Authority.new
15
+
16
+ grant = authority.acquire(owner: worker.id, now: clock.now, ttl: 30.0)
17
+ grant.status # => :granted
18
+ grant.lease.token # => 1
19
+
20
+ # ... do the work, renewing before the ttl runs out ...
21
+ authority.renew(owner: worker.id, token: grant.lease.token, now: clock.now, ttl: 30.0)
22
+
23
+ # ... and hand it back when done.
24
+ authority.release(owner: worker.id, token: grant.lease.token, now: clock.now)
25
+ ```
26
+
27
+ At the resource, keep a fence and show it every token before you touch anything:
28
+
29
+ ```ruby
30
+ fence = LeaseMachines::Fence.new
31
+
32
+ fence.accept(1) # => true, first key seen
33
+ fence.accept(2) # => true, newer key
34
+ fence.accept(1) # => false, that captain is retired
35
+ ```
36
+
37
+ ## A Message to the Resistance
38
+
39
+ So you put a lock in Redis. `SET nightly_report_lock worker-7 NX EX 30`. One worker runs the report, the others back off, everyone goes home.
40
+
41
+ Then worker 7 hits a stop-the-world GC pause for forty seconds. The lock expires. Worker 12 takes it and starts writing the report. Worker 7 wakes up, has no idea anything happened, and finishes writing *its* report over the top of 12's.
42
+
43
+ The lock did its job. It expired on time. The problem is that expiry only tells the *next* owner it can start. It never tells the *old* owner it has to stop, because the old owner isn't listening. It's busy.
44
+
45
+ The only thing that works is a number. Every grant gets a bigger number than the last one, and the thing being protected refuses any number smaller than the biggest it has seen. Worker 7 shows up with token 1. The report store has already seen token 2. Door stays shut.
46
+
47
+ This gem is that number, and the authority that hands it out.
48
+
49
+ ## Chapter 1: Stardate 2025.186, the AMS Cheriton
50
+
51
+ > **AMS Cheriton**, NCC-4211, *Cheriton*-class authority tender.
52
+ > An old captain's key must not open the engine room after a new command takes over.
53
+
54
+ The tender doesn't guard the engine room. It issues keys, each stamped with a higher serial than the last, each valid until a stated time. The engine room hatch keeps its own record of the highest serial it has ever accepted. A captain who went out of radio range and came back still holding serial 1 finds a hatch that has already seen serial 2.
55
+
56
+ Somewhere in the fleet, an ensign proposes:
57
+
58
+ ```ruby
59
+ def run_nightly_report
60
+ return unless redis.set("lock", id, nx: true, ex: 30)
61
+ generate_report # takes "about 20 seconds", usually
62
+ redis.del("lock")
63
+ end
64
+ ```
65
+
66
+ "That," says the quartermaster, "is how two captains end up writing the same log."
67
+
68
+ ## The Flight Plan
69
+
70
+ ```mermaid
71
+ stateDiagram-v2
72
+ [*] --> vacant
73
+ vacant --> held: acquire, granted (token n)
74
+ held --> held: renew by owner with current token
75
+ held --> vacant: release by owner with current token
76
+ held --> vacant: acquire by anyone after expiry (token n+1)
77
+ held --> held: acquire by another owner before expiry, occupied
78
+ ```
79
+
80
+ And the night worker 7 fell asleep:
81
+
82
+ ```mermaid
83
+ sequenceDiagram
84
+ participant W7 as Worker 7
85
+ participant A as Authority
86
+ participant W12 as Worker 12
87
+ participant F as Fence at report store
88
+
89
+ W7->>A: acquire(owner 7, ttl 30)
90
+ A-->>W7: granted, token 1
91
+ W7->>F: accept(1)
92
+ F-->>W7: true
93
+ Note over W7: starts writing... then stalls for 40s
94
+
95
+ W12->>A: acquire(owner 12) at t=35
96
+ A-->>W12: granted, token 2 (lease 1 expired)
97
+ W12->>F: accept(2)
98
+ F-->>W12: true
99
+ Note over W12: writes the report
100
+
101
+ Note over W7: wakes up, still holding token 1
102
+ W7->>F: accept(1)
103
+ F-->>W7: false
104
+ Note over F: highest seen is 2. Door stays shut.
105
+ ```
106
+
107
+ ## How Authority Works
108
+
109
+ ### Acquire
110
+
111
+ ```ruby
112
+ authority = LeaseMachines::Authority.new
113
+
114
+ first = authority.acquire(owner: 1, now: 0.0, ttl: 2.0)
115
+ first.status # => :granted
116
+ first.lease.owner # => 1
117
+ first.lease.token # => 1
118
+ first.lease.expires_at # => 2.0
119
+ ```
120
+
121
+ Every call returns a `Decision` with `status`, `lease`, and `reason`. A grant has a lease and no reason. A rejection has a reason and no lease.
122
+
123
+ ```ruby
124
+ authority.acquire(owner: 2, now: 1.0, ttl: 2.0).reason # => :occupied
125
+ ```
126
+
127
+ Owner 2 asked at second 1. Owner 1's lease runs to second 2. Occupied. Ask again after it lapses and the answer changes:
128
+
129
+ ```ruby
130
+ later = authority.acquire(owner: 2, now: 2.0, ttl: 2.0)
131
+ later.status # => :granted
132
+ later.lease.token # => 2
133
+ ```
134
+
135
+ Token 2. Always bigger. Never reused.
136
+
137
+ ### Renew
138
+
139
+ The holder extends the lease by presenting its token. The new expiry is counted from `now`, not from the old expiry.
140
+
141
+ ```ruby
142
+ authority = LeaseMachines::Authority.new
143
+ grant = authority.acquire(owner: 1, now: 0.0, ttl: 2.0)
144
+
145
+ renewed = authority.renew(owner: 1, token: 1, now: 1.0, ttl: 3.0)
146
+ renewed.status # => :renewed
147
+ renewed.lease.expires_at # => 4.0
148
+ ```
149
+
150
+ Renew with a token the authority didn't issue, or with the wrong owner, and you get a reason instead:
151
+
152
+ ```ruby
153
+ authority.renew(owner: 1, token: 2, now: 1.0, ttl: 1.0).reason # => :stale_token
154
+ ```
155
+
156
+ ### Release
157
+
158
+ ```ruby
159
+ authority.release(owner: 1, token: 1, now: 2.0).status # => :released
160
+ ```
161
+
162
+ Release after the lease already lapsed and the authority tells you so:
163
+
164
+ ```ruby
165
+ authority = LeaseMachines::Authority.new
166
+ authority.acquire(owner: 1, now: 0.0, ttl: 1.0)
167
+ authority.release(owner: 1, token: 1, now: 3.0).reason # => :expired
168
+ ```
169
+
170
+ ### Time only moves forward
171
+
172
+ You supply `now`. The authority remembers the latest `now` it has seen and refuses to go backwards.
173
+
174
+ ```ruby
175
+ authority = LeaseMachines::Authority.new
176
+ authority.acquire(owner: 1, now: 2.0, ttl: 1.0)
177
+ authority.acquire(owner: 2, now: 1.0, ttl: 1.0).reason # => :clock_regressed
178
+ ```
179
+
180
+ Feed it a monotonic clock, not the wall clock. A `ttl` of zero is `:invalid_ttl`.
181
+
182
+ ## The Fence
183
+
184
+ The authority decides who *should* have the resource. The fence is what the resource actually checks. Keep one next to the thing you're protecting and call `accept` with the token before every mutation. `highest` tells you the high-water mark it's enforcing.
185
+
186
+ Restarted the resource and remembered that mark? Start the fence from there:
187
+
188
+ ```ruby
189
+ fence = LeaseMachines::Fence.new(highest: 2)
190
+ fence.accept(2) # => true, equal is fine
191
+ fence.accept(1) # => false
192
+ fence.highest # => 2
193
+ ```
194
+
195
+ `accept` says nothing about expiry or authenticity. It only answers one question: have I seen a newer key than this one? That question is the whole point.
196
+
197
+ ## Checkpoints
198
+
199
+ An authority can hand you a `Checkpoint` and later be rebuilt from it. The rebuilt authority remembers the highest token it ever issued, so the serial numbers keep climbing across the restart.
200
+
201
+ ```ruby
202
+ authority = LeaseMachines::Authority.new
203
+ authority.acquire(owner: 1, now: 0.0, ttl: 1.0)
204
+
205
+ checkpoint = authority.checkpoint
206
+ checkpoint.last_issued # => 1
207
+
208
+ restored = LeaseMachines::Authority.restore(checkpoint)
209
+ restored.acquire(owner: 2, now: 1.0, ttl: 1.0).lease.token # => 2
210
+ ```
211
+
212
+ Stop the old authority before you start the one you restored. Two authorities for one resource is two captains.
213
+
214
+ ## The Rules of the Tender
215
+
216
+ - **Owners and tokens are Integers** in `0..2**64-1`. Tokens start at 1. Floats raise `TypeError`, out-of-range raises `RangeError`.
217
+ - **Times are Floats** in seconds from a monotonic origin you choose. Negative, infinite, or NaN raise `ArgumentError`.
218
+ - **Rejections don't raise.** Wrong owner, stale token, expired lease, clock regression, bad ttl: all come back as a `Decision` with a `reason`. Only malformed input raises.
219
+ - **Frozen means frozen.** `freeze` an authority or a fence and every mutation raises `FrozenError`.
220
+
221
+ ## What You Think You're Doing vs Reality
222
+
223
+ ### You Think: "The lock has a TTL, so stale holders can't hurt anyone."
224
+ ### Reality: A stale holder doesn't know it's stale.
225
+
226
+ Expiry frees the resource for the next owner. It does nothing to the previous one, who may be mid-write, paused, or partitioned, and will finish whatever it was doing the moment it can. The fence is what stops that write. Without it the TTL is a courtesy, not a guarantee.
227
+
228
+ ## What's Yours
229
+
230
+ The authority is a decision engine, not a server. Run one per resource, make sure its decisions are written down before anyone acts on them, and feed it a clock that never runs backwards. Put the fence as close to the protected data as you can get it, ideally in the same transaction as the write.
231
+
232
+ ## License
233
+
234
+ MIT License. See [LICENSE.txt](LICENSE.txt) for details.
235
+
236
+ **Remember: Worker 7 is still out there, still holding token 1, still sure it's the captain.**
@@ -0,0 +1,36 @@
1
+ # Changelog
2
+
3
+ ## [0.3.1](https://github.com/seuros/lease_machines/compare/lease-machines-v0.3.0...lease-machines-v0.3.1) (2026-11-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * share lease renew/release checks in authority ([3c44626](https://github.com/seuros/lease_machines/commit/3c446267e004228b03df335e577f35b19b0c645f))
9
+
10
+ ## [0.3.0](https://github.com/seuros/lease_machines/compare/lease-machines-v0.2.2...lease-machines-v0.3.0) (2026-11-06)
11
+
12
+
13
+ ### Features
14
+
15
+ * rebuild authorities from storage and model the holder side ([5fe2199](https://github.com/seuros/lease_machines/commit/5fe2199af3efca8666f5015511649c802d95b77f))
16
+
17
+ ## [0.2.2](https://github.com/seuros/lease_machines/compare/lease-machines-v0.2.1...lease-machines-v0.2.2) (2026-11-06)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * crates.io keywords and categories for the core ([c4f147f](https://github.com/seuros/lease_machines/commit/c4f147f9cb2d69bac209d2d1c50e851f5137ff80))
23
+
24
+ ## [0.2.1](https://github.com/seuros/lease_machines/compare/lease-machines-v0.2.0...lease-machines-v0.2.1) (2026-11-06)
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * keep test sources out of the packaged gem and crate ([6f0b979](https://github.com/seuros/lease_machines/commit/6f0b979351f1fe456d7ae922203f1ec69274d70d))
30
+
31
+ ## [0.2.0](https://github.com/seuros/lease_machines/compare/lease-machines-v0.1.0...lease-machines-v0.2.0) (2026-11-06)
32
+
33
+
34
+ ### Features
35
+
36
+ * initial extraction ([243e8f6](https://github.com/seuros/lease_machines/commit/243e8f642f63cc2dd61a61524de0f45cd858ab96))
@@ -0,0 +1,172 @@
1
+ # lease-machines
2
+
3
+ > **AMS Cheriton**, NCC-4211, *Cheriton*-class authority tender.
4
+
5
+ Engine-room flight manual: model expiring command keys and resource-side fencing.
6
+
7
+ A deterministic **single-authority decision model**, not a distributed lock,
8
+ failure detector, consensus protocol, or storage backend.
9
+
10
+ ## Lease Lifecycle
11
+
12
+ ```rust
13
+ use core::time::Duration;
14
+ use lease_machines::{Decision, LeaseAuthority, OwnerId, Rejection};
15
+
16
+ let mut authority = LeaseAuthority::new();
17
+ let first = authority.acquire(
18
+ OwnerId::new(1), Duration::ZERO, Duration::from_secs(5),
19
+ );
20
+ let Decision::Granted(lease) = first else {
21
+ panic!("a fresh authority should grant");
22
+ };
23
+ assert_eq!(
24
+ authority.acquire(OwnerId::new(2), Duration::from_secs(1), Duration::from_secs(5)),
25
+ Decision::Rejected(Rejection::Occupied),
26
+ );
27
+ assert_eq!(
28
+ authority.release(OwnerId::new(1), lease.token(), Duration::from_secs(2)),
29
+ Decision::Released,
30
+ );
31
+ ```
32
+
33
+ - Each authority instance models **one resource and one token namespace**.
34
+ - Every acquisition, even by the same owner after release/expiry, gets a strictly
35
+ greater token. Renewal retains the token and never shortens its expiration.
36
+ - A lease expires at `now >= expires_at`. Renewal cannot resurrect an expired
37
+ lease. Release and renewal require both the current owner and current token.
38
+ - Supply durations from one monotonic origin. Regressing observations are rejected.
39
+ Non-regressing observations advance the clock even when the request is rejected.
40
+ - Zero TTL and deadline overflow are rejected. Exhausted token space fails closed;
41
+ tokens never wrap.
42
+ - `reacquire` is `acquire` plus one exception: the current owner may rotate its own
43
+ unexpired grant to a new token. The old token is stale from then on, so anything
44
+ still holding it is fenced. Another owner's live grant is still `Occupied`.
45
+
46
+ ## Rebuilding an Authority from Storage
47
+
48
+ ```rust
49
+ use core::time::Duration;
50
+ use lease_machines::{Checkpoint, Decision, FencingToken, Lease, LeaseAuthority, OwnerId};
51
+
52
+ # fn example() -> Option<()> {
53
+ // One row per resource: owner id, token, expires_at (ms), token high-water.
54
+ let (owner, token, expires_at_ms, last_issued) = (7_u64, 3_u64, 9_000_u64, 3_u64);
55
+ let lease = Lease::new(
56
+ OwnerId::new(owner), FencingToken::new(token)?, Duration::from_millis(expires_at_ms),
57
+ );
58
+ let committed = Checkpoint::from_parts(FencingToken::new(last_issued), Some(lease), Duration::ZERO)?;
59
+ let mut authority = LeaseAuthority::restore(committed);
60
+ let Decision::Granted(next) = authority.acquire(
61
+ OwnerId::new(8), Duration::from_millis(9_000), Duration::from_secs(5),
62
+ ) else {
63
+ panic!("the previous grant expired");
64
+ };
65
+ assert_eq!(next.token().value(), 4);
66
+ # Some(())
67
+ # }
68
+ # assert_eq!(example(), Some(()));
69
+ ```
70
+
71
+ - `OwnerId` and `FencingToken` are plain `u64` on the wire: `new()` in, `value()` out.
72
+ String owners hash to a `u64` on the caller's side; the authority only compares.
73
+ - `from_parts` refuses a lease whose token the history never issued. Keep the
74
+ high-water column even after release; it is what makes the next token greater.
75
+ - `observed` is the clock watermark. Pass `Duration::ZERO` when storage keeps none,
76
+ and the authority trusts whatever `now` the next request brings.
77
+
78
+ ## Clocks
79
+
80
+ Time is a `Duration` from an origin the caller picks: a monotonic start, or the Unix
81
+ epoch when the clock is a database's wall clock in milliseconds. One origin per
82
+ authority history.
83
+
84
+ By default an observation earlier than the last one is `Rejection::ClockRegressed`
85
+ and nothing changes. `with_clock_policy(ClockPolicy::Clamp)` evaluates such a request
86
+ at the last observation instead: expiry checks and new deadlines use the later time,
87
+ so a backwards step can never revive a lease or shorten one.
88
+
89
+ ## Fencing at the Protected Resource
90
+
91
+ ```rust
92
+ use lease_machines::{Fence, FencingToken};
93
+
94
+ # fn example() -> Option<()> {
95
+ let old = FencingToken::new(7)?;
96
+ let current = FencingToken::new(8)?;
97
+ let mut fence = Fence::new(Some(old));
98
+ assert!(fence.accept(current));
99
+ assert!(!fence.accept(old));
100
+ assert!(fence.accept(current)); // Multiple operations in one lease are allowed.
101
+ # Some(())
102
+ # }
103
+ # assert_eq!(example(), Some(()));
104
+ ```
105
+
106
+ Fencing rejects tokens lower than the highest token the resource has accepted.
107
+ Equal tokens remain valid for repeated operations in the same lease.
108
+ It does **not** prove current lease validity, authorize an owner, authenticate a
109
+ token, or deduplicate operations. A resource cannot reject a newly stale owner on
110
+ fencing grounds until it has observed a greater token. If immediate revocation is
111
+ required, add an authority/expiry check under an appropriate protocol.
112
+
113
+ ## Holding a Lease
114
+
115
+ ```rust
116
+ use core::time::Duration;
117
+ use lease_machines::{Cadence, FencingToken, Holder, HolderState};
118
+
119
+ # fn example() -> Option<()> {
120
+ let secs = Duration::from_secs;
121
+ let cadence = Cadence::new(secs(30), secs(10), secs(5))?;
122
+ let mut holder = Holder::new(FencingToken::new(1)?, secs(0), cadence);
123
+ assert!(!holder.renewal_due(secs(9)));
124
+ assert!(holder.renewal_due(secs(10)));
125
+ assert!(holder.refresh(secs(10)));
126
+ assert_eq!(holder.state(), HolderState::Unconfirmed);
127
+ assert!(holder.confirm(FencingToken::new(1)?, secs(11)));
128
+ assert_eq!(holder.state(), HolderState::Confirmed);
129
+ assert!(holder.at_risk(secs(36)));
130
+ assert!(holder.expire());
131
+ assert!(holder.reacquire(FencingToken::new(2)?, secs(40)));
132
+ holder.fence();
133
+ assert_eq!(holder.warning_deadline(), None);
134
+ # Some(())
135
+ # }
136
+ # assert_eq!(example(), Some(()));
137
+ ```
138
+
139
+ The holder is the other half of the conversation: it decides when to renew, how
140
+ nervous to be, and when to stop writing. It runs on the holder's own clock and
141
+ never compares instants with the authority.
142
+
143
+ - `Confirmed` -> `refresh` -> `Unconfirmed`, or `AtRisk` once past the warning
144
+ deadline (`ttl - warn_before` after the last confirmation).
145
+ - `confirm(token, at)` closes an attempt and reschedules. A response with an older
146
+ token than the current one is a stale reply and is ignored; the same or a newer
147
+ token is accepted.
148
+ - `expire` means the authority said so; only `reacquire` with a strictly newer token
149
+ recovers. `fence` is terminal and idempotent; every predicate answers accordingly.
150
+ - `renewal_due` is true whenever a renewal is outstanding, so a failed attempt
151
+ retries on the next poll without any extra state.
152
+
153
+ ## Required Runtime and Persistence Contract
154
+
155
+ 1. There must be one serialized authoritative history per resource. A local
156
+ mutable instance is not enough to arbitrate across partitioned servers.
157
+ 2. Commit authority state, including token high-water mark, active lease, and
158
+ releases/renewals, **before exposing successful decisions**. On uncertain
159
+ persistence, stop serving and recover; do not continue from ambiguous state.
160
+ 3. Resource adapters must authenticate issued tokens and atomically persist
161
+ their fencing high-water mark **with the protected mutation**. A separate
162
+ `accept()` call followed by an unguarded write is not safe under concurrency.
163
+ 4. Restore only the newest committed authority checkpoint, with the same clock
164
+ origin. Never roll back token state or start `new()` over an existing namespace.
165
+ A reboot or clock-origin change requires an external recovery protocol;
166
+ replaying old monotonic durations is not sufficient.
167
+ 5. Fence state must also survive resource restarts. Token comparisons are only
168
+ meaningful within the same resource namespace.
169
+
170
+ `checkpoint()` / `restore()` provide an opaque typed state transfer for a host
171
+ or deterministic simulation. There is no wire format, storage adapter, distributed
172
+ replication, or crash-safe persistence implementation in this initial version.
@@ -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 LeaseMachines
3
+ VERSION = '0.1.6'
4
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'lease_machines/version'
3
+ require 'lease_machines_native/lease_machines_native'
4
+
5
+ module LeaseMachines
6
+ Decision = Data.define(:status, :lease, :reason)
7
+
8
+ class Authority
9
+ def acquire(owner:, now:, ttl:)
10
+ Decision.new(*_acquire(owner, now, ttl))
11
+ end
12
+
13
+ def renew(owner:, token:, now:, ttl:)
14
+ Decision.new(*_renew(owner, token, now, ttl))
15
+ end
16
+
17
+ def release(owner:, token:, now:)
18
+ Decision.new(*_release(owner, token, now))
19
+ end
20
+
21
+ private :_acquire, :_renew, :_release
22
+ end
23
+
24
+ class Fence
25
+ def self.new(highest: nil) = _new(highest)
26
+ private_class_method :_new
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module LeaseMachines
2
+ VERSION: String
3
+ class Lease
4
+ def owner: () -> Integer
5
+ def token: () -> Integer
6
+ def expires_at: () -> Float
7
+ end
8
+ class Checkpoint
9
+ def last_issued: () -> Integer?
10
+ def observed: () -> Float
11
+ end
12
+ class Decision < Data
13
+ attr_reader status: Symbol
14
+ attr_reader lease: Lease?
15
+ attr_reader reason: Symbol?
16
+ end
17
+ class Authority
18
+ def self.new: () -> instance
19
+ def self.restore: (Checkpoint) -> instance
20
+ def acquire: (owner: Integer, now: Float, ttl: Float) -> Decision
21
+ def renew: (owner: Integer, token: Integer, now: Float, ttl: Float) -> Decision
22
+ def release: (owner: Integer, token: Integer, now: Float) -> Decision
23
+ def checkpoint: () -> Checkpoint
24
+ end
25
+ class Fence
26
+ def self.new: (?highest: Integer?) -> instance
27
+ def accept: (Integer) -> bool
28
+ def highest: () -> Integer?
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lease_machines
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.6
5
+ platform: aarch64-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: Thin Ruby bindings to the standalone lease-machines core; durability,
14
+ clocks, and resource enforcement belong to the caller.
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/lease_machines_native/core/CHANGELOG.md
25
+ - ext/lease_machines_native/core/README.md
26
+ - ext/lease_machines_native/extconf.rb
27
+ - lib/lease_machines.rb
28
+ - lib/lease_machines/version.rb
29
+ - lib/lease_machines_native/lease_machines_native.so
30
+ - sig/lease_machines.rbs
31
+ homepage: https://github.com/seuros/lease_machines
32
+ licenses:
33
+ - MIT
34
+ metadata:
35
+ source_code_uri: https://github.com/seuros/lease_machines
36
+ rubygems_mfa_required: 'true'
37
+ cargo_crate_name: lease_machines_native
38
+ cargo_manifest_path: ext/lease_machines_native/ffi/Cargo.toml
39
+ post_install_message:
40
+ rdoc_options: []
41
+ require_paths:
42
+ - lib
43
+ required_ruby_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '4.0'
48
+ - - "<"
49
+ - !ruby/object:Gem::Version
50
+ version: 4.1.dev
51
+ required_rubygems_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ requirements: []
57
+ rubygems_version: 3.5.23
58
+ signing_key:
59
+ specification_version: 4
60
+ summary: Native lease lifecycle and fencing decisions for Ruby
61
+ test_files: []