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