Coverage for src / invariant / store / chain.py: 100.00%
30 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 10:21 +0100
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 10:21 +0100
1"""ChainStore: Composite artifact store chaining MemoryStore and DiskStore."""
3from typing import Any
5from invariant.store.base import ArtifactStore
6from invariant.store.disk import DiskStore
7from invariant.store.memory import MemoryStore
10class ChainStore(ArtifactStore):
11 """Composite artifact store that chains MemoryStore (L1) and DiskStore (L2).
13 Provides a two-tier caching strategy:
14 - L1 (MemoryStore): Fast, session-scoped cache checked first
15 - L2 (DiskStore): Persistent filesystem cache checked if L1 misses
17 On L2 hit, the artifact is promoted to L1 for faster subsequent access.
18 On put, artifacts are written to both L1 and L2.
19 """
21 def __init__(
22 self,
23 l1: MemoryStore | None = None,
24 l2: DiskStore | None = None,
25 ) -> None:
26 """Initialize ChainStore.
28 Args:
29 l1: MemoryStore instance for L1 cache. If None, creates a new one.
30 l2: DiskStore instance for L2 cache. If None, creates a new one.
31 """
32 super().__init__()
33 self.l1 = l1 or MemoryStore(cache="lru")
34 self.l2 = l2 or DiskStore()
36 def exists(self, op_name: str, digest: str) -> bool:
37 """Check if an artifact exists in L1 or L2.
39 Args:
40 op_name: The name of the operation that produced the artifact.
41 digest: The SHA-256 hash (64 character hex string) of the manifest.
43 Returns:
44 True if artifact exists in either store, False otherwise.
45 """
46 # Check L1 first (fast path)
47 if self.l1.exists(op_name, digest):
48 self.stats.hits += 1
49 return True
50 # Check L2 (slower, but persistent)
51 if self.l2.exists(op_name, digest):
52 self.stats.hits += 1
53 return True
54 self.stats.misses += 1
55 return False
57 def get(self, op_name: str, digest: str) -> Any:
58 """Retrieve an artifact from L1 or L2.
60 If found in L2 but not L1, promotes the artifact to L1 for faster
61 subsequent access.
63 Args:
64 op_name: The name of the operation that produced the artifact.
65 digest: The SHA-256 hash (64 character hex string) of the manifest.
67 Returns:
68 The deserialized artifact (native type or ICacheable domain type).
70 Raises:
71 KeyError: If artifact does not exist in either store.
72 """
73 # Try L1 first (fast path)
74 if self.l1.exists(op_name, digest):
75 return self.l1.get(op_name, digest)
77 # Try L2 (slower, but persistent)
78 if self.l2.exists(op_name, digest):
79 # Promote to L1 for faster subsequent access
80 artifact = self.l2.get(op_name, digest)
81 self.l1.put(op_name, digest, artifact)
82 return artifact
84 # Not found in either store
85 raise KeyError(
86 f"Artifact with op_name '{op_name}' and digest '{digest}' not found in L1 or L2"
87 )
89 def put(self, op_name: str, digest: str, artifact: Any) -> None:
90 """Store an artifact in both L1 and L2.
92 Args:
93 op_name: The name of the operation that produced the artifact.
94 digest: The SHA-256 hash (64 character hex string) of the manifest.
95 artifact: The artifact to store (must be cacheable).
96 """
97 # Write to both stores
98 self.l1.put(op_name, digest, artifact)
99 self.l2.put(op_name, digest, artifact)
100 self.stats.puts += 1