1use std::collections::BTreeMap;
24use std::sync::atomic::{AtomicU64, Ordering};
25use std::sync::{Arc, Mutex};
26
27use structfs_core_store::{
28 DetachedFuture, DetachedReader, DetachedWriter, Error, NoCodec, Path, Record, Value,
29};
30
31use crate::gate::CancelToken;
32
33pub struct HandleCx {
35 pub id: u64,
37 pub cancel: CancelToken,
41}
42
43pub trait HandleProtocol: Send + Sync + 'static {
49 type Handle: Send + Sync + 'static;
52
53 fn open(&self, cx: HandleCx, request: Value) -> Result<Self::Handle, Error>;
58
59 fn read(&self, handle: Arc<Self::Handle>, sub: Path) -> DetachedFuture<Option<Record>>;
62
63 fn write(&self, handle: Arc<Self::Handle>, sub: Path, data: Record) -> DetachedFuture<Path>;
67
68 fn close(&self, handle: Arc<Self::Handle>) {
70 let _ = handle;
71 }
72
73 fn docs(&self) -> Option<Value> {
75 None
76 }
77}
78
79struct Entry<H> {
80 handle: Arc<H>,
81 cancel: CancelToken,
82}
83
84struct Inner<P: HandleProtocol> {
85 protocol: Arc<P>,
86 next_id: AtomicU64,
87 entries: Mutex<BTreeMap<u64, Entry<P::Handle>>>,
88}
89
90impl<P: HandleProtocol> Drop for Inner<P> {
91 fn drop(&mut self) {
97 let entries = std::mem::take(self.entries.get_mut().unwrap_or_else(|e| e.into_inner()));
98 for (_, entry) in entries {
99 entry.cancel.cancel();
100 self.protocol.close(entry.handle);
101 }
102 }
103}
104
105pub struct HandleStore<P: HandleProtocol> {
110 inner: Arc<Inner<P>>,
111}
112
113impl<P: HandleProtocol> Clone for HandleStore<P> {
114 fn clone(&self) -> Self {
115 Self {
116 inner: self.inner.clone(),
117 }
118 }
119}
120
121const OUTSTANDING: &str = "outstanding";
122
123impl<P: HandleProtocol> HandleStore<P> {
124 pub fn new(protocol: P) -> Self {
126 Self {
127 inner: Arc::new(Inner {
128 protocol: Arc::new(protocol),
129 next_id: AtomicU64::new(0),
130 entries: Mutex::new(BTreeMap::new()),
131 }),
132 }
133 }
134
135 fn lock_entries(&self) -> std::sync::MutexGuard<'_, BTreeMap<u64, Entry<P::Handle>>> {
136 self.inner.entries.lock().unwrap_or_else(|e| e.into_inner())
137 }
138
139 pub fn handle_path(id: u64) -> Path {
141 Path::from_components(vec![OUTSTANDING.to_string(), id.to_string()])
142 }
143
144 pub fn live_handles(&self) -> usize {
146 self.lock_entries().len()
147 }
148
149 pub fn handle_ids(&self) -> Vec<u64> {
153 self.lock_entries().keys().copied().collect()
154 }
155
156 pub fn get_handle(&self, id: u64) -> Option<Arc<P::Handle>> {
161 self.get(id)
162 }
163
164 fn mint(&self, request: Value) -> Result<Path, Error> {
165 let id = self.inner.next_id.fetch_add(1, Ordering::SeqCst);
166 let cancel = CancelToken::new();
167 let handle = self.inner.protocol.open(
168 HandleCx {
169 id,
170 cancel: cancel.clone(),
171 },
172 request,
173 )?;
174 self.lock_entries().insert(
175 id,
176 Entry {
177 handle: Arc::new(handle),
178 cancel,
179 },
180 );
181 Ok(Self::handle_path(id))
182 }
183
184 fn release(&self, id: u64) {
185 let entry = self.lock_entries().remove(&id);
186 if let Some(entry) = entry {
187 entry.cancel.cancel();
190 self.inner.protocol.close(entry.handle);
191 }
192 }
193
194 fn get(&self, id: u64) -> Option<Arc<P::Handle>> {
195 self.lock_entries().get(&id).map(|e| e.handle.clone())
196 }
197
198 fn listing(&self) -> Value {
199 let items: Vec<Value> = self
200 .lock_entries()
201 .keys()
202 .map(|id| Value::String(Self::handle_path(*id).to_string()))
203 .collect();
204 let mut map = BTreeMap::new();
205 map.insert("items".to_string(), Value::Array(items));
206 Value::Map(map)
207 }
208
209 fn parse_handle(path: &Path) -> Option<(u64, Path)> {
211 if path.len() < 2 || &path[0] != OUTSTANDING {
212 return None;
213 }
214 let id: u64 = path[1].parse().ok()?;
215 Some((id, path.slice(2, path.len())))
216 }
217}
218
219impl<P: HandleProtocol> DetachedReader for HandleStore<P> {
220 fn read_detached(&mut self, from: &Path) -> DetachedFuture<Option<Record>> {
221 if from.is_empty() || (from.len() == 1 && &from[0] == OUTSTANDING) {
223 let listing = self.listing();
224 return Box::pin(async move { Ok(Some(Record::parsed(listing))) });
225 }
226 if from.len() == 1 && &from[0] == "docs" {
227 let docs = self.inner.protocol.docs();
228 return Box::pin(async move { Ok(docs.map(Record::parsed)) });
229 }
230 let Some((id, sub)) = Self::parse_handle(from) else {
231 return Box::pin(async move { Ok(None) });
232 };
233 let Some(handle) = self.get(id) else {
234 return Box::pin(async move { Ok(None) });
236 };
237 self.inner.protocol.read(handle, sub)
238 }
239}
240
241impl<P: HandleProtocol> DetachedWriter for HandleStore<P> {
242 fn write_detached(&mut self, to: &Path, data: Record) -> DetachedFuture<Path> {
243 if to.is_empty() {
245 let result = data.into_value(&NoCodec).and_then(|value| self.mint(value));
246 return Box::pin(async move { result });
247 }
248
249 let Some((id, sub)) = Self::parse_handle(to) else {
250 let path = to.clone();
251 return Box::pin(async move {
252 Err(Error::store(
253 "handle_store",
254 "write",
255 format!("no such path: {}", path),
256 ))
257 });
258 };
259
260 if sub.is_empty() {
261 let result = match data.into_value(&NoCodec) {
263 Err(e) => Err(e),
264 Ok(value) if value.is_null() => {
265 self.release(id);
266 Ok(to.clone())
267 }
268 Ok(_) => Err(Error::conflict(format!(
269 "cannot overwrite outstanding handle {}; write Null to release it",
270 Self::handle_path(id)
271 ))),
272 };
273 return Box::pin(async move { result });
274 }
275
276 let Some(handle) = self.get(id) else {
277 let path = to.clone();
278 return Box::pin(async move { Err(Error::not_found(path)) });
279 };
280 let fut = self.inner.protocol.write(handle, sub, data);
281 Box::pin(async move {
284 let rel = fut.await?;
285 Ok(Self::handle_path(id).join(&rel))
286 })
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::tail::TailLog;
294
295 struct StreamProtocol;
299
300 struct StreamHandle {
301 log: TailLog<Value>,
302 cancel: CancelToken,
303 }
304
305 impl HandleProtocol for StreamProtocol {
306 type Handle = StreamHandle;
307
308 fn open(&self, cx: HandleCx, _request: Value) -> Result<Self::Handle, Error> {
309 Ok(StreamHandle {
310 log: TailLog::new(),
311 cancel: cx.cancel,
312 })
313 }
314
315 fn read(&self, handle: Arc<Self::Handle>, sub: Path) -> DetachedFuture<Option<Record>> {
316 Box::pin(async move {
317 if sub.len() == 3 && &sub[0] == "events" && &sub[1] == "from" {
318 let seq: u64 = sub[2]
319 .parse()
320 .map_err(|_| Error::store("stream", "read", "bad cursor"))?;
321 let page = handle
322 .log
323 .read_from_cancellable(seq, &handle.cancel)
324 .await
325 .map_err(|c| c.into_error("stream handle released"))?;
326 return Ok(Some(Record::parsed(page.into_value())));
327 }
328 if sub.len() == 1 && &sub[0] == "status" {
329 let status = if handle.log.is_done() { "done" } else { "open" };
330 return Ok(Some(Record::parsed(Value::from(status))));
331 }
332 Ok(None)
333 })
334 }
335
336 fn write(
337 &self,
338 handle: Arc<Self::Handle>,
339 sub: Path,
340 data: Record,
341 ) -> DetachedFuture<Path> {
342 Box::pin(async move {
343 if sub.len() == 1 && &sub[0] == "push" {
344 let value = data.into_value(&NoCodec)?;
345 handle.log.push(value);
346 return Ok(sub);
347 }
348 if sub.len() == 1 && &sub[0] == "done" {
349 handle.log.finish();
350 return Ok(sub);
351 }
352 Err(Error::store("stream", "write", "unknown sub-path"))
353 })
354 }
355
356 fn close(&self, handle: Arc<Self::Handle>) {
357 handle.log.finish();
358 }
359 }
360
361 fn store() -> HandleStore<StreamProtocol> {
362 HandleStore::new(StreamProtocol)
363 }
364
365 fn parsed(v: Value) -> Record {
366 Record::parsed(v)
367 }
368
369 #[tokio::test]
370 async fn mint_returns_handle_path() {
371 let mut s = store();
372 let path = s
373 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("req")))
374 .await
375 .unwrap();
376 assert_eq!(path.to_string(), "outstanding/0");
377
378 let second = s
379 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("req")))
380 .await
381 .unwrap();
382 assert_eq!(second.to_string(), "outstanding/1");
383 }
384
385 #[tokio::test]
386 async fn overwrite_is_conflict() {
387 let mut s = store();
388 let path = s
389 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("r")))
390 .await
391 .unwrap();
392 let err = s
393 .write_detached(&path, parsed(Value::from("clobber")))
394 .await
395 .unwrap_err();
396 assert!(matches!(err, Error::Conflict { .. }));
397 }
398
399 #[tokio::test]
400 async fn write_result_is_in_caller_namespace() {
401 let mut s = store();
402 let path = s
403 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("r")))
404 .await
405 .unwrap();
406 let result = s
407 .write_detached(
408 &path.join(&Path::parse("push").unwrap()),
409 parsed(Value::from(1i64)),
410 )
411 .await
412 .unwrap();
413 assert_eq!(result.to_string(), format!("{}/push", path));
414 }
415
416 #[tokio::test]
417 async fn tail_read_through_store() {
418 let mut s = store();
419 let handle = s
420 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("r")))
421 .await
422 .unwrap();
423 s.write_detached(
424 &handle.join(&Path::parse("push").unwrap()),
425 parsed(Value::from(1i64)),
426 )
427 .await
428 .unwrap();
429 s.write_detached(
430 &handle.join(&Path::parse("done").unwrap()),
431 parsed(Value::Null),
432 )
433 .await
434 .unwrap();
435
436 let record = s
437 .read_detached(&handle.join(&Path::parse("events/from/0").unwrap()))
438 .await
439 .unwrap()
440 .unwrap();
441 let map = match record.as_value().unwrap() {
442 Value::Map(m) => m.clone(),
443 _ => panic!("expected envelope"),
444 };
445 assert_eq!(map.get("status"), Some(&Value::from("done")));
446 assert!(matches!(map.get("items"), Some(Value::Array(a)) if a.len() == 1));
447 }
448
449 #[tokio::test]
450 async fn release_cancels_parked_reads() {
451 let mut s = store();
452 let handle = s
453 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("r")))
454 .await
455 .unwrap();
456
457 let mut reader = s.clone();
459 let tail_path = handle.join(&Path::parse("events/from/0").unwrap());
460 let parked = tokio::spawn(async move { reader.read_detached(&tail_path).await });
461 tokio::task::yield_now().await;
462
463 s.write_detached(&handle, parsed(Value::Null))
465 .await
466 .unwrap();
467 let err = parked.await.unwrap().unwrap_err();
468 assert!(err.is_cancelled());
469
470 assert!(s.read_detached(&handle).await.unwrap().is_none());
472 }
473
474 #[tokio::test]
475 async fn dropping_the_store_releases_live_handles() {
476 let s = store();
477 let handle_path = {
478 let mut s = s.clone();
479 s.write_detached(&Path::parse("").unwrap(), parsed(Value::from("r")))
480 .await
481 .unwrap()
482 };
483
484 let tail = handle_path.join(&Path::parse("events/from/0").unwrap());
487 let fut = {
488 let mut reader = s.clone();
489 reader.read_detached(&tail)
490 };
491 let parked = tokio::spawn(fut);
492 tokio::task::yield_now().await;
493 drop(s);
494
495 let err = tokio::time::timeout(std::time::Duration::from_secs(5), parked)
496 .await
497 .expect("parked read never resolved after store drop")
498 .unwrap()
499 .unwrap_err();
500 assert!(err.is_cancelled());
501 }
502
503 #[tokio::test]
504 async fn release_is_idempotent() {
505 let mut s = store();
506 let handle = s
507 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("r")))
508 .await
509 .unwrap();
510 s.write_detached(&handle, parsed(Value::Null))
511 .await
512 .unwrap();
513 s.write_detached(&handle, parsed(Value::Null))
515 .await
516 .unwrap();
517 s.write_detached(
519 &Path::parse("outstanding/999").unwrap(),
520 parsed(Value::Null),
521 )
522 .await
523 .unwrap();
524 }
525
526 #[tokio::test]
527 async fn listing_tracks_live_handles() {
528 let mut s = store();
529 let a = s
530 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("a")))
531 .await
532 .unwrap();
533 let _b = s
534 .write_detached(&Path::parse("").unwrap(), parsed(Value::from("b")))
535 .await
536 .unwrap();
537
538 let listing = s
539 .read_detached(&Path::parse("outstanding").unwrap())
540 .await
541 .unwrap()
542 .unwrap();
543 let items = match listing.as_value().unwrap() {
544 Value::Map(m) => match m.get("items").unwrap() {
545 Value::Array(a) => a.len(),
546 _ => panic!(),
547 },
548 _ => panic!(),
549 };
550 assert_eq!(items, 2);
551
552 s.write_detached(&a, parsed(Value::Null)).await.unwrap();
553 assert_eq!(s.live_handles(), 1);
554 }
555
556 #[tokio::test]
557 async fn unknown_paths_absent() {
558 let mut s = store();
559 assert!(s
560 .read_detached(&Path::parse("outstanding/42").unwrap())
561 .await
562 .unwrap()
563 .is_none());
564 assert!(s
565 .read_detached(&Path::parse("something/else").unwrap())
566 .await
567 .unwrap()
568 .is_none());
569 let err = s
570 .write_detached(
571 &Path::parse("outstanding/42/push").unwrap(),
572 parsed(Value::from(1i64)),
573 )
574 .await
575 .unwrap_err();
576 assert!(err.is_not_found());
577 }
578
579 #[test]
580 fn handle_path_component_is_valid() {
581 let p = HandleStore::<StreamProtocol>::handle_path(7);
582 assert_eq!(p.to_string(), "outstanding/7");
583 let _ = structfs_core_store::PathComponent::try_new("outstanding").unwrap();
584 }
585}