1use super::{handshake_client_sync, read_frame_sync, write_frame_sync, Role};
15use serde::{Deserialize, Serialize};
16use std::net::TcpStream;
17use std::sync::mpsc;
18use std::time::{Duration, Instant};
19
20pub const DEFAULT_TTL: Duration = Duration::from_secs(60);
23
24#[derive(Serialize, Deserialize, Debug, Clone)]
25pub enum LeaseRequest {
26 Acquire { holder: String, ttl_s: u32 },
27 Renew { id: u64 },
28 Release { id: u64 },
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone)]
32pub enum LeaseReply {
33 Granted { id: u64 },
34 Busy { holder: String, expires_in_s: u32 },
36 Ok,
38 Gone,
40}
41
42fn roundtrip(stream: &mut TcpStream, req: &LeaseRequest) -> anyhow::Result<LeaseReply> {
43 write_frame_sync(stream, &postcard::to_stdvec(req)?)?;
44 Ok(postcard::from_bytes(&read_frame_sync(stream)?)?)
45}
46
47fn connect(addr: &str, token: &str) -> anyhow::Result<TcpStream> {
48 let mut stream = TcpStream::connect(addr)
49 .map_err(|e| anyhow::anyhow!("connecting to lease server at {addr}: {e}"))?;
50 stream.set_nodelay(true)?;
51 handshake_client_sync(&mut stream, Role::Lease, token)?;
52 Ok(stream)
53}
54
55pub struct LeaseClient {
57 stop: Option<mpsc::Sender<()>>,
58 thread: Option<std::thread::JoinHandle<()>>,
59}
60
61impl LeaseClient {
62 pub fn acquire(
64 addr: &str,
65 token: &str,
66 holder: &str,
67 timeout: Duration,
68 ) -> anyhow::Result<LeaseClient> {
69 let ttl = DEFAULT_TTL;
70 let deadline = Instant::now() + timeout;
71 let mut stream = connect(addr, token)?;
72 let id = loop {
73 let req = LeaseRequest::Acquire {
74 holder: holder.to_owned(),
75 ttl_s: ttl.as_secs() as u32,
76 };
77 match roundtrip(&mut stream, &req)? {
78 LeaseReply::Granted { id } => break id,
79 LeaseReply::Busy { holder: other, expires_in_s } => {
80 if Instant::now() >= deadline {
81 anyhow::bail!(
82 "rig lease held by '{other}' (expires in {expires_in_s}s), \
83 gave up after {timeout:?}"
84 );
85 }
86 std::thread::sleep(Duration::from_secs(1));
87 }
88 other => anyhow::bail!("unexpected reply to Acquire: {other:?}"),
89 }
90 };
91
92 let (stop, stopped) = mpsc::channel::<()>();
93 let (addr, token, holder) = (addr.to_owned(), token.to_owned(), holder.to_owned());
94 let thread = std::thread::Builder::new()
95 .name("banc-lease-renew".into())
96 .spawn(move || renew_loop(stream, id, ttl, &addr, &token, &holder, stopped))?;
97
98 Ok(LeaseClient { stop: Some(stop), thread: Some(thread) })
99 }
100}
101
102impl Drop for LeaseClient {
103 fn drop(&mut self) {
104 drop(self.stop.take());
105 if let Some(t) = self.thread.take() {
106 let _ = t.join();
107 }
108 }
109}
110
111fn renew_loop(
116 mut stream: TcpStream,
117 mut id: u64,
118 ttl: Duration,
119 addr: &str,
120 token: &str,
121 holder: &str,
122 stopped: mpsc::Receiver<()>,
123) {
124 let period = ttl / 3;
125 loop {
126 match stopped.recv_timeout(period) {
127 Err(mpsc::RecvTimeoutError::Disconnected) | Ok(()) => {
129 let _ = roundtrip(&mut stream, &LeaseRequest::Release { id });
130 return;
131 }
132 Err(mpsc::RecvTimeoutError::Timeout) => {}
133 }
134 let outcome = roundtrip(&mut stream, &LeaseRequest::Renew { id });
135 match outcome {
136 Ok(LeaseReply::Ok) => {}
137 Ok(LeaseReply::Gone) => {
138 eprintln!("banc: rig lease expired mid-run; re-acquiring");
139 match reacquire(addr, token, holder, ttl) {
140 Ok((s, new_id)) => {
141 stream = s;
142 id = new_id;
143 }
144 Err(e) => eprintln!("banc: rig lease lost and re-acquire failed: {e}"),
145 }
146 }
147 Ok(other) => eprintln!("banc: unexpected reply to Renew: {other:?}"),
148 Err(e) => {
149 eprintln!("banc: lease renew failed ({e}); reconnecting");
150 match reacquire(addr, token, holder, ttl) {
151 Ok((s, new_id)) => {
152 stream = s;
153 id = new_id;
154 }
155 Err(e) => eprintln!("banc: lease reconnect failed: {e}"),
156 }
157 }
158 }
159 }
160}
161
162fn reacquire(
163 addr: &str,
164 token: &str,
165 holder: &str,
166 ttl: Duration,
167) -> anyhow::Result<(TcpStream, u64)> {
168 let mut stream = connect(addr, token)?;
169 let req = LeaseRequest::Acquire { holder: holder.to_owned(), ttl_s: ttl.as_secs() as u32 };
170 match roundtrip(&mut stream, &req)? {
171 LeaseReply::Granted { id } => Ok((stream, id)),
172 LeaseReply::Busy { holder: other, .. } => {
173 anyhow::bail!("lease now held by '{other}'")
174 }
175 other => anyhow::bail!("unexpected reply to Acquire: {other:?}"),
176 }
177}
178
179pub struct LeaseServer {
185 state: std::sync::Mutex<Option<Held>>,
186 next_id: std::sync::atomic::AtomicU64,
187}
188
189struct Held {
190 id: u64,
191 holder: String,
192 expires_at: Instant,
193 ttl: Duration,
194}
195
196impl Default for LeaseServer {
197 fn default() -> Self {
198 Self::new()
199 }
200}
201
202impl LeaseServer {
203 pub fn new() -> Self {
204 LeaseServer {
205 state: std::sync::Mutex::new(None),
206 next_id: std::sync::atomic::AtomicU64::new(1),
207 }
208 }
209
210 pub fn holder(&self) -> Option<String> {
212 let state = self.state.lock().unwrap();
213 state
214 .as_ref()
215 .filter(|h| h.expires_at > Instant::now())
216 .map(|h| h.holder.clone())
217 }
218
219 pub fn handle(&self, req: LeaseRequest) -> LeaseReply {
220 let mut state = self.state.lock().unwrap();
221 let now = Instant::now();
222 if state.as_ref().is_some_and(|h| h.expires_at <= now) {
223 *state = None;
224 }
225 match req {
226 LeaseRequest::Acquire { holder, ttl_s } => match &*state {
227 Some(held) => LeaseReply::Busy {
228 holder: held.holder.clone(),
229 expires_in_s: held.expires_at.saturating_duration_since(now).as_secs() as u32,
230 },
231 None => {
232 let id = self.next_id.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
233 let ttl = Duration::from_secs(ttl_s.clamp(5, 3600) as u64);
234 *state = Some(Held { id, holder, expires_at: now + ttl, ttl });
235 LeaseReply::Granted { id }
236 }
237 },
238 LeaseRequest::Renew { id } => match state.as_mut() {
239 Some(held) if held.id == id => {
240 held.expires_at = now + held.ttl;
241 LeaseReply::Ok
242 }
243 _ => LeaseReply::Gone,
244 },
245 LeaseRequest::Release { id } => {
246 if state.as_ref().is_some_and(|h| h.id == id) {
247 *state = None;
248 }
249 LeaseReply::Ok
250 }
251 }
252 }
253
254 pub async fn serve_conn(&self, stream: &mut tokio::net::TcpStream) -> anyhow::Result<()> {
257 loop {
258 let frame = match super::read_frame(stream).await {
259 Ok(f) => f,
260 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(()),
262 Err(e) => return Err(e.into()),
263 };
264 let req: LeaseRequest = postcard::from_bytes(&frame)?;
265 let reply = self.handle(req);
266 super::write_frame(stream, &postcard::to_stdvec(&reply)?).await?;
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn lease_excludes_second_holder_until_released() {
277 let srv = LeaseServer::new();
278 let LeaseReply::Granted { id } =
279 srv.handle(LeaseRequest::Acquire { holder: "a".into(), ttl_s: 60 })
280 else {
281 panic!("first acquire must be granted");
282 };
283 assert!(matches!(
284 srv.handle(LeaseRequest::Acquire { holder: "b".into(), ttl_s: 60 }),
285 LeaseReply::Busy { .. }
286 ));
287 assert!(matches!(srv.handle(LeaseRequest::Renew { id }), LeaseReply::Ok));
288 srv.handle(LeaseRequest::Release { id });
289 assert!(matches!(
290 srv.handle(LeaseRequest::Acquire { holder: "b".into(), ttl_s: 60 }),
291 LeaseReply::Granted { .. }
292 ));
293 }
294
295 #[test]
296 fn expired_lease_is_displaced_and_stale_renew_refused() {
297 let srv = LeaseServer::new();
298 let LeaseReply::Granted { id: stale } =
299 srv.handle(LeaseRequest::Acquire { holder: "a".into(), ttl_s: 5 })
300 else {
301 panic!("first acquire must be granted");
302 };
303 srv.state.lock().unwrap().as_mut().unwrap().expires_at =
305 Instant::now() - Duration::from_secs(1);
306 assert!(matches!(
307 srv.handle(LeaseRequest::Acquire { holder: "b".into(), ttl_s: 60 }),
308 LeaseReply::Granted { .. }
309 ));
310 assert!(matches!(srv.handle(LeaseRequest::Renew { id: stale }), LeaseReply::Gone));
311 }
312}