cellstate/entity/
lock.rs

1//! `cellstate lock` — Manage distributed locks for resource coordination.
2//!
3//! Subcommands: acquire, list, get, release, extend.
4
5use anyhow::{anyhow, Result};
6use clap::{Arg, ArgMatches, Command};
7
8use cellstate_core::api_types::{
9    AcquireLockRequest, ExtendLockRequest, ListLocksResponse, LockResponse, ReleaseLockRequest,
10};
11
12use super::client::ApiClient;
13use super::output::OutputConfig;
14
15const BASE: &str = "/api/v1/locks";
16
17pub fn build_command() -> Command {
18    Command::new("lock")
19        .about("Manage distributed locks for resource coordination")
20        .subcommand_required(true)
21        .subcommand(
22            Command::new("acquire")
23                .about("Acquire a lock on a resource")
24                .arg(
25                    Arg::new("resource-type").required(true).help(
26                        "Type of resource to lock (trajectory, scope, artifact, note, agent)",
27                    ),
28                )
29                .arg(
30                    Arg::new("resource-id")
31                        .required(true)
32                        .help("ID of the resource to lock"),
33                )
34                .arg(
35                    Arg::new("agent-id")
36                        .help("Agent requesting the lock (defaults to session agent)"),
37                )
38                .arg(
39                    Arg::new("timeout-ms")
40                        .long("timeout-ms")
41                        .default_value("30000")
42                        .help("Lock timeout in milliseconds"),
43                )
44                .arg(
45                    Arg::new("mode")
46                        .long("mode")
47                        .default_value("exclusive")
48                        .help("Lock mode (exclusive, shared)"),
49                ),
50        )
51        .subcommand(
52            Command::new("list")
53                .about("List active locks")
54                .arg(
55                    Arg::new("resource-type")
56                        .long("resource-type")
57                        .help("Filter by resource type"),
58                )
59                .arg(
60                    Arg::new("agent-id")
61                        .long("agent-id")
62                        .help("Filter by holder agent"),
63                )
64                .arg(
65                    Arg::new("limit")
66                        .long("limit")
67                        .default_value("20")
68                        .help("Max results"),
69                )
70                .arg(
71                    Arg::new("offset")
72                        .long("offset")
73                        .default_value("0")
74                        .help("Offset"),
75                ),
76        )
77        .subcommand(
78            Command::new("get")
79                .about("Get lock details")
80                .arg(Arg::new("id").required(true).help("Lock ID")),
81        )
82        .subcommand(
83            Command::new("release")
84                .about("Release a held lock")
85                .arg(Arg::new("id").required(true).help("Lock ID"))
86                .arg(
87                    Arg::new("agent-id")
88                        .help("Agent releasing the lock (defaults to session agent)"),
89                ),
90        )
91        .subcommand(
92            Command::new("extend")
93                .about("Extend a lock's expiration")
94                .arg(Arg::new("id").required(true).help("Lock ID"))
95                .arg(
96                    Arg::new("additional-ms")
97                        .required(true)
98                        .help("Additional time in milliseconds"),
99                ),
100        )
101}
102
103pub async fn dispatch(
104    matches: &ArgMatches,
105    client: &ApiClient,
106    output: &OutputConfig,
107    session: &crate::session::CliSession,
108) -> Result<()> {
109    match matches.subcommand() {
110        Some(("acquire", sub)) => {
111            let resource_type = sub
112                .get_one::<String>("resource-type")
113                .unwrap()
114                .parse()
115                .map_err(|e: String| anyhow!(e))?;
116            let resource_id: uuid::Uuid = sub.get_one::<String>("resource-id").unwrap().parse()?;
117            let holder_agent_id =
118                super::require_arg(sub, "agent-id", session.agent_id.as_deref(), "agent")?
119                    .parse()?;
120            let timeout_ms: i64 = sub.get_one::<String>("timeout-ms").unwrap().parse()?;
121            let mode = sub.get_one::<String>("mode").unwrap().clone();
122            let req = AcquireLockRequest {
123                resource_type,
124                resource_id,
125                holder_agent_id,
126                timeout_ms,
127                mode,
128            };
129            let resp: LockResponse = client.post(&format!("{BASE}/acquire"), &req).await?;
130            output.print(&resp);
131        }
132        Some(("list", sub)) => {
133            let limit: i64 = sub.get_one::<String>("limit").unwrap().parse()?;
134            let offset: i64 = sub.get_one::<String>("offset").unwrap().parse()?;
135            let mut path = format!("{BASE}?limit={limit}&offset={offset}");
136            if let Some(resource_type) = sub.get_one::<String>("resource-type") {
137                path.push_str(&format!("&resource_type={resource_type}"));
138            }
139            if let Some(agent_id) = super::resolve_arg(sub, "agent-id", session.agent_id.as_deref())
140            {
141                path.push_str(&format!("&agent_id={agent_id}"));
142            }
143            let resp: ListLocksResponse = client.get(&path).await?;
144            output.print_list(&resp.locks, resp.total);
145        }
146        Some(("get", sub)) => {
147            let id = sub.get_one::<String>("id").unwrap();
148            let resp: LockResponse = client.get(&format!("{BASE}/{id}")).await?;
149            output.print(&resp);
150        }
151        Some(("release", sub)) => {
152            let id = sub.get_one::<String>("id").unwrap();
153            let releasing_agent_id =
154                super::require_arg(sub, "agent-id", session.agent_id.as_deref(), "agent")?
155                    .parse()?;
156            let req = ReleaseLockRequest { releasing_agent_id };
157            let resp: serde_json::Value = client
158                .post_raw(&format!("{BASE}/{id}/release"), &req)
159                .await?;
160            output.print_value(&resp);
161        }
162        Some(("extend", sub)) => {
163            let id = sub.get_one::<String>("id").unwrap();
164            let additional_ms: i64 = sub.get_one::<String>("additional-ms").unwrap().parse()?;
165            let req = ExtendLockRequest { additional_ms };
166            let resp: serde_json::Value = client
167                .post_raw(&format!("{BASE}/{id}/extend"), &req)
168                .await?;
169            output.print_value(&resp);
170        }
171        _ => unreachable!("subcommand_required(true) prevents this"),
172    }
173    Ok(())
174}