lease_machines 0.1.6
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 +43 -0
- data/Cargo.toml +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +236 -0
- data/ext/lease_machines_native/core/CHANGELOG.md +36 -0
- data/ext/lease_machines_native/core/Cargo.toml +13 -0
- data/ext/lease_machines_native/core/README.md +172 -0
- data/ext/lease_machines_native/core/src/authority.rs +287 -0
- data/ext/lease_machines_native/core/src/fencing.rs +67 -0
- data/ext/lease_machines_native/core/src/holder.rs +193 -0
- data/ext/lease_machines_native/core/src/lease.rs +76 -0
- data/ext/lease_machines_native/core/src/lib.rs +12 -0
- data/ext/lease_machines_native/extconf.rb +2 -0
- data/ext/lease_machines_native/ffi/Cargo.toml +17 -0
- data/ext/lease_machines_native/ffi/extconf.rb +12 -0
- data/ext/lease_machines_native/ffi/src/bindings.rs +218 -0
- data/ext/lease_machines_native/ffi/src/lib.rs +2 -0
- data/lib/lease_machines/version.rb +4 -0
- data/lib/lease_machines.rb +28 -0
- data/sig/lease_machines.rbs +30 -0
- metadata +86 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
use core::time::Duration;
|
|
2
|
+
|
|
3
|
+
use crate::{FencingToken, Lease, OwnerId};
|
|
4
|
+
|
|
5
|
+
/// Why an authority rejected a request.
|
|
6
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
7
|
+
#[non_exhaustive]
|
|
8
|
+
pub enum Rejection {
|
|
9
|
+
/// An unexpired lease already occupies the resource.
|
|
10
|
+
Occupied,
|
|
11
|
+
/// No grant exists to renew or release.
|
|
12
|
+
NoLease,
|
|
13
|
+
/// The request's owner does not match the current grant.
|
|
14
|
+
WrongOwner,
|
|
15
|
+
/// The request's token does not match the current grant.
|
|
16
|
+
StaleToken,
|
|
17
|
+
/// The current grant has expired.
|
|
18
|
+
Expired,
|
|
19
|
+
/// Time moved backwards relative to a previous observation.
|
|
20
|
+
ClockRegressed,
|
|
21
|
+
/// TTL is zero or its addition overflows the duration range.
|
|
22
|
+
InvalidTtl,
|
|
23
|
+
/// No greater fencing token can be issued.
|
|
24
|
+
TokenExhausted,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// How an authority treats an observation earlier than its last one.
|
|
28
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
29
|
+
#[non_exhaustive]
|
|
30
|
+
pub enum ClockPolicy {
|
|
31
|
+
/// Reject the request with `Rejection::ClockRegressed`, changing nothing.
|
|
32
|
+
#[default]
|
|
33
|
+
Reject,
|
|
34
|
+
/// Evaluate the request at the last observation instead of the earlier `now`.
|
|
35
|
+
Clamp,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// A proposed state-machine outcome, to commit before exposing success.
|
|
39
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
40
|
+
#[non_exhaustive]
|
|
41
|
+
pub enum Decision {
|
|
42
|
+
/// A new token and lease were assigned.
|
|
43
|
+
Granted(Lease),
|
|
44
|
+
/// The current lease was renewed without changing its token.
|
|
45
|
+
Renewed(Lease),
|
|
46
|
+
/// The current lease was released.
|
|
47
|
+
Released,
|
|
48
|
+
/// The request was rejected.
|
|
49
|
+
Rejected(Rejection),
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Opaque authority state with no promised wire representation.
|
|
53
|
+
///
|
|
54
|
+
/// Restore only the newest committed state with the same monotonic clock origin.
|
|
55
|
+
/// Copying or rolling back this value does not create an independent authority.
|
|
56
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
57
|
+
pub struct Checkpoint {
|
|
58
|
+
last_issued: Option<FencingToken>,
|
|
59
|
+
lease: Option<Lease>,
|
|
60
|
+
observed: Duration,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
impl Checkpoint {
|
|
64
|
+
/// Rebuild committed state from storage columns.
|
|
65
|
+
///
|
|
66
|
+
/// Returns `None` when a lease carries a token the history never issued.
|
|
67
|
+
/// Use `Duration::ZERO` as `observed` when storage keeps no clock watermark.
|
|
68
|
+
#[must_use]
|
|
69
|
+
pub const fn from_parts(
|
|
70
|
+
last_issued: Option<FencingToken>,
|
|
71
|
+
lease: Option<Lease>,
|
|
72
|
+
observed: Duration,
|
|
73
|
+
) -> Option<Self> {
|
|
74
|
+
if let Some(lease) = lease {
|
|
75
|
+
match last_issued {
|
|
76
|
+
Some(highest) if lease.token.value() <= highest.value() => {}
|
|
77
|
+
_ => return None,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
Some(Self {
|
|
81
|
+
last_issued,
|
|
82
|
+
lease,
|
|
83
|
+
observed,
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Greatest token issued in this authority history, even after release.
|
|
88
|
+
#[must_use]
|
|
89
|
+
pub const fn last_issued(self) -> Option<FencingToken> {
|
|
90
|
+
self.last_issued
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Last grant; it may already be expired at the latest observation.
|
|
94
|
+
#[must_use]
|
|
95
|
+
pub const fn lease(self) -> Option<Lease> {
|
|
96
|
+
self.lease
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// Latest accepted clock observation, including rejected requests.
|
|
100
|
+
#[must_use]
|
|
101
|
+
pub const fn observed(self) -> Duration {
|
|
102
|
+
self.observed
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// A deterministic lease authority for one resource and token namespace.
|
|
107
|
+
///
|
|
108
|
+
/// Runtime adapters must serialize and durably commit transitions before
|
|
109
|
+
/// exposing success. This type provides no distributed agreement or I/O.
|
|
110
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
111
|
+
pub struct LeaseAuthority {
|
|
112
|
+
state: Checkpoint,
|
|
113
|
+
clock: ClockPolicy,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
impl Default for LeaseAuthority {
|
|
117
|
+
fn default() -> Self {
|
|
118
|
+
Self::new()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
impl LeaseAuthority {
|
|
123
|
+
/// Start a **fresh** authority namespace. Never use this to reset an old one.
|
|
124
|
+
#[must_use]
|
|
125
|
+
pub const fn new() -> Self {
|
|
126
|
+
Self {
|
|
127
|
+
state: Checkpoint {
|
|
128
|
+
last_issued: None,
|
|
129
|
+
lease: None,
|
|
130
|
+
observed: Duration::ZERO,
|
|
131
|
+
},
|
|
132
|
+
clock: ClockPolicy::Reject,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Restore the newest committed checkpoint under the same clock origin.
|
|
137
|
+
///
|
|
138
|
+
/// The caller must prevent simultaneous serving by the previous authority.
|
|
139
|
+
#[must_use]
|
|
140
|
+
pub const fn restore(checkpoint: Checkpoint) -> Self {
|
|
141
|
+
Self {
|
|
142
|
+
state: checkpoint,
|
|
143
|
+
clock: ClockPolicy::Reject,
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Choose how earlier-than-observed clocks are handled; `Reject` by default.
|
|
148
|
+
#[must_use]
|
|
149
|
+
pub const fn with_clock_policy(mut self, clock: ClockPolicy) -> Self {
|
|
150
|
+
self.clock = clock;
|
|
151
|
+
self
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/// Policy applied to regressing observations.
|
|
155
|
+
#[must_use]
|
|
156
|
+
pub const fn clock_policy(&self) -> ClockPolicy {
|
|
157
|
+
self.clock
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Capture state for a host-controlled commit or deterministic simulation.
|
|
161
|
+
#[must_use]
|
|
162
|
+
pub const fn checkpoint(&self) -> Checkpoint {
|
|
163
|
+
self.state
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// Acquire an unoccupied/expired resource with a new, strictly greater token.
|
|
167
|
+
#[must_use]
|
|
168
|
+
pub fn acquire(&mut self, owner: OwnerId, now: Duration, ttl: Duration) -> Decision {
|
|
169
|
+
self.grant(owner, now, ttl, false)
|
|
170
|
+
.map_or_else(Decision::Rejected, Decision::Granted)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Acquire like `acquire`, or rotate the token of `owner`'s own unexpired grant.
|
|
174
|
+
///
|
|
175
|
+
/// Another owner's unexpired grant is still `Occupied`. The previous token
|
|
176
|
+
/// becomes stale, so in-flight work under it is fenced.
|
|
177
|
+
#[must_use]
|
|
178
|
+
pub fn reacquire(&mut self, owner: OwnerId, now: Duration, ttl: Duration) -> Decision {
|
|
179
|
+
self.grant(owner, now, ttl, true)
|
|
180
|
+
.map_or_else(Decision::Rejected, Decision::Granted)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
fn grant(
|
|
184
|
+
&mut self,
|
|
185
|
+
owner: OwnerId,
|
|
186
|
+
now: Duration,
|
|
187
|
+
ttl: Duration,
|
|
188
|
+
rotate: bool,
|
|
189
|
+
) -> Result<Lease, Rejection> {
|
|
190
|
+
let now = self.observe(now)?;
|
|
191
|
+
let expires_at = expiration(now, ttl).ok_or(Rejection::InvalidTtl)?;
|
|
192
|
+
if self
|
|
193
|
+
.state
|
|
194
|
+
.lease
|
|
195
|
+
.is_some_and(|lease| now < lease.expires_at && !(rotate && lease.owner == owner))
|
|
196
|
+
{
|
|
197
|
+
return Err(Rejection::Occupied);
|
|
198
|
+
}
|
|
199
|
+
let token = FencingToken::next(self.state.last_issued).ok_or(Rejection::TokenExhausted)?;
|
|
200
|
+
let lease = Lease {
|
|
201
|
+
owner,
|
|
202
|
+
token,
|
|
203
|
+
expires_at,
|
|
204
|
+
};
|
|
205
|
+
self.state.last_issued = Some(token);
|
|
206
|
+
self.state.lease = Some(lease);
|
|
207
|
+
Ok(lease)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Renew only the current unexpired grant, retaining its token.
|
|
211
|
+
///
|
|
212
|
+
/// A shorter requested TTL does not shorten the previous expiration.
|
|
213
|
+
#[must_use]
|
|
214
|
+
pub fn renew(
|
|
215
|
+
&mut self,
|
|
216
|
+
owner: OwnerId,
|
|
217
|
+
token: FencingToken,
|
|
218
|
+
now: Duration,
|
|
219
|
+
ttl: Duration,
|
|
220
|
+
) -> Decision {
|
|
221
|
+
self.extend(owner, token, now, ttl)
|
|
222
|
+
.map_or_else(Decision::Rejected, Decision::Renewed)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
fn extend(
|
|
226
|
+
&mut self,
|
|
227
|
+
owner: OwnerId,
|
|
228
|
+
token: FencingToken,
|
|
229
|
+
now: Duration,
|
|
230
|
+
ttl: Duration,
|
|
231
|
+
) -> Result<Lease, Rejection> {
|
|
232
|
+
let (now, mut lease) = self.held(owner, token, now)?;
|
|
233
|
+
let expires_at = expiration(now, ttl).ok_or(Rejection::InvalidTtl)?;
|
|
234
|
+
lease.expires_at = lease.expires_at.max(expires_at);
|
|
235
|
+
self.state.lease = Some(lease);
|
|
236
|
+
Ok(lease)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Release only the current unexpired grant, retaining the token high-water.
|
|
240
|
+
#[must_use]
|
|
241
|
+
pub fn release(&mut self, owner: OwnerId, token: FencingToken, now: Duration) -> Decision {
|
|
242
|
+
match self.held(owner, token, now) {
|
|
243
|
+
Ok(_) => {
|
|
244
|
+
self.state.lease = None;
|
|
245
|
+
Decision::Released
|
|
246
|
+
}
|
|
247
|
+
Err(reason) => Decision::Rejected(reason),
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Observe `now`, then return it with the current grant if `owner` holds
|
|
252
|
+
/// it under `token` and it has not expired.
|
|
253
|
+
fn held(
|
|
254
|
+
&mut self,
|
|
255
|
+
owner: OwnerId,
|
|
256
|
+
token: FencingToken,
|
|
257
|
+
now: Duration,
|
|
258
|
+
) -> Result<(Duration, Lease), Rejection> {
|
|
259
|
+
let now = self.observe(now)?;
|
|
260
|
+
let lease = self.state.lease.ok_or(Rejection::NoLease)?;
|
|
261
|
+
lease
|
|
262
|
+
.rejection(owner, token, now)
|
|
263
|
+
.map_or(Ok((now, lease)), Err)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
fn observe(&mut self, now: Duration) -> Result<Duration, Rejection> {
|
|
267
|
+
if now < self.state.observed {
|
|
268
|
+
return match self.clock {
|
|
269
|
+
ClockPolicy::Reject => Err(Rejection::ClockRegressed),
|
|
270
|
+
ClockPolicy::Clamp => Ok(self.state.observed),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
self.state.observed = now;
|
|
274
|
+
Ok(now)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const fn expiration(now: Duration, ttl: Duration) -> Option<Duration> {
|
|
279
|
+
if ttl.is_zero() {
|
|
280
|
+
None
|
|
281
|
+
} else {
|
|
282
|
+
now.checked_add(ttl)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[cfg(test)]
|
|
287
|
+
mod tests;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
use core::num::NonZeroU64;
|
|
2
|
+
|
|
3
|
+
/// A positive fencing sequence number within one resource's authority namespace.
|
|
4
|
+
///
|
|
5
|
+
/// This value is not a cryptographic proof that an authority issued it.
|
|
6
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
7
|
+
pub struct FencingToken(NonZeroU64);
|
|
8
|
+
|
|
9
|
+
impl FencingToken {
|
|
10
|
+
/// Decode a positive token; zero is not a valid issued token.
|
|
11
|
+
#[must_use]
|
|
12
|
+
pub const fn new(value: u64) -> Option<Self> {
|
|
13
|
+
match NonZeroU64::new(value) {
|
|
14
|
+
Some(value) => Some(Self(value)),
|
|
15
|
+
None => None,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/// Sequence number for trusted storage or transport adapters.
|
|
20
|
+
#[must_use]
|
|
21
|
+
pub const fn value(self) -> u64 {
|
|
22
|
+
self.0.get()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub(crate) fn next(previous: Option<Self>) -> Option<Self> {
|
|
26
|
+
let value = previous.map_or(0, Self::value).checked_add(1)?;
|
|
27
|
+
Self::new(value)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// Protected-resource high-water tracking for a single token namespace.
|
|
32
|
+
///
|
|
33
|
+
/// Persist updates atomically with the protected write. This local predicate does
|
|
34
|
+
/// not check expiry, authenticate tokens, or serialize external mutations.
|
|
35
|
+
#[derive(Debug, PartialEq, Eq)]
|
|
36
|
+
pub struct Fence {
|
|
37
|
+
highest: Option<FencingToken>,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl Fence {
|
|
41
|
+
/// Restore the newest committed high-water mark, or start a fresh resource.
|
|
42
|
+
#[must_use]
|
|
43
|
+
pub const fn new(highest: Option<FencingToken>) -> Self {
|
|
44
|
+
Self { highest }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Accept an equal/newer token and advance the local high-water mark.
|
|
48
|
+
///
|
|
49
|
+
/// Returns `false` for a stale token, without changing state.
|
|
50
|
+
#[must_use]
|
|
51
|
+
pub fn accept(&mut self, token: FencingToken) -> bool {
|
|
52
|
+
if self.highest.is_some_and(|highest| token < highest) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
self.highest = Some(token);
|
|
56
|
+
true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Highest accepted token, to persist atomically with protected mutations.
|
|
60
|
+
#[must_use]
|
|
61
|
+
pub const fn highest(&self) -> Option<FencingToken> {
|
|
62
|
+
self.highest
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[cfg(test)]
|
|
67
|
+
mod tests;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
use core::time::Duration;
|
|
2
|
+
|
|
3
|
+
use crate::FencingToken;
|
|
4
|
+
|
|
5
|
+
/// Renewal timing for one grant, on the holder's own clock.
|
|
6
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
7
|
+
pub struct Cadence {
|
|
8
|
+
ttl: Duration,
|
|
9
|
+
renew_after: Duration,
|
|
10
|
+
warn_before: Duration,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
impl Cadence {
|
|
14
|
+
/// Renew `renew_after` each confirmation; warn `warn_before` the local expiry.
|
|
15
|
+
///
|
|
16
|
+
/// Returns `None` unless `renew_after < ttl` and `warn_before <= ttl`.
|
|
17
|
+
#[must_use]
|
|
18
|
+
pub const fn new(ttl: Duration, renew_after: Duration, warn_before: Duration) -> Option<Self> {
|
|
19
|
+
if ttl.is_zero()
|
|
20
|
+
|| renew_after.as_nanos() >= ttl.as_nanos()
|
|
21
|
+
|| warn_before.as_nanos() > ttl.as_nanos()
|
|
22
|
+
{
|
|
23
|
+
return None;
|
|
24
|
+
}
|
|
25
|
+
Some(Self {
|
|
26
|
+
ttl,
|
|
27
|
+
renew_after,
|
|
28
|
+
warn_before,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Lease duration the holder requests on every renewal.
|
|
33
|
+
#[must_use]
|
|
34
|
+
pub const fn ttl(self) -> Duration {
|
|
35
|
+
self.ttl
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Where a holder stands with the grant it believes it owns.
|
|
40
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
41
|
+
#[non_exhaustive]
|
|
42
|
+
pub enum HolderState {
|
|
43
|
+
/// The last renewal succeeded and none is in flight.
|
|
44
|
+
Confirmed,
|
|
45
|
+
/// A renewal is in flight, with time to spare before the warning deadline.
|
|
46
|
+
Unconfirmed,
|
|
47
|
+
/// A renewal is in flight past the warning deadline.
|
|
48
|
+
AtRisk,
|
|
49
|
+
/// The authority reported expiry; only a fresh grant restores ownership.
|
|
50
|
+
Expired,
|
|
51
|
+
/// Ownership is lost for good; no event leaves this state.
|
|
52
|
+
Fenced,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// Holder-side renewal tracker for a single grant.
|
|
56
|
+
///
|
|
57
|
+
/// Durations come from the holder's clock, not the authority's, so two
|
|
58
|
+
/// machines never compare instants. Deadlines are derived from the moment a
|
|
59
|
+
/// renewal was confirmed locally, which is never later than the authority's view.
|
|
60
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
61
|
+
pub struct Holder {
|
|
62
|
+
state: HolderState,
|
|
63
|
+
cadence: Cadence,
|
|
64
|
+
token: FencingToken,
|
|
65
|
+
renew_due: Duration,
|
|
66
|
+
warning_at: Duration,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl Holder {
|
|
70
|
+
/// Track a grant confirmed at `confirmed_at` on the holder's clock.
|
|
71
|
+
#[must_use]
|
|
72
|
+
pub const fn new(token: FencingToken, confirmed_at: Duration, cadence: Cadence) -> Self {
|
|
73
|
+
let mut holder = Self {
|
|
74
|
+
state: HolderState::Confirmed,
|
|
75
|
+
cadence,
|
|
76
|
+
token,
|
|
77
|
+
renew_due: Duration::ZERO,
|
|
78
|
+
warning_at: Duration::ZERO,
|
|
79
|
+
};
|
|
80
|
+
holder.schedule(confirmed_at);
|
|
81
|
+
holder
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Current lifecycle state.
|
|
85
|
+
#[must_use]
|
|
86
|
+
pub const fn state(&self) -> HolderState {
|
|
87
|
+
self.state
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Newest token this holder has seen for its grant.
|
|
91
|
+
#[must_use]
|
|
92
|
+
pub const fn token(&self) -> FencingToken {
|
|
93
|
+
self.token
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Whether a renewal should be started or retried now.
|
|
97
|
+
#[must_use]
|
|
98
|
+
pub const fn renewal_due(&self, now: Duration) -> bool {
|
|
99
|
+
match self.state {
|
|
100
|
+
HolderState::Confirmed => now.as_nanos() >= self.renew_due.as_nanos(),
|
|
101
|
+
HolderState::Fenced => false,
|
|
102
|
+
HolderState::Unconfirmed | HolderState::AtRisk | HolderState::Expired => true,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Point after which unconfirmed ownership should be treated as doubtful.
|
|
107
|
+
#[must_use]
|
|
108
|
+
pub const fn warning_deadline(&self) -> Option<Duration> {
|
|
109
|
+
match self.state {
|
|
110
|
+
HolderState::Fenced => None,
|
|
111
|
+
_ => Some(self.warning_at),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Whether ownership is unconfirmed or the warning deadline has passed.
|
|
116
|
+
#[must_use]
|
|
117
|
+
pub const fn at_risk(&self, now: Duration) -> bool {
|
|
118
|
+
!matches!(self.state, HolderState::Confirmed)
|
|
119
|
+
|| now.as_nanos() >= self.warning_at.as_nanos()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Start a renewal attempt; repeated calls while one is outstanding are no-ops.
|
|
123
|
+
///
|
|
124
|
+
/// Returns `false` once fenced.
|
|
125
|
+
pub const fn refresh(&mut self, now: Duration) -> bool {
|
|
126
|
+
match self.state {
|
|
127
|
+
HolderState::Fenced => false,
|
|
128
|
+
HolderState::Confirmed | HolderState::Unconfirmed => {
|
|
129
|
+
self.state = if now.as_nanos() >= self.warning_at.as_nanos() {
|
|
130
|
+
HolderState::AtRisk
|
|
131
|
+
} else {
|
|
132
|
+
HolderState::Unconfirmed
|
|
133
|
+
};
|
|
134
|
+
true
|
|
135
|
+
}
|
|
136
|
+
HolderState::AtRisk | HolderState::Expired => true,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Record a successful renewal observed at `at`.
|
|
141
|
+
///
|
|
142
|
+
/// A response carrying a token older than the current one is ignored, as is
|
|
143
|
+
/// one arriving without an outstanding attempt or after expiry or fencing.
|
|
144
|
+
pub const fn confirm(&mut self, token: FencingToken, at: Duration) -> bool {
|
|
145
|
+
match self.state {
|
|
146
|
+
HolderState::Unconfirmed | HolderState::AtRisk
|
|
147
|
+
if token.value() >= self.token.value() =>
|
|
148
|
+
{
|
|
149
|
+
self.token = token;
|
|
150
|
+
self.state = HolderState::Confirmed;
|
|
151
|
+
self.schedule(at);
|
|
152
|
+
true
|
|
153
|
+
}
|
|
154
|
+
_ => false,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Record that the authority reported the grant expired.
|
|
159
|
+
pub const fn expire(&mut self) -> bool {
|
|
160
|
+
if matches!(self.state, HolderState::Fenced) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
self.state = HolderState::Expired;
|
|
164
|
+
true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Record a fresh grant with a strictly newer token after expiry.
|
|
168
|
+
pub const fn reacquire(&mut self, token: FencingToken, at: Duration) -> bool {
|
|
169
|
+
match self.state {
|
|
170
|
+
HolderState::Expired if token.value() > self.token.value() => {
|
|
171
|
+
self.token = token;
|
|
172
|
+
self.state = HolderState::Confirmed;
|
|
173
|
+
self.schedule(at);
|
|
174
|
+
true
|
|
175
|
+
}
|
|
176
|
+
_ => false,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Give up ownership permanently; idempotent.
|
|
181
|
+
pub const fn fence(&mut self) {
|
|
182
|
+
self.state = HolderState::Fenced;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const fn schedule(&mut self, confirmed_at: Duration) {
|
|
186
|
+
self.renew_due = confirmed_at.saturating_add(self.cadence.renew_after);
|
|
187
|
+
let window = self.cadence.ttl.saturating_sub(self.cadence.warn_before);
|
|
188
|
+
self.warning_at = confirmed_at.saturating_add(window);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#[cfg(test)]
|
|
193
|
+
mod tests;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
use core::time::Duration;
|
|
2
|
+
|
|
3
|
+
use crate::{FencingToken, Rejection};
|
|
4
|
+
|
|
5
|
+
/// An opaque numeric owner identity, scoped and authenticated by the adapter.
|
|
6
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
7
|
+
pub struct OwnerId(u64);
|
|
8
|
+
|
|
9
|
+
impl OwnerId {
|
|
10
|
+
/// Construct an owner identity. Zero is a valid identity.
|
|
11
|
+
#[must_use]
|
|
12
|
+
pub const fn new(value: u64) -> Self {
|
|
13
|
+
Self(value)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Numeric identity for storage or transport adapters.
|
|
17
|
+
#[must_use]
|
|
18
|
+
pub const fn value(self) -> u64 {
|
|
19
|
+
self.0
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// One grant in a resource's serialized authority history.
|
|
24
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
25
|
+
pub struct Lease {
|
|
26
|
+
pub(crate) owner: OwnerId,
|
|
27
|
+
pub(crate) token: FencingToken,
|
|
28
|
+
pub(crate) expires_at: Duration,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl Lease {
|
|
32
|
+
/// Rebuild a committed grant from storage; see `Checkpoint::from_parts`.
|
|
33
|
+
#[must_use]
|
|
34
|
+
pub const fn new(owner: OwnerId, token: FencingToken, expires_at: Duration) -> Self {
|
|
35
|
+
Self {
|
|
36
|
+
owner,
|
|
37
|
+
token,
|
|
38
|
+
expires_at,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Owner identity that received this grant.
|
|
43
|
+
#[must_use]
|
|
44
|
+
pub const fn owner(self) -> OwnerId {
|
|
45
|
+
self.owner
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// Resource-scoped token to present to protected mutations.
|
|
49
|
+
#[must_use]
|
|
50
|
+
pub const fn token(self) -> FencingToken {
|
|
51
|
+
self.token
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Expiration relative to the authority's monotonic origin.
|
|
55
|
+
#[must_use]
|
|
56
|
+
pub const fn expires_at(self) -> Duration {
|
|
57
|
+
self.expires_at
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
pub(crate) fn rejection(
|
|
61
|
+
self,
|
|
62
|
+
owner: OwnerId,
|
|
63
|
+
token: FencingToken,
|
|
64
|
+
now: Duration,
|
|
65
|
+
) -> Option<Rejection> {
|
|
66
|
+
if self.owner != owner {
|
|
67
|
+
Some(Rejection::WrongOwner)
|
|
68
|
+
} else if self.token != token {
|
|
69
|
+
Some(Rejection::StaleToken)
|
|
70
|
+
} else if now >= self.expires_at {
|
|
71
|
+
Some(Rejection::Expired)
|
|
72
|
+
} else {
|
|
73
|
+
None
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#![no_std]
|
|
2
|
+
#![doc = include_str!("../README.md")]
|
|
3
|
+
|
|
4
|
+
mod authority;
|
|
5
|
+
mod fencing;
|
|
6
|
+
mod holder;
|
|
7
|
+
mod lease;
|
|
8
|
+
|
|
9
|
+
pub use authority::{Checkpoint, ClockPolicy, Decision, LeaseAuthority, Rejection};
|
|
10
|
+
pub use fencing::{Fence, FencingToken};
|
|
11
|
+
pub use holder::{Cadence, Holder, HolderState};
|
|
12
|
+
pub use lease::{Lease, OwnerId};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "lease_machines_native"
|
|
3
|
+
version = "0.1.5"
|
|
4
|
+
edition.workspace = true
|
|
5
|
+
rust-version.workspace = true
|
|
6
|
+
license.workspace = true
|
|
7
|
+
publish = false
|
|
8
|
+
[lib]
|
|
9
|
+
crate-type = ["cdylib"]
|
|
10
|
+
test = false
|
|
11
|
+
doctest = false
|
|
12
|
+
[dependencies]
|
|
13
|
+
lease-machines.workspace = true
|
|
14
|
+
magnus.workspace = true
|
|
15
|
+
rb-sys.workspace = true
|
|
16
|
+
[lints]
|
|
17
|
+
workspace = true
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'mkmf'
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'rb_sys/mkmf'
|
|
5
|
+
create_rust_makefile('lease_machines_native/lease_machines_native') do |extension|
|
|
6
|
+
extension.ext_dir = Pathname(__dir__).relative_path_from(Pathname(Dir.pwd)).to_s
|
|
7
|
+
extension.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
|
|
8
|
+
end
|
|
9
|
+
makefile = File.join(Dir.pwd, 'Makefile')
|
|
10
|
+
manifest_dir = File.expand_path(__dir__)
|
|
11
|
+
patched = File.read(makefile).gsub(/^RB_SYS_CARGO_MANIFEST_DIR \?=.*$/, "RB_SYS_CARGO_MANIFEST_DIR ?= #{manifest_dir}")
|
|
12
|
+
File.write(makefile, patched)
|