query-cascade 0.3.7__tar.gz → 0.3.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {query_cascade-0.3.7 → query_cascade-0.3.8}/PKG-INFO +12 -1
- {query_cascade-0.3.7 → query_cascade-0.3.8}/README.md +11 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/pyproject.toml +1 -1
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_evaluator.py +30 -2
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_serde.py +3 -1
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_state.py +1 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_store.py +9 -1
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/engine.py +2 -2
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/query_cascade.egg-info/PKG-INFO +12 -1
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/query_cascade.egg-info/SOURCES.txt +2 -1
- query_cascade-0.3.8/tests/test_ttl.py +63 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/setup.cfg +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/__init__.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_canonical.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_disk_cache.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_errors.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_graph_export.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_persistence.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_runtime.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_scheduler.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/cascade/_synthetic_graph.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/query_cascade.egg-info/dependency_links.txt +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/query_cascade.egg-info/requires.txt +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/src/query_cascade.egg-info/top_level.txt +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_async.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_async_coverage.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_code_versioning.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_dependency_parallelism.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_disk_cache.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_engine.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_engine_compute_many.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_engine_concurrency.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_engine_persistence_and_tracing.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_engine_private.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_engine_private2.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_engine_sweep.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_error_caching.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_error_caching_persistence.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_evaluator_and_synthetic_coverage.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_examples.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_fixed_point.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_internal_invariants.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_memoize_false.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_performance.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_repro_touch_memo_keyerror.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_scale_behavior.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_serde.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_stateful_engine_invariants.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_store_coverage.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_subgraph_stats_export.py +0 -0
- {query_cascade-0.3.7 → query_cascade-0.3.8}/tests/test_sweep_unaccessed.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: query-cascade
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Minimal demand-driven query framework for incremental computation.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -139,6 +139,16 @@ A few things to know:
|
|
|
139
139
|
|
|
140
140
|
## Advanced Features
|
|
141
141
|
|
|
142
|
+
### Time-To-Live Cache Invalidation
|
|
143
|
+
Cascade supports expiring cache entries automatically based on wall-clock time. By passing the `ttl` flag to `@engine.query`, you can guarantee that a node will recompute its value if it is accessed after the specified number of seconds has elapsed since its last evaluation.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
@engine.query(ttl=5.0)
|
|
147
|
+
def fetch_external_data():
|
|
148
|
+
return request.get("https://api.example.com/data")
|
|
149
|
+
```
|
|
150
|
+
When a TTL expires, Cascade evaluates the function again. If the newly returned value is identical to the previous one, downstream queries are **not** re-evaluated—early bail-out works exactly as it does for regular dependency changes.
|
|
151
|
+
|
|
142
152
|
### Asynchronous Execution
|
|
143
153
|
Cascade natively supports asynchronous queries via `async def`. This is extremely useful for I/O-bound workflows (such as making database or network calls). When an asynchronous query evaluates, it runs cooperatively on the active `asyncio` event loop.
|
|
144
154
|
|
|
@@ -264,6 +274,7 @@ pip install query-cascade
|
|
|
264
274
|
|--------|----------------|
|
|
265
275
|
| `compiler_pipeline.py` | `source → parse → symbols → typecheck`, warnings accumulator, cache-hit narration |
|
|
266
276
|
| `async_execution.py` | Asynchronous query evaluation and asyncio event loop integration for IO-bound work |
|
|
277
|
+
| `ttl_invalidation.py` | Expiring stale query caches automatically based on wall-clock time |
|
|
267
278
|
| `error_caching.py` | Basic exception caching to prevent repeated re-evaluation on failure |
|
|
268
279
|
| `error_caching_persistence.py` | Disk cache hydration of exceptions across process runs |
|
|
269
280
|
| `code_versioning.py` | Automatic cache invalidation when a function's bytecode logic changes |
|
|
@@ -120,6 +120,16 @@ A few things to know:
|
|
|
120
120
|
|
|
121
121
|
## Advanced Features
|
|
122
122
|
|
|
123
|
+
### Time-To-Live Cache Invalidation
|
|
124
|
+
Cascade supports expiring cache entries automatically based on wall-clock time. By passing the `ttl` flag to `@engine.query`, you can guarantee that a node will recompute its value if it is accessed after the specified number of seconds has elapsed since its last evaluation.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
@engine.query(ttl=5.0)
|
|
128
|
+
def fetch_external_data():
|
|
129
|
+
return request.get("https://api.example.com/data")
|
|
130
|
+
```
|
|
131
|
+
When a TTL expires, Cascade evaluates the function again. If the newly returned value is identical to the previous one, downstream queries are **not** re-evaluated—early bail-out works exactly as it does for regular dependency changes.
|
|
132
|
+
|
|
123
133
|
### Asynchronous Execution
|
|
124
134
|
Cascade natively supports asynchronous queries via `async def`. This is extremely useful for I/O-bound workflows (such as making database or network calls). When an asynchronous query evaluates, it runs cooperatively on the active `asyncio` event loop.
|
|
125
135
|
|
|
@@ -245,6 +255,7 @@ pip install query-cascade
|
|
|
245
255
|
|--------|----------------|
|
|
246
256
|
| `compiler_pipeline.py` | `source → parse → symbols → typecheck`, warnings accumulator, cache-hit narration |
|
|
247
257
|
| `async_execution.py` | Asynchronous query evaluation and asyncio event loop integration for IO-bound work |
|
|
258
|
+
| `ttl_invalidation.py` | Expiring stale query caches automatically based on wall-clock time |
|
|
248
259
|
| `error_caching.py` | Basic exception caching to prevent repeated re-evaluation on failure |
|
|
249
260
|
| `error_caching_persistence.py` | Disk cache hydration of exceptions across process runs |
|
|
250
261
|
| `code_versioning.py` | Automatic cache invalidation when a function's bytecode logic changes |
|
|
@@ -308,7 +308,13 @@ class Evaluator:
|
|
|
308
308
|
is_memoized = self._store.lookup_query_memoize(key[1])
|
|
309
309
|
if existing is not None:
|
|
310
310
|
self._store.touch_memo_locked(key)
|
|
311
|
-
|
|
311
|
+
|
|
312
|
+
ttl = self._store.lookup_query_ttl(key[1])
|
|
313
|
+
is_expired = False
|
|
314
|
+
if ttl is not None and self._store.monotonic_seconds() - existing.computed_at_time > ttl:
|
|
315
|
+
is_expired = True
|
|
316
|
+
|
|
317
|
+
if not is_expired and existing.verified_at == runtime.snapshot.revision:
|
|
312
318
|
self._store.trace_event("cache_hit", key)
|
|
313
319
|
if is_memoized or not need_value:
|
|
314
320
|
return existing, True
|
|
@@ -370,7 +376,13 @@ class Evaluator:
|
|
|
370
376
|
is_memoized = self._store.lookup_query_memoize(key[1])
|
|
371
377
|
if existing is not None:
|
|
372
378
|
self._store.touch_memo_locked(key)
|
|
373
|
-
|
|
379
|
+
|
|
380
|
+
ttl = self._store.lookup_query_ttl(key[1])
|
|
381
|
+
is_expired = False
|
|
382
|
+
if ttl is not None and self._store.monotonic_seconds() - existing.computed_at_time > ttl:
|
|
383
|
+
is_expired = True
|
|
384
|
+
|
|
385
|
+
if not is_expired and existing.verified_at == runtime.snapshot.revision:
|
|
374
386
|
self._store.trace_event("cache_hit", key)
|
|
375
387
|
if is_memoized or not need_value:
|
|
376
388
|
return existing, True
|
|
@@ -423,6 +435,11 @@ class Evaluator:
|
|
|
423
435
|
self._store.in_flight.pop((key, runtime.snapshot.revision), None)
|
|
424
436
|
|
|
425
437
|
def try_mark_green(self, key: QueryKey, entry: MemoEntry, snapshot: Snapshot) -> bool:
|
|
438
|
+
ttl = self._store.lookup_query_ttl(key[1])
|
|
439
|
+
if ttl is not None and self._store.monotonic_seconds() - entry.computed_at_time > ttl:
|
|
440
|
+
self._store.trace_event("cache_red_ttl", key)
|
|
441
|
+
return False
|
|
442
|
+
|
|
426
443
|
for dep in entry.deps:
|
|
427
444
|
dep_changed_at = self.dependency_changed_at(dep.key, snapshot)
|
|
428
445
|
if dep_changed_at != dep.observed_changed_at:
|
|
@@ -431,6 +448,11 @@ class Evaluator:
|
|
|
431
448
|
return True
|
|
432
449
|
|
|
433
450
|
async def try_mark_green_async(self, key: QueryKey, entry: MemoEntry, snapshot: Snapshot) -> bool: # pragma: no cover
|
|
451
|
+
ttl = self._store.lookup_query_ttl(key[1])
|
|
452
|
+
if ttl is not None and self._store.monotonic_seconds() - entry.computed_at_time > ttl:
|
|
453
|
+
self._store.trace_event("cache_red_ttl", key)
|
|
454
|
+
return False
|
|
455
|
+
|
|
434
456
|
for dep in entry.deps:
|
|
435
457
|
dep_changed_at = await self.dependency_changed_at_async(dep.key, snapshot)
|
|
436
458
|
if dep_changed_at != dep.observed_changed_at:
|
|
@@ -636,6 +658,7 @@ class Evaluator:
|
|
|
636
658
|
deps=frame.deps,
|
|
637
659
|
effects=frozen_effects,
|
|
638
660
|
last_access=self._store.next_access_id,
|
|
661
|
+
computed_at_time=start,
|
|
639
662
|
cycle_nodes=tuple(frame.cycle_nodes),
|
|
640
663
|
error=error,
|
|
641
664
|
)
|
|
@@ -650,6 +673,7 @@ class Evaluator:
|
|
|
650
673
|
deps=frame.deps,
|
|
651
674
|
effects=frozen_effects,
|
|
652
675
|
last_access=self._store.next_access_id,
|
|
676
|
+
computed_at_time=start,
|
|
653
677
|
cycle_nodes=tuple(frame.cycle_nodes),
|
|
654
678
|
error=error,
|
|
655
679
|
)
|
|
@@ -768,6 +792,7 @@ class Evaluator:
|
|
|
768
792
|
deps=frame.deps,
|
|
769
793
|
effects=frozen_effects,
|
|
770
794
|
last_access=self._store.next_access_id,
|
|
795
|
+
computed_at_time=start,
|
|
771
796
|
cycle_nodes=tuple(frame.cycle_nodes),
|
|
772
797
|
error=error,
|
|
773
798
|
)
|
|
@@ -782,6 +807,7 @@ class Evaluator:
|
|
|
782
807
|
deps=frame.deps,
|
|
783
808
|
effects=frozen_effects,
|
|
784
809
|
last_access=self._store.next_access_id,
|
|
810
|
+
computed_at_time=start,
|
|
785
811
|
cycle_nodes=tuple(frame.cycle_nodes),
|
|
786
812
|
error=error,
|
|
787
813
|
)
|
|
@@ -866,6 +892,7 @@ class Evaluator:
|
|
|
866
892
|
deps=observed,
|
|
867
893
|
effects=effects,
|
|
868
894
|
last_access=self._store.next_access_id,
|
|
895
|
+
computed_at_time=record.get("computed_at_time", 0.0),
|
|
869
896
|
error=error,
|
|
870
897
|
)
|
|
871
898
|
self._store.drop_memo_locked(key)
|
|
@@ -1197,6 +1224,7 @@ class Evaluator:
|
|
|
1197
1224
|
"deps": deps_records,
|
|
1198
1225
|
"effects": effects_node,
|
|
1199
1226
|
"is_error": memo.error is not None,
|
|
1227
|
+
"computed_at_time": memo.computed_at_time,
|
|
1200
1228
|
}
|
|
1201
1229
|
disk.store_entry(_disk_cache.entry_key(kind, fid, args_blob), record, memo.value_hash, value_blob)
|
|
1202
1230
|
except PersistentCacheError:
|
|
@@ -16,7 +16,7 @@ from typing import Any
|
|
|
16
16
|
|
|
17
17
|
from ._state import Dependency, InputVersion, MemoEntry, TraceEvent
|
|
18
18
|
|
|
19
|
-
PERSISTENCE_FORMAT =
|
|
19
|
+
PERSISTENCE_FORMAT = 2
|
|
20
20
|
|
|
21
21
|
# Used for canonical key ordering; kept separate so tests can monkeypatch `json.dumps`
|
|
22
22
|
# on this module without breaking recursive sorting inside `_to_jsonable`.
|
|
@@ -168,6 +168,7 @@ def _to_jsonable(obj: Any) -> Any:
|
|
|
168
168
|
"deps": _to_jsonable(list(obj.deps)),
|
|
169
169
|
"effects": _to_jsonable(obj.effects),
|
|
170
170
|
"last_access": obj.last_access,
|
|
171
|
+
"computed_at_time": _to_jsonable(obj.computed_at_time),
|
|
171
172
|
}
|
|
172
173
|
}
|
|
173
174
|
|
|
@@ -290,6 +291,7 @@ def _from_jsonable(obj: Any) -> Any:
|
|
|
290
291
|
deps=tuple(deps_raw),
|
|
291
292
|
effects=_from_jsonable(d["effects"]),
|
|
292
293
|
last_access=d["last_access"],
|
|
294
|
+
computed_at_time=_from_jsonable(d.get("computed_at_time", 0.0)),
|
|
293
295
|
)
|
|
294
296
|
if sole_key == "__dataclass__":
|
|
295
297
|
d = sole_val
|
|
@@ -49,6 +49,7 @@ class GraphStore:
|
|
|
49
49
|
self.query_memoize: dict[str, bool] = {}
|
|
50
50
|
self.query_fixed_points: dict[str, Any] = {}
|
|
51
51
|
self.query_cache_exceptions: dict[str, bool | tuple[type[BaseException], ...]] = {}
|
|
52
|
+
self.query_ttl: dict[str, float | None] = {}
|
|
52
53
|
self.input_fns: dict[str, Callable[..., Any]] = {}
|
|
53
54
|
self.memos: dict[QueryKey, MemoEntry] = {}
|
|
54
55
|
self.dependents: dict[QueryKey, set[QueryKey]] = defaultdict(set)
|
|
@@ -106,7 +107,7 @@ class GraphStore:
|
|
|
106
107
|
sk = self.key_to_str(key)
|
|
107
108
|
self._stats_by_key[sk] = self._stats_by_key.get(sk, 0.0) + seconds
|
|
108
109
|
|
|
109
|
-
def register_query(self, query_id: str, fn: Callable[..., Any], *, memoize: bool = True, fixed_point: Any = None, has_fixed_point: bool = False, cache_exceptions: bool | tuple[type[BaseException], ...] = False) -> None:
|
|
110
|
+
def register_query(self, query_id: str, fn: Callable[..., Any], *, memoize: bool = True, fixed_point: Any = None, has_fixed_point: bool = False, cache_exceptions: bool | tuple[type[BaseException], ...] = False, ttl: float | None = None) -> None:
|
|
110
111
|
fn_hash = hash_bytecode(fn)
|
|
111
112
|
with self.lock:
|
|
112
113
|
if query_id in self.query_hashes and self.query_hashes[query_id] != fn_hash:
|
|
@@ -117,9 +118,14 @@ class GraphStore:
|
|
|
117
118
|
self.query_hashes[query_id] = fn_hash
|
|
118
119
|
self.query_memoize[query_id] = memoize
|
|
119
120
|
self.query_cache_exceptions[query_id] = cache_exceptions
|
|
121
|
+
self.query_ttl[query_id] = ttl
|
|
120
122
|
if has_fixed_point:
|
|
121
123
|
self.query_fixed_points[query_id] = fixed_point
|
|
122
124
|
|
|
125
|
+
def lookup_query_ttl(self, query_id: str) -> float | None:
|
|
126
|
+
with self.lock:
|
|
127
|
+
return self.query_ttl.get(query_id)
|
|
128
|
+
|
|
123
129
|
def lookup_query_memoize(self, query_id: str) -> bool:
|
|
124
130
|
with self.lock:
|
|
125
131
|
return self.query_memoize.get(query_id, True)
|
|
@@ -495,6 +501,7 @@ class GraphStore:
|
|
|
495
501
|
deps: dict[QueryKey, int],
|
|
496
502
|
effects: dict[str, tuple[Any, ...]],
|
|
497
503
|
last_access: int,
|
|
504
|
+
computed_at_time: float,
|
|
498
505
|
cycle_nodes: tuple[QueryKey, ...] = (),
|
|
499
506
|
error: BaseException | None = None,
|
|
500
507
|
) -> MemoEntry:
|
|
@@ -506,6 +513,7 @@ class GraphStore:
|
|
|
506
513
|
deps=tuple(Dependency(dep_key, observed) for dep_key, observed in deps.items()),
|
|
507
514
|
effects=effects,
|
|
508
515
|
last_access=last_access,
|
|
516
|
+
computed_at_time=computed_at_time,
|
|
509
517
|
cycle_nodes=cycle_nodes,
|
|
510
518
|
error=error,
|
|
511
519
|
)
|
|
@@ -284,11 +284,11 @@ class Engine:
|
|
|
284
284
|
self._store.register_input(handle.id, fn)
|
|
285
285
|
return handle
|
|
286
286
|
|
|
287
|
-
def query(self, fn: Callable[..., Any] | None = None, *, memoize: bool = True, fixed_point: Any = _UNSET, cache_exceptions: bool | tuple[type[BaseException], ...] = True) -> Any:
|
|
287
|
+
def query(self, fn: Callable[..., Any] | None = None, *, memoize: bool = True, fixed_point: Any = _UNSET, cache_exceptions: bool | tuple[type[BaseException], ...] = True, ttl: float | None = None) -> Any:
|
|
288
288
|
def decorator(f: Callable[..., Any]) -> _QueryHandle:
|
|
289
289
|
handle = _QueryHandle(self, f)
|
|
290
290
|
has_fixed_point = fixed_point is not _UNSET
|
|
291
|
-
self._store.register_query(handle.id, f, memoize=memoize, fixed_point=fixed_point, has_fixed_point=has_fixed_point, cache_exceptions=cache_exceptions)
|
|
291
|
+
self._store.register_query(handle.id, f, memoize=memoize, fixed_point=fixed_point, has_fixed_point=has_fixed_point, cache_exceptions=cache_exceptions, ttl=ttl)
|
|
292
292
|
return handle
|
|
293
293
|
if fn is not None:
|
|
294
294
|
return decorator(fn)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: query-cascade
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.8
|
|
4
4
|
Summary: Minimal demand-driven query framework for incremental computation.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -139,6 +139,16 @@ A few things to know:
|
|
|
139
139
|
|
|
140
140
|
## Advanced Features
|
|
141
141
|
|
|
142
|
+
### Time-To-Live Cache Invalidation
|
|
143
|
+
Cascade supports expiring cache entries automatically based on wall-clock time. By passing the `ttl` flag to `@engine.query`, you can guarantee that a node will recompute its value if it is accessed after the specified number of seconds has elapsed since its last evaluation.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
@engine.query(ttl=5.0)
|
|
147
|
+
def fetch_external_data():
|
|
148
|
+
return request.get("https://api.example.com/data")
|
|
149
|
+
```
|
|
150
|
+
When a TTL expires, Cascade evaluates the function again. If the newly returned value is identical to the previous one, downstream queries are **not** re-evaluated—early bail-out works exactly as it does for regular dependency changes.
|
|
151
|
+
|
|
142
152
|
### Asynchronous Execution
|
|
143
153
|
Cascade natively supports asynchronous queries via `async def`. This is extremely useful for I/O-bound workflows (such as making database or network calls). When an asynchronous query evaluates, it runs cooperatively on the active `asyncio` event loop.
|
|
144
154
|
|
|
@@ -264,6 +274,7 @@ pip install query-cascade
|
|
|
264
274
|
|--------|----------------|
|
|
265
275
|
| `compiler_pipeline.py` | `source → parse → symbols → typecheck`, warnings accumulator, cache-hit narration |
|
|
266
276
|
| `async_execution.py` | Asynchronous query evaluation and asyncio event loop integration for IO-bound work |
|
|
277
|
+
| `ttl_invalidation.py` | Expiring stale query caches automatically based on wall-clock time |
|
|
267
278
|
| `error_caching.py` | Basic exception caching to prevent repeated re-evaluation on failure |
|
|
268
279
|
| `error_caching_persistence.py` | Disk cache hydration of exceptions across process runs |
|
|
269
280
|
| `code_versioning.py` | Automatic cache invalidation when a function's bytecode logic changes |
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from cascade import Engine
|
|
2
|
+
|
|
3
|
+
def test_ttl_invalidation_basic():
|
|
4
|
+
engine = Engine()
|
|
5
|
+
|
|
6
|
+
clock = 0.0
|
|
7
|
+
engine._store.monotonic_seconds = lambda: clock
|
|
8
|
+
|
|
9
|
+
compute_count = 0
|
|
10
|
+
|
|
11
|
+
@engine.query(ttl=2.0)
|
|
12
|
+
def my_query():
|
|
13
|
+
nonlocal compute_count
|
|
14
|
+
compute_count += 1
|
|
15
|
+
return "value"
|
|
16
|
+
|
|
17
|
+
assert my_query() == "value"
|
|
18
|
+
assert compute_count == 1
|
|
19
|
+
|
|
20
|
+
# Still within TTL
|
|
21
|
+
clock = 1.0
|
|
22
|
+
assert my_query() == "value"
|
|
23
|
+
assert compute_count == 1
|
|
24
|
+
|
|
25
|
+
# Exceed TTL
|
|
26
|
+
clock = 3.0
|
|
27
|
+
assert my_query() == "value"
|
|
28
|
+
assert compute_count == 2
|
|
29
|
+
|
|
30
|
+
# Still within new TTL
|
|
31
|
+
clock = 4.0
|
|
32
|
+
assert my_query() == "value"
|
|
33
|
+
assert compute_count == 2
|
|
34
|
+
|
|
35
|
+
def test_ttl_invalidation_async():
|
|
36
|
+
import asyncio
|
|
37
|
+
engine = Engine()
|
|
38
|
+
|
|
39
|
+
clock = 0.0
|
|
40
|
+
engine._store.monotonic_seconds = lambda: clock
|
|
41
|
+
|
|
42
|
+
compute_count = 0
|
|
43
|
+
|
|
44
|
+
@engine.query(ttl=1.5)
|
|
45
|
+
async def my_async_query():
|
|
46
|
+
nonlocal compute_count
|
|
47
|
+
compute_count += 1
|
|
48
|
+
return "async_value"
|
|
49
|
+
|
|
50
|
+
async def run():
|
|
51
|
+
nonlocal clock
|
|
52
|
+
assert await my_async_query() == "async_value"
|
|
53
|
+
assert compute_count == 1
|
|
54
|
+
|
|
55
|
+
clock = 1.0
|
|
56
|
+
assert await my_async_query() == "async_value"
|
|
57
|
+
assert compute_count == 1
|
|
58
|
+
|
|
59
|
+
clock = 2.0
|
|
60
|
+
assert await my_async_query() == "async_value"
|
|
61
|
+
assert compute_count == 2
|
|
62
|
+
|
|
63
|
+
asyncio.run(run())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|