///|
/// Creates an empty resource state. Token zero is never issued.
pub fn LeaseState::new(resource : String) -> LeaseState {
{ resource, lease: None, next_token: 1, revision: 0 }
}
///|
/// Returns whether this lease is valid at the supplied logical time.
pub fn Lease::is_active(self : Lease, now : Int) -> Bool {
now >= self.issued_at && now < self.expires_at
}
///|
/// Returns the active lease, treating expired or not-yet-issued records as absent.
pub fn LeaseState::active_lease(self : LeaseState, now : Int) -> Lease? {
match self.lease {
Some(lease) => if lease.is_active(now) { Some(lease) } else { None }
None => None
}
}
///|
/// Remaining logical ticks, clamped to zero.
pub fn Lease::remaining(self : Lease, now : Int) -> Int {
if self.expires_at > now {
self.expires_at - now
} else {
0
}
}
///|
fn rejected(state : LeaseState, message : String) -> LeaseDecision {
{
state,
accepted: false,
kind: Rejected,
lease: state.lease,
event: None,
message,
}
}
///|
fn accepted(
state : LeaseState,
kind : DecisionKind,
now : Int,
holder : String,
token : Int,
detail : String,
) -> LeaseDecision {
let event = {
resource: state.resource,
kind,
at: now,
holder,
token,
revision: state.revision,
detail,
}
{
state,
accepted: true,
kind,
lease: state.lease,
event: Some(event),
message: detail,
}
}
///|
fn validate_identity(holder : String) -> String? {
if holder == "" {
Some("holder must not be empty")
} else {
None
}
}
///|
/// Applies one command at an explicit logical time.
pub fn LeaseState::apply(
self : LeaseState,
command : LeaseCommand,
now : Int,
) -> LeaseDecision {
if self.resource == "" {
return rejected(self, "resource must not be empty")
}
if now < 0 {
return rejected(self, "logical time must not be negative")
}
match command {
Acquire(holder, ttl, expected_revision) => {
match validate_identity(holder) {
Some(message) => return rejected(self, message)
None => ()
}
if ttl <= 0 {
return rejected(self, "ttl must be positive")
}
match expected_revision {
Some(expected) =>
if expected != self.revision {
return rejected(self, "revision precondition failed")
}
None => ()
}
match self.active_lease(now) {
Some(_) => rejected(self, "resource already has an active lease")
None => {
let revision = self.revision + 1
let lease = {
resource: self.resource,
holder,
token: self.next_token,
revision,
issued_at: now,
expires_at: now + ttl,
}
let state = {
resource: self.resource,
lease: Some(lease),
next_token: self.next_token + 1,
revision,
}
accepted(state, Acquired, now, holder, lease.token, "lease acquired")
}
}
}
Renew(holder, token, ttl) => {
match validate_identity(holder) {
Some(message) => return rejected(self, message)
None => ()
}
if ttl <= 0 {
return rejected(self, "ttl must be positive")
}
match self.active_lease(now) {
None => rejected(self, "cannot renew an absent or expired lease")
Some(current) =>
if current.holder != holder || current.token != token {
rejected(self, "holder or fencing token mismatch")
} else {
let revision = self.revision + 1
let lease = {
..current,
revision,
issued_at: now,
expires_at: now + ttl,
}
let state = { ..self, lease: Some(lease), revision }
accepted(state, Renewed, now, holder, token, "lease renewed")
}
}
}
Release(holder, token) =>
match self.active_lease(now) {
None => rejected(self, "cannot release an absent or expired lease")
Some(current) =>
if current.holder != holder || current.token != token {
rejected(self, "holder or fencing token mismatch")
} else {
let state = { ..self, lease: None, revision: self.revision + 1 }
accepted(state, Released, now, holder, token, "lease released")
}
}
Transfer(holder, token, successor, ttl) => {
match validate_identity(successor) {
Some(_) => return rejected(self, "successor must not be empty")
None => ()
}
if successor == holder {
return rejected(self, "successor must differ from current holder")
}
if ttl <= 0 {
return rejected(self, "ttl must be positive")
}
match self.active_lease(now) {
None => rejected(self, "cannot transfer an absent or expired lease")
Some(current) =>
if current.holder != holder || current.token != token {
rejected(self, "holder or fencing token mismatch")
} else {
let revision = self.revision + 1
let lease = {
resource: self.resource,
holder: successor,
token: self.next_token,
revision,
issued_at: now,
expires_at: now + ttl,
}
let state = {
resource: self.resource,
lease: Some(lease),
next_token: self.next_token + 1,
revision,
}
accepted(
state,
Transferred,
now,
successor,
lease.token,
"lease transferred",
)
}
}
}
Reap =>
match self.lease {
Some(current) =>
if current.is_active(now) {
rejected(self, "active lease cannot be reaped")
} else {
let state = { ..self, lease: None, revision: self.revision + 1 }
accepted(
state,
Reaped,
now,
current.holder,
current.token,
"expired lease reaped",
)
}
None => rejected(self, "no lease to reap")
}
}
}