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 filter_unregistered_process_ids(
1023 &self,
1024 process_ids: &[String],
1025 ) -> Result<Vec<String>, lash_core::PluginError> {
1026 if process_ids.is_empty() {
1027 return Ok(Vec::new());
1028 }
1029 let process_ids_json = serde_json::to_string(process_ids).map_err(process_decode_error)?;
1030 self.conn
1031 .call(move |conn| {
1032 Ok((|| {
1033 let mut stmt = conn
1034 .prepare(
1035 "SELECT candidate.value
1036 FROM json_each(?1) AS candidate
1037 WHERE NOT EXISTS (
1038 SELECT 1 FROM processes p
1039 WHERE p.process_id = candidate.value
1040 )
1041 ORDER BY candidate.key ASC",
1042 )
1043 .map_err(process_sqlite_error)?;
1044 let rows = stmt
1045 .query_map(params![process_ids_json], |row| row.get::<_, String>(0))
1046 .map_err(process_sqlite_error)?;
1047 rows.collect::<Result<Vec<_>, _>>()
1048 .map_err(process_sqlite_error)
1049 })())
1050 })
1051 .await
1052 .map_err(process_sqlite_error)?
1053 }
1054
1055 async fn live_reference_summary(
1056 &self,
1057 ) -> Result<Vec<ProcessLiveReferenceSummary>, lash_core::PluginError> {
1058 let records = self.list_non_terminal().await?;
1059 Ok(ProcessLiveReferenceSummary::from_records(records.iter()))
1060 }
1061
1062 async fn claim_process_lease(
1063 &self,
1064 process_id: &str,
1065 owner: &LeaseOwnerIdentity,
1066 lease_ttl_ms: u64,
1067 ) -> Result<ProcessLeaseClaimOutcome, lash_core::PluginError> {
1068 let process_id = process_id.to_string();
1069 let owner = owner.clone();
1070 let now = self.clock.timestamp_ms();
1071 self.conn
1072 .write_flow(move |tx| {
1073 Ok(tx_outcome((|| {
1074 if Self::load_process_conn(tx, &process_id)?.is_none() {
1075 return Err(lash_core::PluginError::Session(format!(
1076 "unknown process `{process_id}`"
1077 )));
1078 }
1079 let current = Self::load_process_lease_conn(tx, &process_id)?;
1080 if let Some(current) = current.as_ref()
1081 && current.expires_at_epoch_ms > now
1082 {
1083 if current.owner.same_incarnation(&owner) {
1084 let expires_at = now.saturating_add(lease_ttl_ms);
1087 tx.execute(
1088 "UPDATE process_leases
1089 SET lease_expires_at_ms = ?2
1090 WHERE process_id = ?1",
1091 params![process_id, expires_at as i64],
1092 )
1093 .map_err(process_sqlite_error)?;
1094 return Ok(ProcessLeaseClaimOutcome::Acquired(ProcessLease {
1095 expires_at_epoch_ms: expires_at,
1096 ..current.clone()
1097 }));
1098 }
1099 return Ok(ProcessLeaseClaimOutcome::Busy {
1100 holder: current.clone(),
1101 });
1102 }
1103 let fencing_token: u64 = tx
1108 .query_row(
1109 "SELECT lease_fencing_token FROM process_leases WHERE process_id = ?1",
1110 params![process_id],
1111 |row| row.get::<_, i64>(0),
1112 )
1113 .optional()
1114 .map_err(process_sqlite_error)?
1115 .unwrap_or(0) as u64
1116 + 1;
1117 Ok(ProcessLeaseClaimOutcome::Acquired(
1118 Self::acquire_process_lease_conn(
1119 tx,
1120 &process_id,
1121 &owner,
1122 fencing_token,
1123 now,
1124 lease_ttl_ms,
1125 )?,
1126 ))
1127 })()))
1128 })
1129 .await
1130 .map_err(process_sqlite_error)?
1131 }
1132
1133 async fn reclaim_process_lease(
1134 &self,
1135 process_id: &str,
1136 owner: &LeaseOwnerIdentity,
1137 observed_holder: &ProcessLease,
1138 lease_ttl_ms: u64,
1139 ) -> Result<ProcessLeaseClaimOutcome, lash_core::PluginError> {
1140 let process_id = process_id.to_string();
1141 let owner = owner.clone();
1142 let observed_holder = observed_holder.clone();
1143 let now = self.clock.timestamp_ms();
1144 self.conn
1145 .write_flow(move |tx| {
1146 Ok(tx_outcome((|| {
1147 if Self::load_process_conn(tx, &process_id)?.is_none() {
1148 return Err(lash_core::PluginError::Session(format!(
1149 "unknown process `{process_id}`"
1150 )));
1151 }
1152 let current = Self::load_process_lease_conn(tx, &process_id)?;
1153 let Some(current) = current else {
1154 let fencing_token: u64 = tx
1157 .query_row(
1158 "SELECT lease_fencing_token FROM process_leases WHERE process_id = ?1",
1159 params![process_id],
1160 |row| row.get::<_, i64>(0),
1161 )
1162 .optional()
1163 .map_err(process_sqlite_error)?
1164 .unwrap_or(0) as u64
1165 + 1;
1166 return Ok(ProcessLeaseClaimOutcome::Acquired(
1167 Self::acquire_process_lease_conn(
1168 tx,
1169 &process_id,
1170 &owner,
1171 fencing_token,
1172 now,
1173 lease_ttl_ms,
1174 )?,
1175 ));
1176 };
1177 if current.expires_at_epoch_ms <= now {
1178 return Ok(ProcessLeaseClaimOutcome::Acquired(
1179 Self::acquire_process_lease_conn(
1180 tx,
1181 &process_id,
1182 &owner,
1183 current.fencing_token.saturating_add(1),
1184 now,
1185 lease_ttl_ms,
1186 )?,
1187 ));
1188 }
1189 if observed_holder.process_id == process_id
1193 && current.owner.same_incarnation(&observed_holder.owner)
1194 && current.lease_token == observed_holder.lease_token
1195 && current.fencing_token == observed_holder.fencing_token
1196 && current.owner.is_definitely_dead_for_claimant(&owner)
1197 {
1198 let fencing_token = current.fencing_token.saturating_add(1);
1199 let lease = ProcessLease {
1200 schema_version: PROCESS_LEASE_SCHEMA_VERSION,
1201 process_id: process_id.clone(),
1202 owner: owner.clone(),
1203 lease_token: format!(
1204 "{:x}",
1205 Sha256::digest(
1206 format!(
1207 "{process_id}:{}:{}:{now}:{fencing_token}",
1208 owner.owner_id, owner.incarnation_id
1209 )
1210 .as_bytes()
1211 )
1212 ),
1213 fencing_token,
1214 claimed_at_epoch_ms: now,
1215 expires_at_epoch_ms: now.saturating_add(lease_ttl_ms),
1216 };
1217 let changed = tx
1218 .execute(
1219 "UPDATE process_leases
1220 SET lease_owner_id = ?1,
1221 lease_owner_incarnation_id = ?2,
1222 lease_owner_liveness_json = ?3,
1223 lease_token = ?4,
1224 lease_fencing_token = ?5,
1225 lease_claimed_at_ms = ?6,
1226 lease_expires_at_ms = ?7
1227 WHERE process_id = ?8
1228 AND lease_owner_id = ?9
1229 AND lease_owner_incarnation_id = ?10
1230 AND lease_token = ?11
1231 AND lease_fencing_token = ?12",
1232 params![
1233 lease.owner.owner_id,
1234 lease.owner.incarnation_id,
1235 encode_process_lease_liveness(&lease.owner.liveness)?,
1236 lease.lease_token,
1237 lease.fencing_token as i64,
1238 lease.claimed_at_epoch_ms as i64,
1239 lease.expires_at_epoch_ms as i64,
1240 process_id,
1241 observed_holder.owner.owner_id,
1242 observed_holder.owner.incarnation_id,
1243 observed_holder.lease_token,
1244 observed_holder.fencing_token as i64,
1245 ],
1246 )
1247 .map_err(process_sqlite_error)?;
1248 if changed == 1 {
1249 return Ok(ProcessLeaseClaimOutcome::Acquired(lease));
1250 }
1251 if let Some(current) = Self::load_process_lease_conn(tx, &process_id)?
1253 && current.expires_at_epoch_ms > now
1254 {
1255 return Ok(ProcessLeaseClaimOutcome::Busy { holder: current });
1256 }
1257 return Err(process_lease_expired(&process_id));
1258 }
1259 Ok(ProcessLeaseClaimOutcome::Busy { holder: current })
1260 })()))
1261 })
1262 .await
1263 .map_err(process_sqlite_error)?
1264 }
1265
1266 async fn renew_process_lease(
1267 &self,
1268 lease: &ProcessLease,
1269 lease_ttl_ms: u64,
1270 ) -> Result<ProcessLease, lash_core::PluginError> {
1271 let lease = lease.clone();
1272 let now = self.clock.timestamp_ms();
1273 self.conn
1274 .write_flow(move |tx| {
1275 Ok(tx_outcome((|| {
1276 let current = Self::load_process_lease_conn(tx, &lease.process_id)?;
1277 if !guard_lease(current.as_ref(), &lease.lease_token, now)
1278 || !current.as_ref().is_some_and(|current| {
1279 current.owner.same_incarnation(&lease.owner)
1280 && current.fencing_token == lease.fencing_token
1281 })
1282 {
1283 return Err(process_lease_expired(&lease.process_id));
1284 }
1285 let renewed = ProcessLease {
1286 expires_at_epoch_ms: now.saturating_add(lease_ttl_ms),
1287 ..lease.clone()
1288 };
1289 tx.execute(
1290 "UPDATE process_leases
1291 SET lease_expires_at_ms = ?2
1292 WHERE process_id = ?1 AND lease_token = ?3",
1293 params![
1294 renewed.process_id.as_str(),
1295 renewed.expires_at_epoch_ms as i64,
1296 renewed.lease_token.as_str(),
1297 ],
1298 )
1299 .map_err(process_sqlite_error)?;
1300 Ok(renewed)
1301 })()))
1302 })
1303 .await
1304 .map_err(process_sqlite_error)?
1305 }
1306
1307 async fn get_process_lease(
1308 &self,
1309 process_id: &str,
1310 ) -> Result<Option<ProcessLease>, lash_core::PluginError> {
1311 let process_id = process_id.to_string();
1312 self.conn
1313 .call(move |conn| Ok(Self::load_process_lease_conn(conn, &process_id)))
1314 .await
1315 .map_err(process_sqlite_error)?
1316 }
1317
1318 async fn complete_process_lease(
1319 &self,
1320 completion: &ProcessLeaseCompletion,
1321 ) -> Result<(), lash_core::PluginError> {
1322 let process_id = completion.process_id.clone();
1323 let lease_token = completion.lease_token.clone();
1324 self.conn
1325 .call(move |conn| {
1326 conn.execute(
1327 "UPDATE process_leases
1328 SET lease_owner_id = NULL,
1329 lease_token = NULL,
1330 lease_claimed_at_ms = 0,
1331 lease_expires_at_ms = 0
1332 WHERE process_id = ?1 AND lease_token = ?2",
1333 params![process_id, lease_token],
1334 )
1335 })
1336 .await
1337 .map_err(process_sqlite_error)?;
1338 Ok(())
1339 }
1340
1341 async fn prune_terminal_processes(
1342 &self,
1343 cutoff_epoch_ms: u64,
1344 filter: Option<ProcessListFilter>,
1345 up_to_change_seq: Option<ProcessChangeCursor>,
1346 ) -> Result<ProcessPruneReport, lash_core::PluginError> {
1347 let cutoff = cutoff_epoch_ms as i64;
1348 let max_change_seq = up_to_change_seq.map(ProcessChangeCursor::store_sequence);
1349 self.conn
1350 .write_flow(move |tx| {
1351 Ok(tx_outcome(
1352 crate::process_registry_change::prune_terminal_processes_conn(
1353 tx,
1354 cutoff,
1355 filter,
1356 max_change_seq,
1357 ),
1358 ))
1359 })
1360 .await
1361 .map_err(process_sqlite_error)?
1362 }
1363}
1364
1365pub(super) fn process_lease_expired(process_id: &str) -> lash_core::PluginError {
1367 lash_core::PluginError::Session(format!(
1368 "process lease for `{process_id}` is missing or expired"
1369 ))
1370}
1371
1372fn process_lease_owner_from_columns(
1373 owner_id: String,
1374 incarnation_id: Option<String>,
1375 liveness_json: Option<String>,
1376) -> LeaseOwnerIdentity {
1377 LeaseOwnerIdentity {
1378 incarnation_id: incarnation_id.unwrap_or_else(|| owner_id.clone()),
1379 owner_id,
1380 liveness: liveness_json
1381 .as_deref()
1382 .and_then(|json| serde_json::from_str(json).ok())
1383 .unwrap_or(LeaseOwnerLiveness::Opaque),
1384 }
1385}
1386
1387fn encode_process_lease_liveness(
1388 liveness: &LeaseOwnerLiveness,
1389) -> Result<String, lash_core::PluginError> {
1390 serde_json::to_string(liveness).map_err(|err| {
1391 lash_core::PluginError::Session(format!("failed to encode process lease liveness: {err}"))
1392 })
1393}
1394
1395fn process_external_ref_conflict(
1396 process_id: &str,
1397 existing: &ProcessExternalRef,
1398 new: &ProcessExternalRef,
1399) -> lash_core::PluginError {
1400 lash_core::PluginError::Session(format!(
1401 "process `{process_id}` external ref conflict: existing {existing:?}, new {new:?}"
1402 ))
1403}