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