1use super::*;
26
27fn process_status_label(record: &ProcessRecord) -> &'static str {
28 record.status.label()
29}
30
31impl SqliteProcessRegistry {
32 pub async fn open(path: &Path) -> tokio_rusqlite::Result<Self> {
33 let conn = SqliteConnection::open(path).await?;
34 ensure_process_schema(&conn).await?;
35 apply_pragmas(&conn, StoreBacking::File).await?;
36 Ok(Self {
37 conn,
38 notify: tokio::sync::Notify::new(),
39 })
40 }
41
42 pub async fn memory() -> tokio_rusqlite::Result<Self> {
43 let conn = SqliteConnection::open_in_memory().await?;
44 ensure_process_schema(&conn).await?;
45 apply_pragmas(&conn, StoreBacking::Memory).await?;
46 Ok(Self {
47 conn,
48 notify: tokio::sync::Notify::new(),
49 })
50 }
51
52 fn load_process_conn(
53 conn: &Connection,
54 process_id: &str,
55 ) -> Result<Option<ProcessRecord>, lash_core::PluginError> {
56 let json: Option<String> = conn
57 .query_row(
58 "SELECT record_json FROM processes WHERE process_id = ?1",
59 params![process_id],
60 |row| row.get(0),
61 )
62 .optional()
63 .map_err(process_sqlite_error)?;
64 json.map(|json| serde_json::from_str(&json).map_err(process_decode_error))
65 .transpose()
66 }
67
68 fn save_process_conn(
69 conn: &Connection,
70 record: &ProcessRecord,
71 ) -> Result<(), lash_core::PluginError> {
72 conn.execute(
73 "UPDATE processes
74 SET updated_at_ms = ?2, status = ?3, record_json = ?4
75 WHERE process_id = ?1",
76 params![
77 record.id.as_str(),
78 record.updated_at_ms as i64,
79 process_status_label(record),
80 process_encode_json(record)?
81 ],
82 )
83 .map_err(process_sqlite_error)?;
84 Ok(())
85 }
86
87 fn load_event_by_key_conn(
88 conn: &Connection,
89 process_id: &str,
90 replay_key: &str,
91 ) -> Result<Option<(String, ProcessEvent)>, lash_core::PluginError> {
92 let row: Option<(String, String)> = conn
93 .query_row(
94 "SELECT payload_hash, event_json
95 FROM process_events
96 WHERE process_id = ?1 AND idempotency_key = ?2",
97 params![process_id, replay_key],
98 |row| Ok((row.get(0)?, row.get(1)?)),
99 )
100 .optional()
101 .map_err(process_sqlite_error)?;
102 row.map(|(hash, json)| {
103 serde_json::from_str(&json)
104 .map(|event| (hash, event))
105 .map_err(process_decode_error)
106 })
107 .transpose()
108 }
109
110 fn load_process_lease_conn(
111 conn: &Connection,
112 process_id: &str,
113 ) -> Result<Option<ProcessLease>, lash_core::PluginError> {
114 conn.query_row(
115 "SELECT lease_owner_id, lease_token, lease_fencing_token,
116 lease_claimed_at_ms, lease_expires_at_ms,
117 lease_owner_incarnation_id, lease_owner_liveness_json
118 FROM process_leases
119 WHERE process_id = ?1",
120 params![process_id],
121 |row| {
122 let owner_id: Option<String> = row.get(0)?;
123 let lease_token: Option<String> = row.get(1)?;
124 let incarnation_id: Option<String> = row.get(5)?;
125 let liveness_json: Option<String> = row.get(6)?;
126 let (Some(owner_id), Some(lease_token)) = (owner_id, lease_token) else {
127 return Ok(None);
128 };
129 Ok(Some(ProcessLease {
130 schema_version: PROCESS_LEASE_SCHEMA_VERSION,
131 process_id: process_id.to_string(),
132 owner: process_lease_owner_from_columns(
133 owner_id,
134 incarnation_id,
135 liveness_json,
136 ),
137 lease_token,
138 fencing_token: row.get::<_, i64>(2)? as u64,
139 claimed_at_epoch_ms: row.get::<_, i64>(3)? as u64,
140 expires_at_epoch_ms: row.get::<_, i64>(4)? as u64,
141 }))
142 },
143 )
144 .optional()
145 .map(|lease| lease.flatten())
146 .map_err(process_sqlite_error)
147 }
148
149 fn acquire_process_lease_conn(
152 conn: &Connection,
153 process_id: &str,
154 owner: &LeaseOwnerIdentity,
155 fencing_token: u64,
156 now: u64,
157 lease_ttl_ms: u64,
158 ) -> Result<ProcessLease, lash_core::PluginError> {
159 let lease = ProcessLease {
160 schema_version: PROCESS_LEASE_SCHEMA_VERSION,
161 process_id: process_id.to_string(),
162 owner: owner.clone(),
163 lease_token: format!(
164 "{:x}",
165 Sha256::digest(
166 format!(
167 "{process_id}:{}:{}:{now}:{fencing_token}",
168 owner.owner_id, owner.incarnation_id
169 )
170 .as_bytes()
171 )
172 ),
173 fencing_token,
174 claimed_at_epoch_ms: now,
175 expires_at_epoch_ms: now.saturating_add(lease_ttl_ms),
176 };
177 conn.execute(
178 "INSERT INTO process_leases (
179 process_id, lease_owner_id, lease_owner_incarnation_id,
180 lease_owner_liveness_json, lease_token, lease_fencing_token,
181 lease_claimed_at_ms, lease_expires_at_ms
182 )
183 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
184 ON CONFLICT(process_id) DO UPDATE SET
185 lease_owner_id = excluded.lease_owner_id,
186 lease_owner_incarnation_id = excluded.lease_owner_incarnation_id,
187 lease_owner_liveness_json = excluded.lease_owner_liveness_json,
188 lease_token = excluded.lease_token,
189 lease_fencing_token = excluded.lease_fencing_token,
190 lease_claimed_at_ms = excluded.lease_claimed_at_ms,
191 lease_expires_at_ms = excluded.lease_expires_at_ms",
192 params![
193 lease.process_id.as_str(),
194 lease.owner.owner_id.as_str(),
195 lease.owner.incarnation_id.as_str(),
196 encode_process_lease_liveness(&lease.owner.liveness)?,
197 lease.lease_token.as_str(),
198 lease.fencing_token as i64,
199 lease.claimed_at_epoch_ms as i64,
200 lease.expires_at_epoch_ms as i64,
201 ],
202 )
203 .map_err(process_sqlite_error)?;
204 Ok(lease)
205 }
206
207 fn list_grants_for_scope_conn(
208 conn: &Connection,
209 session_scope: &SessionScope,
210 live_only: bool,
211 ) -> Result<Vec<ProcessHandleGrantEntry>, lash_core::PluginError> {
212 let session_scope_id = session_scope.id();
213 let status_clause = if live_only {
214 "AND p.status = 'running'"
215 } else {
216 ""
217 };
218 let mut stmt = conn
219 .prepare(&format!(
220 "SELECT g.process_id, g.descriptor_json, p.record_json
221 FROM process_handle_grants g
222 JOIN processes p ON p.process_id = g.process_id
223 WHERE g.scope_id = ?1 {status_clause}
224 ORDER BY g.process_id ASC"
225 ))
226 .map_err(process_sqlite_error)?;
227 let rows = stmt
228 .query_map(params![session_scope_id.as_str()], |row| {
229 Ok((
230 row.get::<_, String>(0)?,
231 row.get::<_, String>(1)?,
232 row.get::<_, String>(2)?,
233 ))
234 })
235 .map_err(process_sqlite_error)?;
236 let mut entries = Vec::new();
237 for row in rows {
238 let (process_id, descriptor_json, record_json) = row.map_err(process_sqlite_error)?;
239 let descriptor: ProcessHandleDescriptor =
240 serde_json::from_str(&descriptor_json).map_err(process_decode_error)?;
241 let record: ProcessRecord =
242 serde_json::from_str(&record_json).map_err(process_decode_error)?;
243 entries.push((
244 ProcessHandleGrant {
245 session_id: session_scope.session_id.clone(),
246 process_id,
247 descriptor,
248 },
249 record,
250 ));
251 }
252 Ok(entries)
253 }
254}
255
256fn tx_outcome<T>(
261 result: Result<T, lash_core::PluginError>,
262) -> TxOutcome<Result<T, lash_core::PluginError>> {
263 match result {
264 Ok(value) => TxOutcome::Commit(Ok(value)),
265 Err(err) => TxOutcome::Rollback(Err(err)),
266 }
267}
268
269#[async_trait::async_trait]
270impl ProcessRegistry for SqliteProcessRegistry {
271 fn durability_tier(&self) -> DurabilityTier {
272 DurabilityTier::Durable
273 }
274
275 async fn register_process(
276 &self,
277 registration: ProcessRegistration,
278 ) -> Result<ProcessRecord, lash_core::PluginError> {
279 let (registration, registration_hash) = prepare_process_registration(registration)?;
280 let record = self
281 .conn
282 .write_flow(move |tx| {
283 Ok(tx_outcome((|| {
284 if let Some(existing) = Self::load_process_conn(tx, ®istration.id)? {
285 if existing.registration_hash == registration_hash {
286 return Ok(existing);
287 }
288 return Err(lash_core::PluginError::Session(format!(
289 "process `{}` registration hash conflict: existing {}, new {}",
290 registration.id, existing.registration_hash, registration_hash
291 )));
292 }
293 let now = current_epoch_ms();
294 let record = ProcessRecord::from_prepared_registration(
295 registration,
296 registration_hash,
297 now,
298 );
299 let originator_scope_id = record.originator_scope_id();
300 tx.execute(
301 "INSERT INTO processes (
302 process_id, registration_hash, owner_scope_id,
303 created_at_ms, updated_at_ms, status, record_json
304 )
305 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
306 params![
307 record.id.as_str(),
308 record.registration_hash.as_str(),
309 originator_scope_id.as_str(),
310 record.created_at_ms as i64,
311 record.updated_at_ms as i64,
312 process_status_label(&record),
313 process_encode_json(&record)?,
314 ],
315 )
316 .map_err(process_sqlite_error)?;
317 Ok(record)
318 })()))
319 })
320 .await
321 .map_err(process_sqlite_error)??;
322 self.notify.notify_waiters();
323 Ok(record)
324 }
325
326 async fn set_external_ref(
327 &self,
328 process_id: &str,
329 external_ref: ProcessExternalRef,
330 ) -> Result<ProcessRecord, lash_core::PluginError> {
331 let process_id = process_id.to_string();
332 let (record, changed) = self
333 .conn
334 .write_flow(move |tx| {
335 Ok(tx_outcome((|| {
336 let mut record =
337 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
338 lash_core::PluginError::Session(format!(
339 "unknown process `{process_id}`"
340 ))
341 })?;
342 if let Some(existing) = &record.external_ref {
343 if existing == &external_ref {
344 return Ok((record, false));
345 }
346 return Err(process_external_ref_conflict(
347 &process_id,
348 existing,
349 &external_ref,
350 ));
351 }
352 record.external_ref = Some(external_ref);
353 record.updated_at_ms = current_epoch_ms();
354 Self::save_process_conn(tx, &record)?;
355 Ok((record, true))
356 })()))
357 })
358 .await
359 .map_err(process_sqlite_error)??;
360 if changed {
361 self.notify.notify_waiters();
362 }
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 if appended {
813 self.notify.notify_waiters();
814 }
815 Ok(result)
816 }
817
818 async fn events_after(
819 &self,
820 process_id: &str,
821 after_sequence: u64,
822 ) -> Result<Vec<ProcessEvent>, lash_core::PluginError> {
823 let process_id = process_id.to_string();
824 self.conn
825 .call(move |conn| {
826 Ok((|| {
827 if Self::load_process_conn(conn, &process_id)?.is_none() {
828 return Err(lash_core::PluginError::Session(format!(
829 "unknown process `{process_id}`"
830 )));
831 }
832 let mut stmt = conn
833 .prepare(
834 "SELECT event_json FROM process_events
835 WHERE process_id = ?1 AND sequence > ?2
836 ORDER BY sequence ASC",
837 )
838 .map_err(process_sqlite_error)?;
839 let rows = stmt
840 .query_map(params![process_id, after_sequence as i64], |row| {
841 row.get::<_, String>(0)
842 })
843 .map_err(process_sqlite_error)?;
844 let mut events = Vec::new();
845 for row in rows {
846 events.push(
847 serde_json::from_str(&row.map_err(process_sqlite_error)?)
848 .map_err(process_decode_error)?,
849 );
850 }
851 Ok(events)
852 })())
853 })
854 .await
855 .map_err(process_sqlite_error)?
856 }
857
858 async fn count_events_through(
859 &self,
860 process_id: &str,
861 event_type: &str,
862 up_to_sequence: u64,
863 ) -> Result<u64, lash_core::PluginError> {
864 let process_id = process_id.to_string();
865 let event_type = event_type.to_string();
866 self.conn
867 .call(move |conn| {
868 Ok((|| {
869 if Self::load_process_conn(conn, &process_id)?.is_none() {
870 return Err(lash_core::PluginError::Session(format!(
871 "unknown process `{process_id}`"
872 )));
873 }
874 conn.query_row(
875 "SELECT COUNT(*) FROM process_events
876 WHERE process_id = ?1 AND event_type = ?2 AND sequence <= ?3",
877 params![process_id, event_type, up_to_sequence as i64],
878 |row| row.get::<_, i64>(0),
879 )
880 .map(|count| count as u64)
881 .map_err(process_sqlite_error)
882 })())
883 })
884 .await
885 .map_err(process_sqlite_error)?
886 }
887
888 async fn recent_events(
889 &self,
890 process_id: &str,
891 limit: usize,
892 ) -> Result<Vec<ProcessEvent>, lash_core::PluginError> {
893 let process_id = process_id.to_string();
894 self.conn
895 .call(move |conn| {
896 Ok((|| {
897 if Self::load_process_conn(conn, &process_id)?.is_none() {
898 return Err(lash_core::PluginError::Session(format!(
899 "unknown process `{process_id}`"
900 )));
901 }
902 let mut stmt = conn
903 .prepare(
904 "SELECT event_json FROM process_events
905 WHERE process_id = ?1
906 ORDER BY sequence DESC
907 LIMIT ?2",
908 )
909 .map_err(process_sqlite_error)?;
910 let rows = stmt
911 .query_map(params![process_id, limit as i64], |row| {
912 row.get::<_, String>(0)
913 })
914 .map_err(process_sqlite_error)?;
915 let mut events: Vec<ProcessEvent> = Vec::new();
916 for row in rows {
917 events.push(
918 serde_json::from_str(&row.map_err(process_sqlite_error)?)
919 .map_err(process_decode_error)?,
920 );
921 }
922 events.reverse();
923 Ok(events)
924 })())
925 })
926 .await
927 .map_err(process_sqlite_error)?
928 }
929
930 async fn wake_events_after(
931 &self,
932 process_id: &str,
933 after_sequence: u64,
934 ) -> Result<Vec<ProcessEvent>, lash_core::PluginError> {
935 let acked: std::collections::HashSet<u64> = {
936 let process_id = process_id.to_string();
937 self.conn
938 .call(move |conn| {
939 Ok(
940 (|| -> Result<std::collections::HashSet<u64>, lash_core::PluginError> {
941 let mut stmt = conn
942 .prepare(
943 "SELECT sequence FROM process_wake_acks WHERE process_id = ?1",
944 )
945 .map_err(process_sqlite_error)?;
946 let rows = stmt
947 .query_map(params![process_id], |row| row.get::<_, i64>(0))
948 .map_err(process_sqlite_error)?;
949 let mut set = std::collections::HashSet::new();
950 for row in rows {
951 set.insert(row.map_err(process_sqlite_error)? as u64);
952 }
953 Ok(set)
954 })(),
955 )
956 })
957 .await
958 .map_err(process_sqlite_error)??
959 };
960 Ok(self
961 .events_after(process_id, after_sequence)
962 .await?
963 .into_iter()
964 .filter(|event| event.semantics.wake.is_some() && !acked.contains(&event.sequence))
965 .collect())
966 }
967
968 async fn wait_event_after(
969 &self,
970 process_id: &str,
971 event_type: &str,
972 after_sequence: u64,
973 ) -> Result<ProcessEvent, lash_core::PluginError> {
974 loop {
975 if let Some(event) = self
976 .events_after(process_id, after_sequence)
977 .await?
978 .into_iter()
979 .find(|event| event.event_type == event_type)
980 {
981 return Ok(event);
982 }
983 tokio::select! {
984 _ = self.notify.notified() => {}
985 _ = tokio::time::sleep(Duration::from_millis(50)) => {}
986 }
987 }
988 }
989
990 async fn await_process(
991 &self,
992 process_id: &str,
993 ) -> Result<ProcessAwaitOutput, lash_core::PluginError> {
994 loop {
995 let record = self.get_process(process_id).await.ok_or_else(|| {
996 lash_core::PluginError::Session(format!("unknown process `{process_id}`"))
997 })?;
998 if let Some(await_output) = record.status.await_output() {
999 return Ok(await_output.clone());
1000 }
1001 tokio::select! {
1002 _ = self.notify.notified() => {}
1003 _ = tokio::time::sleep(Duration::from_millis(50)) => {}
1004 }
1005 }
1006 }
1007
1008 async fn complete_process(
1009 &self,
1010 process_id: &str,
1011 await_output: ProcessAwaitOutput,
1012 ) -> Result<ProcessRecord, lash_core::PluginError> {
1013 let event_type = match await_output.terminal_state() {
1014 lash_core::ProcessTerminalState::Completed => "process.completed",
1015 lash_core::ProcessTerminalState::Failed => "process.failed",
1016 lash_core::ProcessTerminalState::Cancelled => "process.cancelled",
1017 };
1018 self.append_event(
1019 process_id,
1020 ProcessEventAppendRequest::new(
1021 event_type,
1022 serde_json::json!({ "await_output": await_output }),
1023 )
1024 .with_replay_key(format!("process:{process_id}:terminal:{event_type}")),
1025 )
1026 .await?;
1027 self.get_process(process_id).await.ok_or_else(|| {
1028 lash_core::PluginError::Session(format!(
1029 "unknown process `{process_id}` after terminal event"
1030 ))
1031 })
1032 }
1033
1034 async fn set_process_wait(
1035 &self,
1036 process_id: &str,
1037 wait: lash_core::WaitState,
1038 ) -> Result<ProcessRecord, lash_core::PluginError> {
1039 let process_id = process_id.to_string();
1040 self.conn
1041 .write_flow(move |tx| {
1042 Ok(tx_outcome((|| {
1043 let mut record =
1044 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
1045 lash_core::PluginError::Session(format!(
1046 "unknown process `{process_id}`"
1047 ))
1048 })?;
1049 if record.is_terminal() {
1050 return Err(lash_core::PluginError::Session(format!(
1051 "terminal process `{process_id}` cannot enter a wait state"
1052 )));
1053 }
1054 record.wait = Some(wait);
1055 record.updated_at_ms = current_epoch_ms();
1056 Self::save_process_conn(tx, &record)?;
1057 Ok(record)
1058 })()))
1059 })
1060 .await
1061 .map_err(process_sqlite_error)?
1062 }
1063
1064 async fn clear_process_wait(
1065 &self,
1066 process_id: &str,
1067 ) -> Result<ProcessRecord, lash_core::PluginError> {
1068 let process_id = process_id.to_string();
1069 self.conn
1070 .write_flow(move |tx| {
1071 Ok(tx_outcome((|| {
1072 let mut record =
1073 Self::load_process_conn(tx, &process_id)?.ok_or_else(|| {
1074 lash_core::PluginError::Session(format!(
1075 "unknown process `{process_id}`"
1076 ))
1077 })?;
1078 record.wait = None;
1079 record.updated_at_ms = current_epoch_ms();
1080 Self::save_process_conn(tx, &record)?;
1081 Ok(record)
1082 })()))
1083 })
1084 .await
1085 .map_err(process_sqlite_error)?
1086 }
1087
1088 async fn get_process(&self, process_id: &str) -> Option<ProcessRecord> {
1089 let process_id = process_id.to_string();
1090 self.conn
1091 .call(move |conn| Ok(Self::load_process_conn(conn, &process_id).ok().flatten()))
1092 .await
1093 .ok()
1094 .flatten()
1095 }
1096
1097 async fn list_processes(
1098 &self,
1099 filter: &lash_core::ProcessListFilter,
1100 ) -> Result<Vec<ProcessRecord>, lash_core::PluginError> {
1101 let filter = filter.clone();
1102 self.conn
1103 .call(move |conn| {
1104 Ok((|| {
1105 let mut stmt = conn
1106 .prepare(
1107 "SELECT record_json FROM processes
1108 ORDER BY process_id ASC",
1109 )
1110 .map_err(process_sqlite_error)?;
1111 let rows = stmt
1112 .query_map([], |row| row.get::<_, String>(0))
1113 .map_err(process_sqlite_error)?;
1114 let mut records = Vec::new();
1115 for row in rows {
1116 let record: ProcessRecord =
1117 serde_json::from_str(&row.map_err(process_sqlite_error)?)
1118 .map_err(process_decode_error)?;
1119 if filter.matches_record(&record) {
1120 records.push(record);
1121 }
1122 }
1123 Ok(records)
1124 })())
1125 })
1126 .await
1127 .map_err(process_sqlite_error)?
1128 }
1129
1130 async fn ack_wake(
1131 &self,
1132 process_id: &str,
1133 sequence: u64,
1134 ) -> Result<(), lash_core::PluginError> {
1135 let process_id = process_id.to_string();
1136 self.conn
1137 .call(move |conn| {
1138 Ok((|| {
1139 if Self::load_process_conn(conn, &process_id)?.is_none() {
1140 return Err(lash_core::PluginError::Session(format!(
1141 "unknown process `{process_id}`"
1142 )));
1143 }
1144 conn.execute(
1145 "INSERT OR IGNORE INTO process_wake_acks (process_id, sequence) VALUES (?1, ?2)",
1146 params![process_id, sequence as i64],
1147 )
1148 .map_err(process_sqlite_error)?;
1149 Ok(())
1150 })())
1151 })
1152 .await
1153 .map_err(process_sqlite_error)?
1154 }
1155
1156 async fn list_non_terminal(&self) -> Result<Vec<ProcessRecord>, lash_core::PluginError> {
1157 self.conn
1158 .call(move |conn| {
1159 Ok((|| {
1160 let mut stmt = conn
1161 .prepare(
1162 "SELECT record_json FROM processes
1163 WHERE status = 'running'
1164 ORDER BY process_id ASC",
1165 )
1166 .map_err(process_sqlite_error)?;
1167 let rows = stmt
1168 .query_map([], |row| row.get::<_, String>(0))
1169 .map_err(process_sqlite_error)?;
1170 let mut records = Vec::new();
1171 for row in rows {
1172 let record: ProcessRecord =
1173 serde_json::from_str(&row.map_err(process_sqlite_error)?)
1174 .map_err(process_decode_error)?;
1175 records.push(record);
1176 }
1177 Ok(records)
1178 })())
1179 })
1180 .await
1181 .map_err(process_sqlite_error)?
1182 }
1183
1184 async fn claim_process_lease(
1185 &self,
1186 process_id: &str,
1187 owner: &LeaseOwnerIdentity,
1188 lease_ttl_ms: u64,
1189 ) -> Result<ProcessLeaseClaimOutcome, lash_core::PluginError> {
1190 let process_id = process_id.to_string();
1191 let owner = owner.clone();
1192 self.conn
1193 .write_flow(move |tx| {
1194 Ok(tx_outcome((|| {
1195 if Self::load_process_conn(tx, &process_id)?.is_none() {
1196 return Err(lash_core::PluginError::Session(format!(
1197 "unknown process `{process_id}`"
1198 )));
1199 }
1200 let now = current_epoch_ms();
1201 let current = Self::load_process_lease_conn(tx, &process_id)?;
1202 if let Some(current) = current.as_ref()
1203 && current.expires_at_epoch_ms > now
1204 {
1205 if current.owner.same_incarnation(&owner) {
1206 let expires_at = now.saturating_add(lease_ttl_ms);
1209 tx.execute(
1210 "UPDATE process_leases
1211 SET lease_expires_at_ms = ?2
1212 WHERE process_id = ?1",
1213 params![process_id, expires_at as i64],
1214 )
1215 .map_err(process_sqlite_error)?;
1216 return Ok(ProcessLeaseClaimOutcome::Acquired(ProcessLease {
1217 expires_at_epoch_ms: expires_at,
1218 ..current.clone()
1219 }));
1220 }
1221 return Ok(ProcessLeaseClaimOutcome::Busy {
1222 holder: current.clone(),
1223 });
1224 }
1225 let fencing_token: u64 = tx
1230 .query_row(
1231 "SELECT lease_fencing_token FROM process_leases WHERE process_id = ?1",
1232 params![process_id],
1233 |row| row.get::<_, i64>(0),
1234 )
1235 .optional()
1236 .map_err(process_sqlite_error)?
1237 .unwrap_or(0) as u64
1238 + 1;
1239 Ok(ProcessLeaseClaimOutcome::Acquired(
1240 Self::acquire_process_lease_conn(
1241 tx,
1242 &process_id,
1243 &owner,
1244 fencing_token,
1245 now,
1246 lease_ttl_ms,
1247 )?,
1248 ))
1249 })()))
1250 })
1251 .await
1252 .map_err(process_sqlite_error)?
1253 }
1254
1255 async fn reclaim_process_lease(
1256 &self,
1257 process_id: &str,
1258 owner: &LeaseOwnerIdentity,
1259 observed_holder: &ProcessLease,
1260 lease_ttl_ms: u64,
1261 ) -> Result<ProcessLeaseClaimOutcome, lash_core::PluginError> {
1262 let process_id = process_id.to_string();
1263 let owner = owner.clone();
1264 let observed_holder = observed_holder.clone();
1265 self.conn
1266 .write_flow(move |tx| {
1267 Ok(tx_outcome((|| {
1268 if Self::load_process_conn(tx, &process_id)?.is_none() {
1269 return Err(lash_core::PluginError::Session(format!(
1270 "unknown process `{process_id}`"
1271 )));
1272 }
1273 let now = current_epoch_ms();
1274 let current = Self::load_process_lease_conn(tx, &process_id)?;
1275 let Some(current) = current else {
1276 let fencing_token: u64 = tx
1279 .query_row(
1280 "SELECT lease_fencing_token FROM process_leases WHERE process_id = ?1",
1281 params![process_id],
1282 |row| row.get::<_, i64>(0),
1283 )
1284 .optional()
1285 .map_err(process_sqlite_error)?
1286 .unwrap_or(0) as u64
1287 + 1;
1288 return Ok(ProcessLeaseClaimOutcome::Acquired(
1289 Self::acquire_process_lease_conn(
1290 tx,
1291 &process_id,
1292 &owner,
1293 fencing_token,
1294 now,
1295 lease_ttl_ms,
1296 )?,
1297 ));
1298 };
1299 if current.expires_at_epoch_ms <= now {
1300 return Ok(ProcessLeaseClaimOutcome::Acquired(
1301 Self::acquire_process_lease_conn(
1302 tx,
1303 &process_id,
1304 &owner,
1305 current.fencing_token.saturating_add(1),
1306 now,
1307 lease_ttl_ms,
1308 )?,
1309 ));
1310 }
1311 if observed_holder.process_id == process_id
1315 && current.owner.same_incarnation(&observed_holder.owner)
1316 && current.lease_token == observed_holder.lease_token
1317 && current.fencing_token == observed_holder.fencing_token
1318 && current.owner.is_definitely_dead_for_claimant(&owner)
1319 {
1320 let fencing_token = current.fencing_token.saturating_add(1);
1321 let lease = ProcessLease {
1322 schema_version: PROCESS_LEASE_SCHEMA_VERSION,
1323 process_id: process_id.clone(),
1324 owner: owner.clone(),
1325 lease_token: format!(
1326 "{:x}",
1327 Sha256::digest(
1328 format!(
1329 "{process_id}:{}:{}:{now}:{fencing_token}",
1330 owner.owner_id, owner.incarnation_id
1331 )
1332 .as_bytes()
1333 )
1334 ),
1335 fencing_token,
1336 claimed_at_epoch_ms: now,
1337 expires_at_epoch_ms: now.saturating_add(lease_ttl_ms),
1338 };
1339 let changed = tx
1340 .execute(
1341 "UPDATE process_leases
1342 SET lease_owner_id = ?1,
1343 lease_owner_incarnation_id = ?2,
1344 lease_owner_liveness_json = ?3,
1345 lease_token = ?4,
1346 lease_fencing_token = ?5,
1347 lease_claimed_at_ms = ?6,
1348 lease_expires_at_ms = ?7
1349 WHERE process_id = ?8
1350 AND lease_owner_id = ?9
1351 AND lease_owner_incarnation_id = ?10
1352 AND lease_token = ?11
1353 AND lease_fencing_token = ?12",
1354 params![
1355 lease.owner.owner_id,
1356 lease.owner.incarnation_id,
1357 encode_process_lease_liveness(&lease.owner.liveness)?,
1358 lease.lease_token,
1359 lease.fencing_token as i64,
1360 lease.claimed_at_epoch_ms as i64,
1361 lease.expires_at_epoch_ms as i64,
1362 process_id,
1363 observed_holder.owner.owner_id,
1364 observed_holder.owner.incarnation_id,
1365 observed_holder.lease_token,
1366 observed_holder.fencing_token as i64,
1367 ],
1368 )
1369 .map_err(process_sqlite_error)?;
1370 if changed == 1 {
1371 return Ok(ProcessLeaseClaimOutcome::Acquired(lease));
1372 }
1373 if let Some(current) = Self::load_process_lease_conn(tx, &process_id)?
1375 && current.expires_at_epoch_ms > now
1376 {
1377 return Ok(ProcessLeaseClaimOutcome::Busy { holder: current });
1378 }
1379 return Err(process_lease_expired(&process_id));
1380 }
1381 Ok(ProcessLeaseClaimOutcome::Busy { holder: current })
1382 })()))
1383 })
1384 .await
1385 .map_err(process_sqlite_error)?
1386 }
1387
1388 async fn renew_process_lease(
1389 &self,
1390 lease: &ProcessLease,
1391 lease_ttl_ms: u64,
1392 ) -> Result<ProcessLease, lash_core::PluginError> {
1393 let lease = lease.clone();
1394 self.conn
1395 .write_flow(move |tx| {
1396 Ok(tx_outcome((|| {
1397 let now = current_epoch_ms();
1398 let current = Self::load_process_lease_conn(tx, &lease.process_id)?;
1399 if !guard_lease(current.as_ref(), &lease.lease_token, now)
1400 || !current.as_ref().is_some_and(|current| {
1401 current.owner.same_incarnation(&lease.owner)
1402 && current.fencing_token == lease.fencing_token
1403 })
1404 {
1405 return Err(process_lease_expired(&lease.process_id));
1406 }
1407 let renewed = ProcessLease {
1408 expires_at_epoch_ms: now.saturating_add(lease_ttl_ms),
1409 ..lease.clone()
1410 };
1411 tx.execute(
1412 "UPDATE process_leases
1413 SET lease_expires_at_ms = ?2
1414 WHERE process_id = ?1 AND lease_token = ?3",
1415 params![
1416 renewed.process_id.as_str(),
1417 renewed.expires_at_epoch_ms as i64,
1418 renewed.lease_token.as_str(),
1419 ],
1420 )
1421 .map_err(process_sqlite_error)?;
1422 Ok(renewed)
1423 })()))
1424 })
1425 .await
1426 .map_err(process_sqlite_error)?
1427 }
1428
1429 async fn complete_process_lease(
1430 &self,
1431 completion: &ProcessLeaseCompletion,
1432 ) -> Result<(), lash_core::PluginError> {
1433 let process_id = completion.process_id.clone();
1434 let lease_token = completion.lease_token.clone();
1435 self.conn
1436 .call(move |conn| {
1437 conn.execute(
1438 "UPDATE process_leases
1439 SET lease_owner_id = NULL,
1440 lease_token = NULL,
1441 lease_claimed_at_ms = 0,
1442 lease_expires_at_ms = 0
1443 WHERE process_id = ?1 AND lease_token = ?2",
1444 params![process_id, lease_token],
1445 )
1446 })
1447 .await
1448 .map_err(process_sqlite_error)?;
1449 Ok(())
1450 }
1451}
1452
1453fn process_lease_expired(process_id: &str) -> lash_core::PluginError {
1455 lash_core::PluginError::Session(format!(
1456 "process lease for `{process_id}` is missing or expired"
1457 ))
1458}
1459
1460fn process_lease_owner_from_columns(
1461 owner_id: String,
1462 incarnation_id: Option<String>,
1463 liveness_json: Option<String>,
1464) -> LeaseOwnerIdentity {
1465 LeaseOwnerIdentity {
1466 incarnation_id: incarnation_id.unwrap_or_else(|| owner_id.clone()),
1467 owner_id,
1468 liveness: liveness_json
1469 .as_deref()
1470 .and_then(|json| serde_json::from_str(json).ok())
1471 .unwrap_or(LeaseOwnerLiveness::Opaque),
1472 }
1473}
1474
1475fn encode_process_lease_liveness(
1476 liveness: &LeaseOwnerLiveness,
1477) -> Result<String, lash_core::PluginError> {
1478 serde_json::to_string(liveness).map_err(|err| {
1479 lash_core::PluginError::Session(format!("failed to encode process lease liveness: {err}"))
1480 })
1481}
1482
1483fn process_external_ref_conflict(
1484 process_id: &str,
1485 existing: &ProcessExternalRef,
1486 new: &ProcessExternalRef,
1487) -> lash_core::PluginError {
1488 lash_core::PluginError::Session(format!(
1489 "process `{process_id}` external ref conflict: existing {existing:?}, new {new:?}"
1490 ))
1491}