1use super::*;
16
17pub(crate) const MODULE_ARTIFACT_NAMESPACE: &str = "lashlang_module";
25pub(crate) const RAW_ARTIFACT_NAMESPACE: &str = "lashlang_artifact";
26pub(crate) const PROCESS_ENV_NAMESPACE: &str = "process_execution_env";
27pub(crate) const CURRENT_TRIGGER_MANIFEST_NAMESPACE: &str = "lashlang_trigger_manifest";
28
29impl Store {
30 async fn put_artifact_ref_blob(
31 &self,
32 namespace: &'static str,
33 artifact_ref: String,
34 descriptor: BlobArtifactDescriptor,
35 bytes: Vec<u8>,
36 ) -> Result<(), StoreError> {
37 let blob_profile = self.options.blob_profile;
38 self.conn
39 .write(move |tx| {
40 let blob_ref =
41 Self::insert_artifact_blob_conn(tx, descriptor, &bytes, blob_profile)?;
42 tx.execute(
43 "INSERT OR REPLACE INTO artifact_refs (namespace, artifact_ref, blob_ref)
44 VALUES (?1, ?2, ?3)",
45 params![namespace, artifact_ref, blob_ref.as_str()],
46 )?;
47 Ok(())
48 })
49 .await
50 .map_err(sqlite_error)
51 }
52
53 async fn get_artifact_ref_blob(
54 &self,
55 namespace: &'static str,
56 artifact_ref: String,
57 missing_diagnostic: String,
58 ) -> Result<Option<Vec<u8>>, StoreError> {
59 let resolved = self
60 .conn
61 .call(move |conn| {
62 let blob_ref: Option<String> = conn
63 .query_row(
64 "SELECT blob_ref FROM artifact_refs
65 WHERE namespace = ?1 AND artifact_ref = ?2",
66 params![namespace, artifact_ref],
67 |row| row.get::<_, String>(0),
68 )
69 .optional()?;
70 let Some(blob_ref) = blob_ref else {
71 return Ok(None);
72 };
73 Ok(Some(Self::get_blob_conn(conn, &BlobRef(blob_ref))))
74 })
75 .await
76 .map_err(sqlite_error)?;
77 let Some(blob) = resolved else {
78 return Ok(None);
79 };
80 blob.ok_or_else(|| {
81 StoreError::Backend(format!("{missing_diagnostic} points at a missing blob"))
82 })
83 .map(Some)
84 }
85}
86
87#[async_trait::async_trait]
88impl lashlang::LashlangArtifactStore for Store {
89 fn durability_tier(&self) -> lashlang::DurabilityTier {
90 lashlang::DurabilityTier::Durable
91 }
92
93 async fn put_module_artifact(
94 &self,
95 artifact: &lashlang::ModuleArtifact,
96 ) -> Result<(), lashlang::ArtifactStoreError> {
97 let bytes = artifact
98 .to_store_bytes()
99 .map_err(|err| lashlang::ArtifactStoreError::Encode(err.to_string()))?;
100 let artifact_ref = artifact.module_ref.as_str().to_string();
101 self.put_artifact_ref_blob(
102 MODULE_ARTIFACT_NAMESPACE,
103 artifact_ref,
104 BlobArtifactDescriptor::lashlang_module(),
105 bytes,
106 )
107 .await
108 .map_err(|err| lashlang::ArtifactStoreError::Backend(err.to_string()))?;
109 self.artifact_cache
110 .lock()
111 .map_err(|_| {
112 lashlang::ArtifactStoreError::Backend("artifact cache lock poisoned".to_string())
113 })?
114 .insert(artifact.module_ref.clone(), Arc::new(artifact.clone()));
115 Ok(())
116 }
117
118 async fn get_module_artifact(
119 &self,
120 module_ref: &lashlang::ModuleRef,
121 ) -> Result<Option<Arc<lashlang::ModuleArtifact>>, lashlang::ArtifactStoreError> {
122 if let Some(artifact) = self
123 .artifact_cache
124 .lock()
125 .map_err(|_| {
126 lashlang::ArtifactStoreError::Backend("artifact cache lock poisoned".to_string())
127 })?
128 .get(module_ref)
129 .cloned()
130 {
131 return Ok(Some(artifact));
132 }
133
134 let artifact_ref = module_ref.as_str().to_string();
135 let Some(bytes) = self
136 .get_artifact_ref_blob(
137 MODULE_ARTIFACT_NAMESPACE,
138 artifact_ref,
139 format!("lashlang module artifact `{module_ref}`"),
140 )
141 .await
142 .map_err(|err| lashlang::ArtifactStoreError::Backend(err.to_string()))?
143 else {
144 return Ok(None);
145 };
146 let artifact = Arc::new(
147 lashlang::ModuleArtifact::from_store_bytes(&bytes)
148 .map_err(lashlang::ArtifactStoreError::from)?,
149 );
150 self.artifact_cache
151 .lock()
152 .map_err(|_| {
153 lashlang::ArtifactStoreError::Backend("artifact cache lock poisoned".to_string())
154 })?
155 .insert(module_ref.clone(), artifact.clone());
156 Ok(Some(artifact))
157 }
158
159 async fn replace_current_trigger_manifest(
160 &self,
161 owner_namespace: &str,
162 artifact: &lashlang::ModuleArtifact,
163 ) -> Result<lashlang::TriggerManifestReplacement, lashlang::ArtifactStoreError> {
164 let current = lashlang::CurrentTriggerKeyManifest {
165 module_ref: artifact.module_ref.clone(),
166 manifest: artifact.trigger_key_manifest.clone(),
167 };
168 let bytes = serde_json::to_vec(¤t)
169 .map_err(|err| lashlang::ArtifactStoreError::Encode(err.to_string()))?;
170 let owner_namespace = owner_namespace.to_string();
171 let blob_profile = self.options.blob_profile;
172 let previous_bytes = self
173 .conn
174 .write(move |tx| {
175 let previous_blob_ref: Option<String> = tx
176 .query_row(
177 "SELECT blob_ref FROM artifact_refs
178 WHERE namespace = ?1 AND artifact_ref = ?2",
179 params![CURRENT_TRIGGER_MANIFEST_NAMESPACE, owner_namespace.as_str()],
180 |row| row.get(0),
181 )
182 .optional()?;
183 let previous_bytes = previous_blob_ref
184 .and_then(|blob_ref| Self::get_blob_conn(tx, &BlobRef(blob_ref)));
185 let blob_ref = Self::insert_artifact_blob_conn(
186 tx,
187 BlobArtifactDescriptor::lashlang_module(),
188 &bytes,
189 blob_profile,
190 )?;
191 tx.execute(
192 "INSERT OR REPLACE INTO artifact_refs (namespace, artifact_ref, blob_ref)
193 VALUES (?1, ?2, ?3)",
194 params![
195 CURRENT_TRIGGER_MANIFEST_NAMESPACE,
196 owner_namespace.as_str(),
197 blob_ref.as_str()
198 ],
199 )?;
200 Ok(previous_bytes)
201 })
202 .await
203 .map_err(|err| lashlang::ArtifactStoreError::Backend(err.to_string()))?;
204 let previous = previous_bytes
205 .map(|bytes| {
206 serde_json::from_slice::<lashlang::CurrentTriggerKeyManifest>(&bytes)
207 .map_err(|err| lashlang::ArtifactStoreError::Decode(err.to_string()))
208 })
209 .transpose()?;
210 Ok(lashlang::TriggerManifestReplacement {
211 previous_module_ref: previous.as_ref().map(|entry| entry.module_ref.clone()),
212 current_module_ref: artifact.module_ref.clone(),
213 diff: previous
214 .map(|entry| entry.manifest.diff(&artifact.trigger_key_manifest))
215 .unwrap_or_default(),
216 })
217 }
218
219 async fn get_current_trigger_manifest(
220 &self,
221 owner_namespace: &str,
222 ) -> Result<Option<lashlang::CurrentTriggerKeyManifest>, lashlang::ArtifactStoreError> {
223 let bytes = self
224 .get_artifact_ref_blob(
225 CURRENT_TRIGGER_MANIFEST_NAMESPACE,
226 owner_namespace.to_string(),
227 format!("current trigger manifest `{owner_namespace}`"),
228 )
229 .await
230 .map_err(|err| lashlang::ArtifactStoreError::Backend(err.to_string()))?;
231 bytes
232 .map(|bytes| {
233 serde_json::from_slice(&bytes)
234 .map_err(|err| lashlang::ArtifactStoreError::Decode(err.to_string()))
235 })
236 .transpose()
237 }
238
239 async fn put_artifact_bytes(
240 &self,
241 artifact_ref: &str,
242 descriptor: &str,
243 bytes: &[u8],
244 ) -> Result<(), lashlang::ArtifactStoreError> {
245 let artifact_ref = artifact_ref.to_string();
246 let descriptor = match descriptor {
247 "process_execution_env" => BlobArtifactDescriptor::process_execution_env(),
248 _ => BlobArtifactDescriptor::new(PersistedArtifactKind::GenericBlob, Vec::new()),
249 };
250 self.put_artifact_ref_blob(
251 RAW_ARTIFACT_NAMESPACE,
252 artifact_ref,
253 descriptor,
254 bytes.to_vec(),
255 )
256 .await
257 .map_err(|err| lashlang::ArtifactStoreError::Backend(err.to_string()))
258 }
259
260 async fn get_artifact_bytes(
261 &self,
262 artifact_ref: &str,
263 ) -> Result<Option<Vec<u8>>, lashlang::ArtifactStoreError> {
264 let artifact_ref = artifact_ref.to_string();
265 self.get_artifact_ref_blob(
266 RAW_ARTIFACT_NAMESPACE,
267 artifact_ref.clone(),
268 format!("artifact `{artifact_ref}`"),
269 )
270 .await
271 .map_err(|err| lashlang::ArtifactStoreError::Backend(err.to_string()))
272 }
273}
274
275#[async_trait::async_trait]
276impl lash_core::ProcessExecutionEnvStore for Store {
277 fn durability_tier(&self) -> DurabilityTier {
278 DurabilityTier::Durable
279 }
280
281 async fn put_process_execution_env(
282 &self,
283 env_ref: &lash_core::ProcessExecutionEnvRef,
284 bytes: &[u8],
285 ) -> Result<(), lash_core::PluginError> {
286 let artifact_ref = env_ref.as_str().to_string();
287 self.put_artifact_ref_blob(
288 PROCESS_ENV_NAMESPACE,
289 artifact_ref,
290 BlobArtifactDescriptor::process_execution_env(),
291 bytes.to_vec(),
292 )
293 .await
294 .map_err(|err| lash_core::PluginError::Session(err.to_string()))
295 }
296
297 async fn get_process_execution_env(
298 &self,
299 env_ref: &lash_core::ProcessExecutionEnvRef,
300 ) -> Result<Option<Vec<u8>>, lash_core::PluginError> {
301 let artifact_ref = env_ref.as_str().to_string();
302 self.get_artifact_ref_blob(
303 PROCESS_ENV_NAMESPACE,
304 artifact_ref.clone(),
305 format!("process execution env `{artifact_ref}`"),
306 )
307 .await
308 .map_err(|err| lash_core::PluginError::Session(err.to_string()))
309 }
310}
311
312impl AttachmentManifest for Store {
313 fn record_intent(&self, intent: AttachmentIntent) -> Result<(), StoreError> {
314 block_on_store(async {
315 let attachment_id = intent.attachment_id.as_str().to_string();
316 let session_id = intent.session_id.as_str().to_string();
317 let canonical_uri = intent.canonical_uri.as_str().to_string();
318 let intent_at_ms = intent.intent_at_epoch_ms as i64;
319 let owner_kind = intent.owner_kind.map(AttachmentOwnerKind::as_str);
320 let owner_id = intent.owner_id;
321 self.conn
322 .call(move |conn| {
323 conn.execute(
326 "INSERT INTO attachment_manifest
327 (attachment_id, session_id, canonical_uri, intent_at_ms,
328 committed_at_ms, owner_kind, owner_id)
329 VALUES (?1, ?2, ?3, ?4, NULL, ?5, ?6)
330 ON CONFLICT(session_id, attachment_id) DO UPDATE SET
331 canonical_uri = excluded.canonical_uri,
332 intent_at_ms = excluded.intent_at_ms,
333 owner_kind = excluded.owner_kind,
334 owner_id = excluded.owner_id",
335 params![
336 attachment_id,
337 session_id,
338 canonical_uri,
339 intent_at_ms,
340 owner_kind,
341 owner_id
342 ],
343 )
344 })
345 .await
346 .map_err(sqlite_error)?;
347 Ok(())
348 })
349 }
350
351 fn commit_refs(
352 &self,
353 session_id: &str,
354 attachment_ids: &[AttachmentId],
355 ) -> Result<(), StoreError> {
356 if attachment_ids.is_empty() {
357 return Ok(());
358 }
359 block_on_store(async {
360 let session_id = session_id.to_string();
361 let attachment_ids: Vec<String> = attachment_ids
362 .iter()
363 .map(|id| id.as_str().to_string())
364 .collect();
365 let now = self.clock.timestamp_ms() as i64;
366 self.conn
367 .write(move |tx| {
368 let mut stmt = tx.prepare(
369 "UPDATE attachment_manifest
370 SET committed_at_ms = COALESCE(committed_at_ms, ?1)
371 WHERE attachment_id = ?2 AND session_id = ?3",
372 )?;
373 for id in &attachment_ids {
374 stmt.execute(params![now, id, session_id])?;
375 }
376 Ok(())
377 })
378 .await
379 .map_err(sqlite_error)?;
380 Ok(())
381 })
382 }
383
384 fn list_uncommitted(
385 &self,
386 older_than_epoch_ms: u64,
387 ) -> Result<Vec<AttachmentManifestEntry>, StoreError> {
388 block_on_store(async {
389 let older_than = older_than_epoch_ms as i64;
390 self.conn
391 .call(move |conn| {
392 let mut stmt = conn.prepare(
393 "SELECT attachment_id, session_id, canonical_uri, intent_at_ms,
394 committed_at_ms, owner_kind, owner_id
395 FROM attachment_manifest
396 WHERE committed_at_ms IS NULL AND intent_at_ms <= ?1
397 ORDER BY intent_at_ms ASC",
398 )?;
399 let rows = stmt.query_map(params![older_than], |row| {
400 let id: String = row.get(0)?;
401 let session_id: String = row.get(1)?;
402 let canonical_uri: String = row.get(2)?;
403 let intent_at_ms: i64 = row.get(3)?;
404 let committed_at_ms: Option<i64> = row.get(4)?;
405 let owner_kind: Option<String> = row.get(5)?;
406 let owner_id: Option<String> = row.get(6)?;
407 Ok(AttachmentManifestEntry {
408 attachment_id: AttachmentId::new(id),
409 session_id,
410 canonical_uri,
411 intent_at_epoch_ms: intent_at_ms as u64,
412 committed_at_epoch_ms: committed_at_ms.map(|v| v as u64),
413 owner_kind: match owner_kind.as_deref() {
414 Some("turn") => Some(AttachmentOwnerKind::Turn),
415 Some("process") => Some(AttachmentOwnerKind::Process),
416 _ => None,
417 },
418 owner_id,
419 })
420 })?;
421 Ok(rows.filter_map(Result::ok).collect())
422 })
423 .await
424 .map_err(sqlite_error)
425 })
426 }
427
428 fn forget_aged_uncommitted_intents(
429 &self,
430 intent_grace_cutoff_epoch_ms: u64,
431 ) -> Result<(), StoreError> {
432 block_on_store(async {
433 let cutoff = intent_grace_cutoff_epoch_ms as i64;
434 let process_registry_attached = self.process_registry_attached;
435 self.conn
436 .write(move |tx| {
437 let process_dead = if process_registry_attached {
442 "OR (
443 manifest.owner_kind = 'process'
444 AND NOT EXISTS (
445 SELECT 1 FROM process_registry.processes AS process
446 WHERE process.process_id = manifest.owner_id
447 )
448 )"
449 } else {
450 ""
453 };
454 let sql = format!(
455 "DELETE FROM attachment_manifest AS manifest
456 WHERE manifest.committed_at_ms IS NULL
457 AND manifest.intent_at_ms <= ?1
458 AND (
459 manifest.owner_kind IS NULL
460 OR (
461 manifest.owner_kind = 'turn'
462 AND EXISTS (
463 SELECT 1 FROM runtime_turn_commits AS turn_commit
464 WHERE turn_commit.session_id = manifest.session_id
465 AND turn_commit.turn_id <> manifest.owner_id
466 AND turn_commit.committed_at_ms > manifest.intent_at_ms
467 )
468 )
469 {process_dead}
470 )"
471 );
472 tx.execute(&sql, params![cutoff])?;
473 Ok(())
474 })
475 .await
476 .map_err(sqlite_error)?;
477 Ok(())
478 })
479 }
480
481 fn has_live_ref_for_id(
482 &self,
483 attachment_id: &AttachmentId,
484 intent_grace_cutoff_epoch_ms: u64,
485 ) -> Result<bool, StoreError> {
486 block_on_store(async {
487 let attachment_id = attachment_id.as_str().to_string();
488 let cutoff = intent_grace_cutoff_epoch_ms as i64;
489 let process_registry_attached = self.process_registry_attached;
490 self.conn
491 .call(move |conn| {
492 let process_dead = if process_registry_attached {
493 "OR (
494 manifest.owner_kind = 'process'
495 AND NOT EXISTS (
496 SELECT 1 FROM process_registry.processes AS process
497 WHERE process.process_id = manifest.owner_id
498 )
499 )"
500 } else {
501 ""
502 };
503 let sql = format!(
504 "SELECT 1 FROM attachment_manifest AS manifest
505 WHERE manifest.attachment_id = ?1
506 AND NOT (
507 manifest.committed_at_ms IS NULL
508 AND manifest.intent_at_ms <= ?2
509 AND (
510 manifest.owner_kind IS NULL
511 OR (
512 manifest.owner_kind = 'turn'
513 AND EXISTS (
514 SELECT 1 FROM runtime_turn_commits AS turn_commit
515 WHERE turn_commit.session_id = manifest.session_id
516 AND turn_commit.turn_id <> manifest.owner_id
517 AND turn_commit.committed_at_ms > manifest.intent_at_ms
518 )
519 )
520 {process_dead}
521 )
522 )
523 LIMIT 1"
524 );
525 conn.query_row(
526 &sql,
527 params![attachment_id, cutoff],
528 |_| Ok(()),
529 )
530 .optional()
531 .map(|found| found.is_some())
532 })
533 .await
534 .map_err(sqlite_error)
535 })
536 }
537
538 fn forget(&self, session_id: &str, attachment_id: &AttachmentId) -> Result<(), StoreError> {
539 block_on_store(async {
540 let session_id = session_id.to_string();
541 let attachment_id = attachment_id.as_str().to_string();
542 self.conn
543 .call(move |conn| {
544 conn.execute(
545 "DELETE FROM attachment_manifest
546 WHERE session_id = ?1 AND attachment_id = ?2",
547 params![session_id, attachment_id],
548 )
549 })
550 .await
551 .map_err(sqlite_error)?;
552 Ok(())
553 })
554 }
555
556 fn holds_ref(
557 &self,
558 session_id: &str,
559 attachment_id: &AttachmentId,
560 ) -> Result<bool, StoreError> {
561 block_on_store(async {
562 let session_id = session_id.to_string();
563 let attachment_id = attachment_id.as_str().to_string();
564 self.conn
565 .call(move |conn| {
566 conn.query_row(
567 "SELECT 1 FROM attachment_manifest
568 WHERE session_id = ?1 AND attachment_id = ?2",
569 params![session_id, attachment_id],
570 |_| Ok(()),
571 )
572 .optional()
573 .map(|found| found.is_some())
574 })
575 .await
576 .map_err(sqlite_error)
577 })
578 }
579
580 fn list_all_refs(&self) -> Result<Vec<AttachmentId>, StoreError> {
581 block_on_store(async {
582 self.conn
583 .call(move |conn| {
584 let mut stmt =
585 conn.prepare("SELECT DISTINCT attachment_id FROM attachment_manifest")?;
586 let rows = stmt.query_map([], |row| {
587 let id: String = row.get(0)?;
588 Ok(AttachmentId::new(id))
589 })?;
590 Ok(rows.filter_map(Result::ok).collect())
591 })
592 .await
593 .map_err(sqlite_error)
594 })
595 }
596}