Skip to main content

ff_script/functions/
lease.rs

1//! Typed FCALL wrappers for lease management functions.
2//!
3//! These wrap the Lua functions defined in `lua/lease.lua`.
4//! Each function uses the `ff_function!` macro to generate an async fn
5//! that builds KEYS/ARGV, calls FCALL, and parses the result.
6
7use crate::error::ScriptError;
8use ff_core::contracts::{
9    MarkLeaseExpiredArgs, MarkLeaseExpiredResult, RenewLeaseArgs, RenewLeaseResult,
10    RevokeLeaseArgs, RevokeLeaseResult,
11};
12use ff_core::keys::ExecKeyContext;
13use ff_core::types::TimestampMs;
14
15use crate::result::{FcallResult, FromFcallResult};
16
17// ─── FromFcallResult implementations ───
18
19impl FromFcallResult for RenewLeaseResult {
20    fn from_fcall_result(raw: &ferriskey::Value) -> Result<Self, ScriptError> {
21        let r = FcallResult::parse(raw)?.into_success()?;
22        // Lua returns: ok(new_expires_at_string)
23        let expires_str = r.field_str(0);
24        let expires_ms: i64 = expires_str
25            .parse()
26            .map_err(|_| ScriptError::Parse {
27                fcall: "ff_renew_lease".into(),
28                execution_id: None,
29                message: format!("invalid expires_at: {expires_str}"),
30            })?;
31        Ok(RenewLeaseResult::Renewed {
32            expires_at: TimestampMs::from_millis(expires_ms),
33        })
34    }
35}
36
37impl FromFcallResult for MarkLeaseExpiredResult {
38    fn from_fcall_result(raw: &ferriskey::Value) -> Result<Self, ScriptError> {
39        let r = FcallResult::parse(raw)?.into_success()?;
40        // Lua returns: ok("marked_expired") or ok_already_satisfied("reason")
41        match r.status.as_str() {
42            "OK" => Ok(MarkLeaseExpiredResult::MarkedExpired),
43            "ALREADY_SATISFIED" => Ok(MarkLeaseExpiredResult::AlreadySatisfied {
44                reason: r.field_str(0),
45            }),
46            other => Err(ScriptError::Parse {
47                fcall: "ff_mark_lease_expired_if_due".into(),
48                execution_id: None,
49                message: format!(
50                "unexpected status from ff_mark_lease_expired_if_due: {other}"
51            ),
52            }),
53        }
54    }
55}
56
57impl FromFcallResult for RevokeLeaseResult {
58    fn from_fcall_result(raw: &ferriskey::Value) -> Result<Self, ScriptError> {
59        let r = FcallResult::parse(raw)?.into_success()?;
60        // Lua returns: ok("revoked", lease_id, lease_epoch)
61        //           or ok_already_satisfied("reason")
62        match r.status.as_str() {
63            "OK" => {
64                // fields[0] = "revoked", fields[1] = lease_id, fields[2] = lease_epoch
65                Ok(RevokeLeaseResult::Revoked {
66                    lease_id: r.field_str(1),
67                    lease_epoch: r.field_str(2),
68                })
69            }
70            "ALREADY_SATISFIED" => Ok(RevokeLeaseResult::AlreadySatisfied {
71                reason: r.field_str(0),
72            }),
73            other => Err(ScriptError::Parse {
74                fcall: "ff_revoke_lease".into(),
75                execution_id: None,
76                message: format!(
77                "unexpected status from ff_revoke_lease: {other}"
78            ),
79            }),
80        }
81    }
82}
83
84// ─── ff_function! invocations ───
85
86ff_function! {
87    /// Renew an active lease. Extends expires_at by lease_ttl_ms.
88    ///
89    /// KEYS(4): exec_core, lease_current, lease_history, lease_expiry_zset
90    /// ARGV(7): execution_id, attempt_index, attempt_id, lease_id, lease_epoch,
91    ///          lease_ttl_ms, lease_history_grace_ms
92    pub ff_renew_lease(args: RenewLeaseArgs) -> RenewLeaseResult {
93        keys(ctx: &ExecKeyContext) {
94            ctx.core(),
95            ctx.lease_current(),
96            ctx.lease_history(),
97            format!("ff:idx:{}:lease_expiry", ctx.hash_tag()),
98        }
99        // RFC #58.5: `fence` is Option<LeaseFence>. Renew hard-rejects
100        // empty triples with `fence_required` — no operator override.
101        argv {
102            args.execution_id.to_string(),
103            args.attempt_index.to_string(),
104            args.fence.as_ref().map(|f| f.attempt_id.to_string()).unwrap_or_default(),
105            args.fence.as_ref().map(|f| f.lease_id.to_string()).unwrap_or_default(),
106            args.fence.as_ref().map(|f| f.lease_epoch.to_string()).unwrap_or_default(),
107            args.lease_ttl_ms.to_string(),
108            args.lease_history_grace_ms.to_string(),
109        }
110    }
111
112    /// Mark a lease as expired if it is actually due.
113    /// Called by the lease expiry scanner.
114    ///
115    /// KEYS(4): exec_core, lease_current, lease_expiry_zset, lease_history
116    /// ARGV(1): execution_id
117    pub ff_mark_lease_expired_if_due(args: MarkLeaseExpiredArgs) -> MarkLeaseExpiredResult {
118        keys(ctx: &ExecKeyContext) {
119            ctx.core(),
120            ctx.lease_current(),
121            format!("ff:idx:{}:lease_expiry", ctx.hash_tag()),
122            ctx.lease_history(),
123        }
124        argv {
125            args.execution_id.to_string(),
126        }
127    }
128
129    /// Revoke an active lease (operator-initiated).
130    ///
131    /// KEYS(5): exec_core, lease_current, lease_history, lease_expiry_zset, worker_leases
132    /// ARGV(3): execution_id, expected_lease_id, revoke_reason
133    pub ff_revoke_lease(args: RevokeLeaseArgs) -> RevokeLeaseResult {
134        keys(ctx: &ExecKeyContext) {
135            ctx.core(),
136            ctx.lease_current(),
137            ctx.lease_history(),
138            format!("ff:idx:{}:lease_expiry", ctx.hash_tag()),
139            format!("ff:idx:{}:worker:{}:leases", ctx.hash_tag(), args.worker_instance_id),
140        }
141        argv {
142            args.execution_id.to_string(),
143            args.expected_lease_id.as_deref().unwrap_or("").to_string(),
144            args.reason.clone(),
145        }
146    }
147}