1use super::*;
15
16mod segment_handover;
17mod support;
18
19use support::process_status_label;
20pub(crate) use support::tx_outcome;
21
22#[async_trait::async_trait]
23impl ProcessRegistry for SqliteProcessRegistry {
24 fn durability_tier(&self) -> DurabilityTier {
25 DurabilityTier::Durable
26 }
27
28 async fn register_process(
29 &self,
30 registration: ProcessRegistration,
31 ) -> Result<ProcessRecord, lash_core::PluginError> {
32 let (registration, registration_hash) = prepare_process_registration(registration)?;
33 let now = self.clock.timestamp_ms();
34 let record = self
35 .conn
36 .write_flow(move |tx| {
37 Ok(tx_outcome((|| {
38 if let Some(existing) = Self::load_process_conn(tx, ®istration.id)? {
39 if existing.registration_hash == registration_hash {
40 return Ok(existing);
41 }
42 return Err(lash_core::PluginError::Session(format!(
43 "process `{}` registration hash conflict: existing {}, new {}",
44 registration.id, existing.registration_hash, registration_hash
45 )));
46 }
47 let record = ProcessRecord::from_prepared_registration(
48 registration,
49 registration_hash,
50 now,
51 );
52 let originator_scope_id = record.originator_scope_id();
53 let change_seq = Self::next_change_seq_conn(tx)?;
54 tx.execute(
55 "INSERT INTO processes (
56 process_id, registration_hash, owner_scope_id,
57 created_at_ms, updated_at_ms, change_seq, status, record_json
58 )
59 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
60 params![
61 record.id.as_str(),
62 record.registration_hash.as_str(),
63 originator_scope_id.as_str(),
64 record.created_at_ms as i64,
65 record.updated_at_ms as i64,
66 change_seq as i64,
67 process_status_label(&record),
68 process_encode_json(&record)?,
69 ],
70 )
71 .map_err(process_sqlite_error)?;
72 Ok(record)
73 })()))
74 })
75 .await
76 .map_err(process_sqlite_error)??;
77 Ok(record)
78 }
79
80 async fn put_segment_handover(
81 &self,
82 process_id: &str,
83 handover: PersistedSegmentHandover,
84 ) -> Result<(), lash_core::PluginError> {
85 self.put_segment_handover_impl(process_id, handover).await
86 }
87
88 async fn get_segment_handover(
89 &self,
90 process_id: &str,
91 segment_ordinal: u64,
92 ) -> Result<Option<PersistedSegmentHandover>, lash_core::PluginError> {
93 self.get_segment_handover_impl(process_id, segment_ordinal)
94 .await
95 }
96
97 async fn latest_segment_handover(
98 &self,
99 process_id: &str,
100 ) -> Result<Option<PersistedSegmentHandover>, lash_core::PluginError> {
101 self.latest_segment_handover_impl(process_id).await
102 }
103
104 async fn delete_segment_handovers(
105 &self,
106 process_id: &str,
107 ) -> Result<(), lash_core::PluginError> {
108 self.delete_segment_handovers_impl(process_id).await
109 }
110
111 async fn set_external_ref(
112 &self,
113 process_id: &str,
114 external_ref: ProcessExternalRef,
115 ) -> Result<ProcessRecord, lash_core::PluginError> {
116 let process_id = process_id.to_string();
117 let now = self.clock.timestamp_ms();
118 let (record, _changed) = self
119 .conn
120 .write_flow(move |tx| {
121 Ok(tx_outcome((|| {
122 let mut record =
123 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
124 lash_core::PluginError::Session(format!(
125 "unknown process `{process_id}`"
126 ))
127 })?;
128 if let Some(existing) = &record.external_ref {
129 if existing == &external_ref {
130 return Ok((record, false));
131 }
132 return Err(process_external_ref_conflict(
133 &process_id,
134 existing,
135 &external_ref,
136 ));
137 }
138 record.external_ref = Some(external_ref);
139 record.updated_at_ms = now;
140 Self::save_process_conn(tx, &record)?;
141 Ok((record, true))
142 })()))
143 })
144 .await
145 .map_err(process_sqlite_error)??;
146 Ok(record)
147 }
148
149 async fn grant_handle(
150 &self,
151 session_scope: &SessionScope,
152 process_id: &str,
153 descriptor: ProcessHandleDescriptor,
154 ) -> Result<ProcessHandleGrant, lash_core::PluginError> {
155 let session_scope = session_scope.clone();
156 let process_id = process_id.to_string();
157 self.conn
158 .write_flow(move |tx| {
159 Ok(tx_outcome((|| {
160 let session_scope_id = session_scope.id();
161 if Self::load_process_conn(tx, &process_id)?.is_none() {
162 return Err(lash_core::PluginError::Session(format!(
163 "unknown process `{process_id}`"
164 )));
165 }
166 tx.execute(
167 "INSERT INTO process_handle_grants (session_id, scope_id, process_id, descriptor_json)
168 VALUES (?1, ?2, ?3, ?4)
169 ON CONFLICT(scope_id, process_id) DO UPDATE SET
170 session_id = excluded.session_id,
171 descriptor_json = excluded.descriptor_json",
172 params![
173 session_scope.session_id.as_str(),
174 session_scope_id.as_str(),
175 process_id.as_str(),
176 process_encode_json(&descriptor)?
177 ],
178 )
179 .map_err(process_sqlite_error)?;
180 Ok(ProcessHandleGrant {
181 session_id: session_scope.session_id.clone(),
182 process_id: process_id.clone(),
183 descriptor,
184 })
185 })()))
186 })
187 .await
188 .map_err(process_sqlite_error)?
189 }
190
191 async fn revoke_handle(
192 &self,
193 session_scope: &SessionScope,
194 process_id: &str,
195 ) -> Result<(), lash_core::PluginError> {
196 let session_scope_id = session_scope.id().as_str().to_string();
197 let process_id = process_id.to_string();
198 self.conn
199 .call(move |conn| {
200 conn.execute(
201 "DELETE FROM process_handle_grants WHERE scope_id = ?1 AND process_id = ?2",
202 params![session_scope_id, process_id],
203 )
204 })
205 .await
206 .map_err(process_sqlite_error)?;
207 Ok(())
208 }
209
210 async fn transfer_handle_grants(
211 &self,
212 from_scope: &SessionScope,
213 to_scope: &SessionScope,
214 process_ids: &[String],
215 ) -> Result<(), lash_core::PluginError> {
216 let from_scope = from_scope.clone();
217 let to_scope = to_scope.clone();
218 let process_ids = process_ids.to_vec();
219 self.conn
220 .write_flow(move |tx| {
221 Ok(tx_outcome((|| {
222 let from_scope_id = from_scope.id();
223 let to_scope_id = to_scope.id();
224 for process_id in &process_ids {
225 let descriptor_json: Option<String> = tx
226 .query_row(
227 "SELECT descriptor_json
228 FROM process_handle_grants
229 WHERE scope_id = ?1 AND process_id = ?2",
230 params![from_scope_id.as_str(), process_id.as_str()],
231 |row| row.get(0),
232 )
233 .optional()
234 .map_err(process_sqlite_error)?;
235 let Some(descriptor_json) = descriptor_json else {
236 return Err(lash_core::PluginError::Session(format!(
237 "process handle `{process_id}` is not granted to session `{}`",
238 from_scope.session_id
239 )));
240 };
241 tx.execute(
242 "DELETE FROM process_handle_grants
243 WHERE scope_id = ?1 AND process_id = ?2",
244 params![from_scope_id.as_str(), process_id.as_str()],
245 )
246 .map_err(process_sqlite_error)?;
247 tx.execute(
248 "INSERT INTO process_handle_grants (session_id, scope_id, process_id, descriptor_json)
249 VALUES (?1, ?2, ?3, ?4)
250 ON CONFLICT(scope_id, process_id) DO UPDATE SET
251 session_id = excluded.session_id,
252 descriptor_json = excluded.descriptor_json",
253 params![
254 to_scope.session_id.as_str(),
255 to_scope_id.as_str(),
256 process_id.as_str(),
257 descriptor_json
258 ],
259 )
260 .map_err(process_sqlite_error)?;
261 }
262 Ok(())
263 })()))
264 })
265 .await
266 .map_err(process_sqlite_error)?
267 }
268
269 async fn list_handle_grants(
270 &self,
271 session_scope: &SessionScope,
272 ) -> Result<Vec<ProcessHandleGrantEntry>, lash_core::PluginError> {
273 let session_scope = session_scope.clone();
274 self.conn
275 .call(move |conn| {
276 Ok(Self::list_grants_for_scope_conn(
277 conn,
278 &session_scope,
279 false,
280 ))
281 })
282 .await
283 .map_err(process_sqlite_error)?
284 }
285
286 async fn list_live_handle_grants(
287 &self,
288 session_scope: &SessionScope,
289 ) -> Result<Vec<ProcessHandleGrantEntry>, lash_core::PluginError> {
290 let session_scope = session_scope.clone();
291 self.conn
292 .call(move |conn| Ok(Self::list_grants_for_scope_conn(conn, &session_scope, true)))
293 .await
294 .map_err(process_sqlite_error)?
295 }
296
297 async fn has_handle_grant(
298 &self,
299 session_scope: &SessionScope,
300 process_id: &str,
301 ) -> Result<bool, lash_core::PluginError> {
302 let session_scope_id = session_scope.id().as_str().to_string();
303 let process_id = process_id.to_string();
304 self.conn
305 .call(move |conn| {
306 let exists = conn
307 .query_row(
308 "SELECT 1
309 FROM process_handle_grants g
310 JOIN processes p ON p.process_id = g.process_id
311 WHERE g.scope_id = ?1 AND g.process_id = ?2
312 LIMIT 1",
313 params![session_scope_id, process_id],
314 |_| Ok(()),
315 )
316 .optional()?
317 .is_some();
318 Ok(exists)
319 })
320 .await
321 .map_err(process_sqlite_error)
322 }
323
324 async fn handle_grants_for_process(
325 &self,
326 process_id: &str,
327 ) -> Result<Vec<ProcessHandleGrant>, lash_core::PluginError> {
328 let process_id = process_id.to_string();
329 self.conn
330 .call(move |conn| {
331 Ok((|| {
332 if Self::load_process_conn(conn, &process_id)?.is_none() {
333 return Err(lash_core::PluginError::Session(format!(
334 "unknown process `{process_id}`"
335 )));
336 }
337 let mut stmt = conn
338 .prepare(
339 "SELECT session_id, descriptor_json
340 FROM process_handle_grants
341 WHERE process_id = ?1
342 ORDER BY session_id ASC, scope_id ASC",
343 )
344 .map_err(process_sqlite_error)?;
345 let rows = stmt
346 .query_map(params![process_id], |row| {
347 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
348 })
349 .map_err(process_sqlite_error)?;
350 let mut grants = Vec::new();
351 for row in rows {
352 let (session_id, descriptor_json) = row.map_err(process_sqlite_error)?;
353 let descriptor: ProcessHandleDescriptor =
354 serde_json::from_str(&descriptor_json).map_err(process_decode_error)?;
355 grants.push(ProcessHandleGrant {
356 session_id,
357 process_id: process_id.clone(),
358 descriptor,
359 });
360 }
361 Ok(grants)
362 })())
363 })
364 .await
365 .map_err(process_sqlite_error)?
366 }
367
368 async fn delete_session_process_state(
369 &self,
370 session_id: &str,
371 ) -> Result<lash_core::ProcessSessionDeleteReport, lash_core::PluginError> {
372 let session_id_owned = session_id.to_string();
373 let (
374 revoked_handle_count,
375 deleted_wake_count,
376 mut orphaned_process_ids,
377 mut preserved_process_ids,
378 ) = self
379 .conn
380 .write_flow(move |tx| {
381 Ok(tx_outcome((|| {
382 let session_id = session_id_owned;
383 let removed = {
384 let mut stmt = tx
385 .prepare(
386 "SELECT g.process_id, p.record_json
387 FROM process_handle_grants g
388 JOIN processes p ON p.process_id = g.process_id
389 WHERE g.session_id = ?1
390 ORDER BY g.process_id ASC",
391 )
392 .map_err(process_sqlite_error)?;
393 let rows = stmt
394 .query_map(params![session_id], |row| {
395 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
396 })
397 .map_err(process_sqlite_error)?;
398 let mut removed = Vec::new();
399 for row in rows {
400 let (process_id, record_json) = row.map_err(process_sqlite_error)?;
401 let record: ProcessRecord =
402 serde_json::from_str(&record_json).map_err(process_decode_error)?;
403 removed.push((process_id, record));
404 }
405 removed
406 };
407
408 let deleted_wake_count = 0;
413 let revoked_handle_count = tx
414 .execute(
415 "DELETE FROM process_handle_grants WHERE session_id = ?1",
416 params![session_id],
417 )
418 .map_err(process_sqlite_error)?;
419 let mut orphaned_process_ids = Vec::new();
420 let mut preserved_process_ids = Vec::new();
421 for (process_id, record) in removed {
422 if record.is_terminal() {
423 continue;
424 }
425 let remaining_grants: i64 = tx
426 .query_row(
427 "SELECT COUNT(*) FROM process_handle_grants WHERE process_id = ?1",
428 params![process_id],
429 |row| row.get(0),
430 )
431 .map_err(process_sqlite_error)?;
432 if remaining_grants == 0 {
433 orphaned_process_ids.push(process_id);
434 } else {
435 preserved_process_ids.push(process_id);
436 }
437 }
438 let wake_targeted = {
439 let mut stmt = tx
440 .prepare("SELECT process_id, record_json FROM processes ORDER BY process_id ASC")
441 .map_err(process_sqlite_error)?;
442 let rows = stmt
443 .query_map([], |row| {
444 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
445 })
446 .map_err(process_sqlite_error)?;
447 let mut records = Vec::new();
448 for row in rows {
449 let (process_id, record_json) = row.map_err(process_sqlite_error)?;
450 let record: ProcessRecord =
451 serde_json::from_str(&record_json).map_err(process_decode_error)?;
452 records.push((process_id, record));
453 }
454 records
455 };
456 for (_process_id, mut record) in wake_targeted {
457 if record.clear_wake_target_for_session(&session_id) {
458 Self::save_process_conn(tx, &record)?;
459 }
460 }
461 Ok((
462 revoked_handle_count,
463 deleted_wake_count,
464 orphaned_process_ids,
465 preserved_process_ids,
466 ))
467 })()))
468 })
469 .await
470 .map_err(process_sqlite_error)??;
471 orphaned_process_ids.sort();
472 orphaned_process_ids.dedup();
473 preserved_process_ids.sort();
474 preserved_process_ids.dedup();
475 Ok(lash_core::ProcessSessionDeleteReport {
476 session_id: session_id.to_string(),
477 revoked_handle_count,
478 deleted_wake_count,
479 orphaned_process_ids,
480 preserved_process_ids,
481 })
482 }
483
484 async fn append_event(
485 &self,
486 process_id: &str,
487 request: ProcessEventAppendRequest,
488 ) -> Result<ProcessEventAppendResult, lash_core::PluginError> {
489 let process_id = process_id.to_string();
490 let occurred_at_ms = self.clock.timestamp_ms();
491 let (result, _appended) = self
492 .conn
493 .write_flow(move |tx| {
494 Ok(tx_outcome((|| {
495 let mut record =
496 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
497 lash_core::PluginError::Session(format!(
498 "unknown process `{process_id}`"
499 ))
500 })?;
501 let replay_lookup = if let Some(replay_key) =
502 request.replay.as_ref().map(|replay| replay.key.as_str())
503 {
504 Self::load_event_by_key_conn(tx, &process_id, replay_key)?
505 } else {
506 None
507 };
508 let sequence = tx
509 .query_row(
510 "SELECT COALESCE(MAX(sequence), 0) + 1 FROM process_events WHERE process_id = ?1",
511 params![process_id],
512 |row| row.get::<_, i64>(0),
513 )
514 .map_err(process_sqlite_error)? as u64;
515 let prepared = prepare_process_event_append(
516 &record,
517 request,
518 sequence,
519 replay_lookup,
520 occurred_at_ms,
521 )?;
522 match prepared {
523 lash_core::ProcessEventAppendPlan::Replay {
524 event,
525 repair_status,
526 wake_delivery,
527 occurred_at_ms,
528 } => {
529 let repaired = if let Some(status) = repair_status {
530 lash_core::apply_process_status_projection(
531 &mut record,
532 status,
533 occurred_at_ms,
534 );
535 Self::save_process_conn(tx, &record)?;
536 true
537 } else {
538 false
539 };
540 Ok((
541 ProcessEventAppendResult {
542 event,
543 wake_delivery,
544 },
545 repaired,
546 ))
547 }
548 lash_core::ProcessEventAppendPlan::Insert {
549 event,
550 payload_hash,
551 status_update,
552 wake_delivery,
553 occurred_at_ms,
554 } => {
555 tx.execute(
556 "INSERT INTO process_events (
557 process_id, sequence, event_type, payload_hash, idempotency_key,
558 occurred_at_ms, event_json
559 )
560 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
561 params![
562 process_id,
563 sequence as i64,
564 event.event_type.as_str(),
565 payload_hash.as_str(),
566 event.invocation.replay_key(),
567 occurred_at_ms as i64,
568 process_encode_json(&event)?,
569 ],
570 )
571 .map_err(process_sqlite_error)?;
572 if let Some(status) = status_update {
573 lash_core::apply_process_status_projection(
574 &mut record,
575 status,
576 occurred_at_ms,
577 );
578 } else {
579 record.updated_at_ms = occurred_at_ms;
580 }
581 Self::save_process_conn(tx, &record)?;
582 Ok((
583 ProcessEventAppendResult {
584 event,
585 wake_delivery,
586 },
587 true,
588 ))
589 }
590 }
591 })()))
592 })
593 .await
594 .map_err(process_sqlite_error)??;
595 Ok(result)
596 }
597
598 async fn events_after(
599 &self,
600 process_id: &str,
601 after_sequence: u64,
602 ) -> Result<Vec<ProcessEvent>, lash_core::PluginError> {
603 let process_id = process_id.to_string();
604 self.conn
605 .call(move |conn| {
606 Ok((|| {
607 if Self::load_process_conn(conn, &process_id)?.is_none() {
608 return Err(lash_core::PluginError::Session(format!(
609 "unknown process `{process_id}`"
610 )));
611 }
612 let mut stmt = conn
613 .prepare(
614 "SELECT event_json FROM process_events
615 WHERE process_id = ?1 AND sequence > ?2
616 ORDER BY sequence ASC",
617 )
618 .map_err(process_sqlite_error)?;
619 let rows = stmt
620 .query_map(params![process_id, after_sequence as i64], |row| {
621 row.get::<_, String>(0)
622 })
623 .map_err(process_sqlite_error)?;
624 let mut events = Vec::new();
625 for row in rows {
626 events.push(
627 serde_json::from_str(&row.map_err(process_sqlite_error)?)
628 .map_err(process_decode_error)?,
629 );
630 }
631 Ok(events)
632 })())
633 })
634 .await
635 .map_err(process_sqlite_error)?
636 }
637
638 async fn count_events_through(
639 &self,
640 process_id: &str,
641 event_type: &str,
642 up_to_sequence: u64,
643 ) -> Result<u64, lash_core::PluginError> {
644 let process_id = process_id.to_string();
645 let event_type = event_type.to_string();
646 self.conn
647 .call(move |conn| {
648 Ok((|| {
649 if Self::load_process_conn(conn, &process_id)?.is_none() {
650 return Err(lash_core::PluginError::Session(format!(
651 "unknown process `{process_id}`"
652 )));
653 }
654 conn.query_row(
655 "SELECT COUNT(*) FROM process_events
656 WHERE process_id = ?1 AND event_type = ?2 AND sequence <= ?3",
657 params![process_id, event_type, up_to_sequence as i64],
658 |row| row.get::<_, i64>(0),
659 )
660 .map(|count| count as u64)
661 .map_err(process_sqlite_error)
662 })())
663 })
664 .await
665 .map_err(process_sqlite_error)?
666 }
667
668 async fn recent_events(
669 &self,
670 process_id: &str,
671 limit: usize,
672 ) -> Result<Vec<ProcessEvent>, lash_core::PluginError> {
673 let process_id = process_id.to_string();
674 self.conn
675 .call(move |conn| {
676 Ok((|| {
677 if Self::load_process_conn(conn, &process_id)?.is_none() {
678 return Err(lash_core::PluginError::Session(format!(
679 "unknown process `{process_id}`"
680 )));
681 }
682 let mut stmt = conn
683 .prepare(
684 "SELECT event_json FROM process_events
685 WHERE process_id = ?1
686 ORDER BY sequence DESC
687 LIMIT ?2",
688 )
689 .map_err(process_sqlite_error)?;
690 let rows = stmt
691 .query_map(params![process_id, limit as i64], |row| {
692 row.get::<_, String>(0)
693 })
694 .map_err(process_sqlite_error)?;
695 let mut events: Vec<ProcessEvent> = Vec::new();
696 for row in rows {
697 events.push(
698 serde_json::from_str(&row.map_err(process_sqlite_error)?)
699 .map_err(process_decode_error)?,
700 );
701 }
702 events.reverse();
703 Ok(events)
704 })())
705 })
706 .await
707 .map_err(process_sqlite_error)?
708 }
709
710 async fn wake_events_after(
711 &self,
712 process_id: &str,
713 after_sequence: u64,
714 ) -> Result<Vec<ProcessEvent>, lash_core::PluginError> {
715 let acked: std::collections::HashSet<u64> = {
716 let process_id = process_id.to_string();
717 self.conn
718 .call(move |conn| {
719 Ok(
720 (|| -> Result<std::collections::HashSet<u64>, lash_core::PluginError> {
721 let mut stmt = conn
722 .prepare(
723 "SELECT sequence FROM process_wake_acks WHERE process_id = ?1",
724 )
725 .map_err(process_sqlite_error)?;
726 let rows = stmt
727 .query_map(params![process_id], |row| row.get::<_, i64>(0))
728 .map_err(process_sqlite_error)?;
729 let mut set = std::collections::HashSet::new();
730 for row in rows {
731 set.insert(row.map_err(process_sqlite_error)? as u64);
732 }
733 Ok(set)
734 })(),
735 )
736 })
737 .await
738 .map_err(process_sqlite_error)??
739 };
740 Ok(self
741 .events_after(process_id, after_sequence)
742 .await?
743 .into_iter()
744 .filter(|event| event.semantics.wake.is_some() && !acked.contains(&event.sequence))
745 .collect())
746 }
747
748 async fn complete_process(
749 &self,
750 process_id: &str,
751 await_output: ProcessAwaitOutput,
752 authority: lash_core::ProcessCompletionAuthority,
753 ) -> Result<ProcessRecord, lash_core::PluginError> {
754 super::process_registry_completion::complete_process(
759 self,
760 process_id,
761 await_output,
762 authority,
763 )
764 .await
765 }
766
767 async fn complete_process_with_lease(
768 &self,
769 lease: &ProcessLease,
770 await_output: ProcessAwaitOutput,
771 ) -> Result<ProcessRecord, lash_core::PluginError> {
772 super::process_registry_completion::complete_process_with_lease(self, lease, await_output)
773 .await
774 }
775
776 async fn record_first_started(
777 &self,
778 process_id: &str,
779 started: ProcessStarted,
780 ) -> Result<ProcessRecord, lash_core::PluginError> {
781 let process_id = process_id.to_string();
782 let now = self.clock.timestamp_ms();
783 self.conn
784 .write_flow(move |tx| {
785 Ok(tx_outcome((|| {
786 let mut record =
787 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
788 lash_core::PluginError::Session(format!(
789 "unknown process `{process_id}`"
790 ))
791 })?;
792 if record.first_started.is_none() {
794 record.first_started = Some(Box::new(started));
795 record.updated_at_ms = now;
796 Self::save_process_conn(tx, &record)?;
797 }
798 Ok(record)
799 })()))
800 })
801 .await
802 .map_err(process_sqlite_error)?
803 }
804
805 async fn request_process_abandon(
806 &self,
807 process_id: &str,
808 request: AbandonRequest,
809 ) -> Result<ProcessRecord, lash_core::PluginError> {
810 let process_id = process_id.to_string();
811 let now = self.clock.timestamp_ms();
812 self.conn
813 .write_flow(move |tx| {
814 Ok(tx_outcome((|| {
815 let mut record =
816 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
817 lash_core::PluginError::Session(format!(
818 "unknown process `{process_id}`"
819 ))
820 })?;
821 if record.is_terminal() {
822 return Err(lash_core::PluginError::Session(format!(
823 "terminal process `{process_id}` cannot accept an abandon request"
824 )));
825 }
826 if record.abandon_request.is_none() {
828 record.abandon_request = Some(Box::new(request));
829 record.updated_at_ms = now;
830 Self::save_process_conn(tx, &record)?;
831 }
832 Ok(record)
833 })()))
834 })
835 .await
836 .map_err(process_sqlite_error)?
837 }
838
839 async fn set_process_wait(
840 &self,
841 process_id: &str,
842 wait: lash_core::WaitState,
843 ) -> Result<ProcessRecord, lash_core::PluginError> {
844 let process_id = process_id.to_string();
845 let now = self.clock.timestamp_ms();
846 self.conn
847 .write_flow(move |tx| {
848 Ok(tx_outcome((|| {
849 let mut record =
850 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
851 lash_core::PluginError::Session(format!(
852 "unknown process `{process_id}`"
853 ))
854 })?;
855 if record.is_terminal() {
856 return Err(lash_core::PluginError::Session(format!(
857 "terminal process `{process_id}` cannot enter a wait state"
858 )));
859 }
860 record.wait = Some(wait);
861 record.updated_at_ms = now;
862 Self::save_process_conn(tx, &record)?;
863 Ok(record)
864 })()))
865 })
866 .await
867 .map_err(process_sqlite_error)?
868 }
869
870 async fn clear_process_wait(
871 &self,
872 process_id: &str,
873 ) -> Result<ProcessRecord, lash_core::PluginError> {
874 let process_id = process_id.to_string();
875 let now = self.clock.timestamp_ms();
876 self.conn
877 .write_flow(move |tx| {
878 Ok(tx_outcome((|| {
879 let mut record =
880 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
881 lash_core::PluginError::Session(format!(
882 "unknown process `{process_id}`"
883 ))
884 })?;
885 record.wait = None;
886 record.updated_at_ms = now;
887 Self::save_process_conn(tx, &record)?;
888 Ok(record)
889 })()))
890 })
891 .await
892 .map_err(process_sqlite_error)?
893 }
894
895 async fn get_process(&self, process_id: &str) -> Option<ProcessRecord> {
896 let process_id = process_id.to_string();
897 self.conn
898 .call(move |conn| Ok(Self::load_process_conn(conn, &process_id).ok().flatten()))
899 .await
900 .ok()
901 .flatten()
902 }
903
904 async fn try_get_process(
905 &self,
906 process_id: &str,
907 ) -> Result<Option<ProcessRecord>, lash_core::PluginError> {
908 let process_id = process_id.to_string();
909 self.conn
910 .call(move |conn| Ok(Self::load_process_conn(conn, &process_id)))
911 .await
912 .map_err(process_sqlite_error)?
913 }
914
915 async fn list_processes(
916 &self,
917 filter: &lash_core::ProcessListFilter,
918 ) -> Result<Vec<ProcessRecord>, lash_core::PluginError> {
919 let filter = filter.clone();
920 self.conn
921 .call(move |conn| {
922 Ok((|| {
923 let mut stmt = conn
924 .prepare(
925 "SELECT record_json FROM processes
926 ORDER BY process_id ASC",
927 )
928 .map_err(process_sqlite_error)?;
929 let rows = stmt
930 .query_map([], |row| row.get::<_, String>(0))
931 .map_err(process_sqlite_error)?;
932 let mut records = Vec::new();
933 for row in rows {
934 let record: ProcessRecord =
935 serde_json::from_str(&row.map_err(process_sqlite_error)?)
936 .map_err(process_decode_error)?;
937 if filter.matches_record(&record) {
938 records.push(record);
939 }
940 }
941 Ok(records)
942 })())
943 })
944 .await
945 .map_err(process_sqlite_error)?
946 }
947
948 async fn processes_changed_since(
949 &self,
950 cursor: ProcessChangeCursor,
951 limit: usize,
952 ) -> Result<(Vec<ProcessRecord>, ProcessChangeCursor), lash_core::PluginError> {
953 if limit == 0 {
954 return Ok((Vec::new(), cursor));
955 }
956 self.conn
957 .call(move |conn| {
958 Ok(
959 crate::process_registry_change::processes_changed_since_conn(
960 conn, cursor, limit,
961 ),
962 )
963 })
964 .await
965 .map_err(process_sqlite_error)?
966 }
967
968 async fn ack_wake(
969 &self,
970 process_id: &str,
971 sequence: u64,
972 ) -> Result<(), lash_core::PluginError> {
973 let process_id = process_id.to_string();
974 self.conn
975 .call(move |conn| {
976 Ok((|| {
977 if Self::load_process_conn(conn, &process_id)?.is_none() {
978 return Err(lash_core::PluginError::Session(format!(
979 "unknown process `{process_id}`"
980 )));
981 }
982 conn.execute(
983 "INSERT OR IGNORE INTO process_wake_acks (process_id, sequence) VALUES (?1, ?2)",
984 params![process_id, sequence as i64],
985 )
986 .map_err(process_sqlite_error)?;
987 Ok(())
988 })())
989 })
990 .await
991 .map_err(process_sqlite_error)?
992 }
993
994 async fn list_non_terminal(&self) -> Result<Vec<ProcessRecord>, lash_core::PluginError> {
995 self.conn
996 .call(move |conn| {
997 Ok((|| {
998 let mut stmt = conn
999 .prepare(
1000 "SELECT record_json FROM processes
1001 WHERE status = 'running'
1002 ORDER BY process_id ASC",
1003 )
1004 .map_err(process_sqlite_error)?;
1005 let rows = stmt
1006 .query_map([], |row| row.get::<_, String>(0))
1007 .map_err(process_sqlite_error)?;
1008 let mut records = Vec::new();
1009 for row in rows {
1010 let record: ProcessRecord =
1011 serde_json::from_str(&row.map_err(process_sqlite_error)?)
1012 .map_err(process_decode_error)?;
1013 records.push(record);
1014 }
1015 Ok(records)
1016 })())
1017 })
1018 .await
1019 .map_err(process_sqlite_error)?
1020 }
1021
1022 async fn live_reference_summary(
1023 &self,
1024 ) -> Result<Vec<ProcessLiveReferenceSummary>, lash_core::PluginError> {
1025 let records = self.list_non_terminal().await?;
1026 Ok(ProcessLiveReferenceSummary::from_records(records.iter()))
1027 }
1028
1029 async fn claim_process_lease(
1030 &self,
1031 process_id: &str,
1032 owner: &LeaseOwnerIdentity,
1033 lease_ttl_ms: u64,
1034 ) -> Result<ProcessLeaseClaimOutcome, lash_core::PluginError> {
1035 let process_id = process_id.to_string();
1036 let owner = owner.clone();
1037 let now = self.clock.timestamp_ms();
1038 self.conn
1039 .write_flow(move |tx| {
1040 Ok(tx_outcome((|| {
1041 if Self::load_process_conn(tx, &process_id)?.is_none() {
1042 return Err(lash_core::PluginError::Session(format!(
1043 "unknown process `{process_id}`"
1044 )));
1045 }
1046 let current = Self::load_process_lease_conn(tx, &process_id)?;
1047 if let Some(current) = current.as_ref()
1048 && current.expires_at_epoch_ms > now
1049 {
1050 if current.owner.same_incarnation(&owner) {
1051 let expires_at = now.saturating_add(lease_ttl_ms);
1054 tx.execute(
1055 "UPDATE process_leases
1056 SET lease_expires_at_ms = ?2
1057 WHERE process_id = ?1",
1058 params![process_id, expires_at as i64],
1059 )
1060 .map_err(process_sqlite_error)?;
1061 return Ok(ProcessLeaseClaimOutcome::Acquired(ProcessLease {
1062 expires_at_epoch_ms: expires_at,
1063 ..current.clone()
1064 }));
1065 }
1066 return Ok(ProcessLeaseClaimOutcome::Busy {
1067 holder: current.clone(),
1068 });
1069 }
1070 let fencing_token: u64 = tx
1075 .query_row(
1076 "SELECT lease_fencing_token FROM process_leases WHERE process_id = ?1",
1077 params![process_id],
1078 |row| row.get::<_, i64>(0),
1079 )
1080 .optional()
1081 .map_err(process_sqlite_error)?
1082 .unwrap_or(0) as u64
1083 + 1;
1084 Ok(ProcessLeaseClaimOutcome::Acquired(
1085 Self::acquire_process_lease_conn(
1086 tx,
1087 &process_id,
1088 &owner,
1089 fencing_token,
1090 now,
1091 lease_ttl_ms,
1092 )?,
1093 ))
1094 })()))
1095 })
1096 .await
1097 .map_err(process_sqlite_error)?
1098 }
1099
1100 async fn reclaim_process_lease(
1101 &self,
1102 process_id: &str,
1103 owner: &LeaseOwnerIdentity,
1104 observed_holder: &ProcessLease,
1105 lease_ttl_ms: u64,
1106 ) -> Result<ProcessLeaseClaimOutcome, lash_core::PluginError> {
1107 let process_id = process_id.to_string();
1108 let owner = owner.clone();
1109 let observed_holder = observed_holder.clone();
1110 let now = self.clock.timestamp_ms();
1111 self.conn
1112 .write_flow(move |tx| {
1113 Ok(tx_outcome((|| {
1114 if Self::load_process_conn(tx, &process_id)?.is_none() {
1115 return Err(lash_core::PluginError::Session(format!(
1116 "unknown process `{process_id}`"
1117 )));
1118 }
1119 let current = Self::load_process_lease_conn(tx, &process_id)?;
1120 let Some(current) = current else {
1121 let fencing_token: u64 = tx
1124 .query_row(
1125 "SELECT lease_fencing_token FROM process_leases WHERE process_id = ?1",
1126 params![process_id],
1127 |row| row.get::<_, i64>(0),
1128 )
1129 .optional()
1130 .map_err(process_sqlite_error)?
1131 .unwrap_or(0) as u64
1132 + 1;
1133 return Ok(ProcessLeaseClaimOutcome::Acquired(
1134 Self::acquire_process_lease_conn(
1135 tx,
1136 &process_id,
1137 &owner,
1138 fencing_token,
1139 now,
1140 lease_ttl_ms,
1141 )?,
1142 ));
1143 };
1144 if current.expires_at_epoch_ms <= now {
1145 return Ok(ProcessLeaseClaimOutcome::Acquired(
1146 Self::acquire_process_lease_conn(
1147 tx,
1148 &process_id,
1149 &owner,
1150 current.fencing_token.saturating_add(1),
1151 now,
1152 lease_ttl_ms,
1153 )?,
1154 ));
1155 }
1156 if observed_holder.process_id == process_id
1160 && current.owner.same_incarnation(&observed_holder.owner)
1161 && current.lease_token == observed_holder.lease_token
1162 && current.fencing_token == observed_holder.fencing_token
1163 && current.owner.is_definitely_dead_for_claimant(&owner)
1164 {
1165 let fencing_token = current.fencing_token.saturating_add(1);
1166 let lease = ProcessLease {
1167 schema_version: PROCESS_LEASE_SCHEMA_VERSION,
1168 process_id: process_id.clone(),
1169 owner: owner.clone(),
1170 lease_token: format!(
1171 "{:x}",
1172 Sha256::digest(
1173 format!(
1174 "{process_id}:{}:{}:{now}:{fencing_token}",
1175 owner.owner_id, owner.incarnation_id
1176 )
1177 .as_bytes()
1178 )
1179 ),
1180 fencing_token,
1181 claimed_at_epoch_ms: now,
1182 expires_at_epoch_ms: now.saturating_add(lease_ttl_ms),
1183 };
1184 let changed = tx
1185 .execute(
1186 "UPDATE process_leases
1187 SET lease_owner_id = ?1,
1188 lease_owner_incarnation_id = ?2,
1189 lease_owner_liveness_json = ?3,
1190 lease_token = ?4,
1191 lease_fencing_token = ?5,
1192 lease_claimed_at_ms = ?6,
1193 lease_expires_at_ms = ?7
1194 WHERE process_id = ?8
1195 AND lease_owner_id = ?9
1196 AND lease_owner_incarnation_id = ?10
1197 AND lease_token = ?11
1198 AND lease_fencing_token = ?12",
1199 params![
1200 lease.owner.owner_id,
1201 lease.owner.incarnation_id,
1202 encode_process_lease_liveness(&lease.owner.liveness)?,
1203 lease.lease_token,
1204 lease.fencing_token as i64,
1205 lease.claimed_at_epoch_ms as i64,
1206 lease.expires_at_epoch_ms as i64,
1207 process_id,
1208 observed_holder.owner.owner_id,
1209 observed_holder.owner.incarnation_id,
1210 observed_holder.lease_token,
1211 observed_holder.fencing_token as i64,
1212 ],
1213 )
1214 .map_err(process_sqlite_error)?;
1215 if changed == 1 {
1216 return Ok(ProcessLeaseClaimOutcome::Acquired(lease));
1217 }
1218 if let Some(current) = Self::load_process_lease_conn(tx, &process_id)?
1220 && current.expires_at_epoch_ms > now
1221 {
1222 return Ok(ProcessLeaseClaimOutcome::Busy { holder: current });
1223 }
1224 return Err(process_lease_expired(&process_id));
1225 }
1226 Ok(ProcessLeaseClaimOutcome::Busy { holder: current })
1227 })()))
1228 })
1229 .await
1230 .map_err(process_sqlite_error)?
1231 }
1232
1233 async fn renew_process_lease(
1234 &self,
1235 lease: &ProcessLease,
1236 lease_ttl_ms: u64,
1237 ) -> Result<ProcessLease, lash_core::PluginError> {
1238 let lease = lease.clone();
1239 let now = self.clock.timestamp_ms();
1240 self.conn
1241 .write_flow(move |tx| {
1242 Ok(tx_outcome((|| {
1243 let current = Self::load_process_lease_conn(tx, &lease.process_id)?;
1244 if !guard_lease(current.as_ref(), &lease.lease_token, now)
1245 || !current.as_ref().is_some_and(|current| {
1246 current.owner.same_incarnation(&lease.owner)
1247 && current.fencing_token == lease.fencing_token
1248 })
1249 {
1250 return Err(process_lease_expired(&lease.process_id));
1251 }
1252 let renewed = ProcessLease {
1253 expires_at_epoch_ms: now.saturating_add(lease_ttl_ms),
1254 ..lease.clone()
1255 };
1256 tx.execute(
1257 "UPDATE process_leases
1258 SET lease_expires_at_ms = ?2
1259 WHERE process_id = ?1 AND lease_token = ?3",
1260 params![
1261 renewed.process_id.as_str(),
1262 renewed.expires_at_epoch_ms as i64,
1263 renewed.lease_token.as_str(),
1264 ],
1265 )
1266 .map_err(process_sqlite_error)?;
1267 Ok(renewed)
1268 })()))
1269 })
1270 .await
1271 .map_err(process_sqlite_error)?
1272 }
1273
1274 async fn get_process_lease(
1275 &self,
1276 process_id: &str,
1277 ) -> Result<Option<ProcessLease>, lash_core::PluginError> {
1278 let process_id = process_id.to_string();
1279 self.conn
1280 .call(move |conn| Ok(Self::load_process_lease_conn(conn, &process_id)))
1281 .await
1282 .map_err(process_sqlite_error)?
1283 }
1284
1285 async fn complete_process_lease(
1286 &self,
1287 completion: &ProcessLeaseCompletion,
1288 ) -> Result<(), lash_core::PluginError> {
1289 let process_id = completion.process_id.clone();
1290 let lease_token = completion.lease_token.clone();
1291 self.conn
1292 .call(move |conn| {
1293 conn.execute(
1294 "UPDATE process_leases
1295 SET lease_owner_id = NULL,
1296 lease_token = NULL,
1297 lease_claimed_at_ms = 0,
1298 lease_expires_at_ms = 0
1299 WHERE process_id = ?1 AND lease_token = ?2",
1300 params![process_id, lease_token],
1301 )
1302 })
1303 .await
1304 .map_err(process_sqlite_error)?;
1305 Ok(())
1306 }
1307
1308 async fn prune_terminal_processes(
1309 &self,
1310 cutoff_epoch_ms: u64,
1311 filter: Option<ProcessListFilter>,
1312 up_to_change_seq: Option<ProcessChangeCursor>,
1313 ) -> Result<ProcessPruneReport, lash_core::PluginError> {
1314 let cutoff = cutoff_epoch_ms as i64;
1315 let max_change_seq = up_to_change_seq.map(ProcessChangeCursor::store_sequence);
1316 self.conn
1317 .write_flow(move |tx| {
1318 Ok(tx_outcome(
1319 crate::process_registry_change::prune_terminal_processes_conn(
1320 tx,
1321 cutoff,
1322 filter,
1323 max_change_seq,
1324 ),
1325 ))
1326 })
1327 .await
1328 .map_err(process_sqlite_error)?
1329 }
1330}
1331
1332pub(super) fn process_lease_expired(process_id: &str) -> lash_core::PluginError {
1334 lash_core::PluginError::Session(format!(
1335 "process lease for `{process_id}` is missing or expired"
1336 ))
1337}
1338
1339fn process_lease_owner_from_columns(
1340 owner_id: String,
1341 incarnation_id: Option<String>,
1342 liveness_json: Option<String>,
1343) -> LeaseOwnerIdentity {
1344 LeaseOwnerIdentity {
1345 incarnation_id: incarnation_id.unwrap_or_else(|| owner_id.clone()),
1346 owner_id,
1347 liveness: liveness_json
1348 .as_deref()
1349 .and_then(|json| serde_json::from_str(json).ok())
1350 .unwrap_or(LeaseOwnerLiveness::Opaque),
1351 }
1352}
1353
1354fn encode_process_lease_liveness(
1355 liveness: &LeaseOwnerLiveness,
1356) -> Result<String, lash_core::PluginError> {
1357 serde_json::to_string(liveness).map_err(|err| {
1358 lash_core::PluginError::Session(format!("failed to encode process lease liveness: {err}"))
1359 })
1360}
1361
1362fn process_external_ref_conflict(
1363 process_id: &str,
1364 existing: &ProcessExternalRef,
1365 new: &ProcessExternalRef,
1366) -> lash_core::PluginError {
1367 lash_core::PluginError::Session(format!(
1368 "process `{process_id}` external ref conflict: existing {existing:?}, new {new:?}"
1369 ))
1370}