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 ff_core::contracts::{
8    MarkLeaseExpiredArgs, MarkLeaseExpiredResult, RenewLeaseArgs, RenewLeaseResult,
9    RevokeLeaseArgs, RevokeLeaseResult,
10};
11use crate::error::ScriptError;
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(format!("invalid expires_at: {expires_str}")))?;
27        Ok(RenewLeaseResult::Renewed {
28            expires_at: TimestampMs::from_millis(expires_ms),
29        })
30    }
31}
32
33impl FromFcallResult for MarkLeaseExpiredResult {
34    fn from_fcall_result(raw: &ferriskey::Value) -> Result<Self, ScriptError> {
35        let r = FcallResult::parse(raw)?.into_success()?;
36        // Lua returns: ok("marked_expired") or ok_already_satisfied("reason")
37        match r.status.as_str() {
38            "OK" => Ok(MarkLeaseExpiredResult::MarkedExpired),
39            "ALREADY_SATISFIED" => Ok(MarkLeaseExpiredResult::AlreadySatisfied {
40                reason: r.field_str(0),
41            }),
42            other => Err(ScriptError::Parse(format!(
43                "unexpected status from ff_mark_lease_expired_if_due: {other}"
44            ))),
45        }
46    }
47}
48
49impl FromFcallResult for RevokeLeaseResult {
50    fn from_fcall_result(raw: &ferriskey::Value) -> Result<Self, ScriptError> {
51        let r = FcallResult::parse(raw)?.into_success()?;
52        // Lua returns: ok("revoked", lease_id, lease_epoch)
53        //           or ok_already_satisfied("reason")
54        match r.status.as_str() {
55            "OK" => {
56                // fields[0] = "revoked", fields[1] = lease_id, fields[2] = lease_epoch
57                Ok(RevokeLeaseResult::Revoked {
58                    lease_id: r.field_str(1),
59                    lease_epoch: r.field_str(2),
60                })
61            }
62            "ALREADY_SATISFIED" => Ok(RevokeLeaseResult::AlreadySatisfied {
63                reason: r.field_str(0),
64            }),
65            other => Err(ScriptError::Parse(format!(
66                "unexpected status from ff_revoke_lease: {other}"
67            ))),
68        }
69    }
70}
71
72// ─── ff_function! invocations ───
73
74ff_function! {
75    /// Renew an active lease. Extends expires_at by lease_ttl_ms.
76    ///
77    /// KEYS(4): exec_core, lease_current, lease_history, lease_expiry_zset
78    /// ARGV(7): execution_id, attempt_index, attempt_id, lease_id, lease_epoch,
79    ///          lease_ttl_ms, lease_history_grace_ms
80    pub ff_renew_lease(args: RenewLeaseArgs) -> RenewLeaseResult {
81        keys(ctx: &ExecKeyContext) {
82            ctx.core(),
83            ctx.lease_current(),
84            ctx.lease_history(),
85            format!("ff:idx:{}:lease_expiry", ctx.hash_tag()),
86        }
87        argv {
88            args.execution_id.to_string(),
89            args.attempt_index.to_string(),
90            args.attempt_id.to_string(),
91            args.lease_id.to_string(),
92            args.lease_epoch.to_string(),
93            args.lease_ttl_ms.to_string(),
94            args.lease_history_grace_ms.to_string(),
95        }
96    }
97
98    /// Mark a lease as expired if it is actually due.
99    /// Called by the lease expiry scanner.
100    ///
101    /// KEYS(4): exec_core, lease_current, lease_expiry_zset, lease_history
102    /// ARGV(1): execution_id
103    pub ff_mark_lease_expired_if_due(args: MarkLeaseExpiredArgs) -> MarkLeaseExpiredResult {
104        keys(ctx: &ExecKeyContext) {
105            ctx.core(),
106            ctx.lease_current(),
107            format!("ff:idx:{}:lease_expiry", ctx.hash_tag()),
108            ctx.lease_history(),
109        }
110        argv {
111            args.execution_id.to_string(),
112        }
113    }
114
115    /// Revoke an active lease (operator-initiated).
116    ///
117    /// KEYS(5): exec_core, lease_current, lease_history, lease_expiry_zset, worker_leases
118    /// ARGV(3): execution_id, expected_lease_id, revoke_reason
119    pub ff_revoke_lease(args: RevokeLeaseArgs) -> RevokeLeaseResult {
120        keys(ctx: &ExecKeyContext) {
121            ctx.core(),
122            ctx.lease_current(),
123            ctx.lease_history(),
124            format!("ff:idx:{}:lease_expiry", ctx.hash_tag()),
125            format!("ff:idx:{}:worker:{}:leases", ctx.hash_tag(), args.worker_instance_id),
126        }
127        argv {
128            args.execution_id.to_string(),
129            args.expected_lease_id.as_deref().unwrap_or("").to_string(),
130            args.reason.clone(),
131        }
132    }
133}