query-cascade 0.3.2__tar.gz → 0.3.3__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.2 → query_cascade-0.3.3}/PKG-INFO +1 -1
- {query_cascade-0.3.2 → query_cascade-0.3.3}/pyproject.toml +1 -1
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_evaluator.py +63 -10
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_runtime.py +5 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_state.py +1 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_store.py +13 -1
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/engine.py +9 -4
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/query_cascade.egg-info/PKG-INFO +1 -1
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/query_cascade.egg-info/SOURCES.txt +1 -0
- query_cascade-0.3.3/tests/test_fixed_point.py +38 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/README.md +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/setup.cfg +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/__init__.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_canonical.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_disk_cache.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_errors.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_graph_export.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_persistence.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_scheduler.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_serde.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/cascade/_synthetic_graph.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/query_cascade.egg-info/dependency_links.txt +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/query_cascade.egg-info/requires.txt +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/src/query_cascade.egg-info/top_level.txt +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_dependency_parallelism.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_disk_cache.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_engine.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_engine_compute_many.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_engine_concurrency.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_engine_persistence_and_tracing.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_engine_private.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_engine_private2.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_evaluator_and_synthetic_coverage.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_examples.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_internal_invariants.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_performance.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_repro_touch_memo_keyerror.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_scale_behavior.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_serde.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_stateful_engine_invariants.py +0 -0
- {query_cascade-0.3.2 → query_cascade-0.3.3}/tests/test_subgraph_stats_export.py +0 -0
|
@@ -7,7 +7,7 @@ from typing import Any, Callable, Mapping, Sequence
|
|
|
7
7
|
from . import _canonical, _disk_cache
|
|
8
8
|
from ._disk_cache import DiskCache
|
|
9
9
|
from ._errors import CycleError, PersistentCacheError, QueryCancelled
|
|
10
|
-
from ._runtime import RuntimeFrame, RuntimeState
|
|
10
|
+
from ._runtime import UNSET, RuntimeFrame, RuntimeState
|
|
11
11
|
from ._state import InputVersion, MemoEntry, QueryKey, Snapshot
|
|
12
12
|
from ._store import GraphStore
|
|
13
13
|
|
|
@@ -146,10 +146,24 @@ class Evaluator:
|
|
|
146
146
|
)
|
|
147
147
|
|
|
148
148
|
key: QueryKey = ("query", query_id, args)
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
149
|
+
for i, frame in enumerate(runtime.stack):
|
|
150
|
+
if frame.key == key:
|
|
151
|
+
if frame.cycle_guess is UNSET:
|
|
152
|
+
has_fp, fp_val = self._store.lookup_query_fixed_point(query_id)
|
|
153
|
+
if not has_fp:
|
|
154
|
+
cycle_start = [f.key for f in runtime.stack[i:]] + [key]
|
|
155
|
+
cycle = " -> ".join(self._store.key_to_str(k) for k in cycle_start)
|
|
156
|
+
raise CycleError(f"cycle detected: {cycle}")
|
|
157
|
+
frame.cycle_guess = fp_val
|
|
158
|
+
|
|
159
|
+
frame.is_cycle_root = True
|
|
160
|
+
for f in runtime.stack[i:]:
|
|
161
|
+
frame.cycle_nodes.add(f.key)
|
|
162
|
+
|
|
163
|
+
# Record the back-edge dependency so the calling node is properly invalidated later
|
|
164
|
+
if len(runtime.stack) > 0:
|
|
165
|
+
runtime.stack[-1].deps[key] = runtime.snapshot.revision
|
|
166
|
+
return frame.cycle_guess
|
|
153
167
|
self.check_cancelled(runtime.cancel_epoch)
|
|
154
168
|
entry, replay_needed = self.compute_or_get_memo(key, fn, runtime)
|
|
155
169
|
self.record_dependency(key, entry.changed_at)
|
|
@@ -235,6 +249,11 @@ class Evaluator:
|
|
|
235
249
|
return memo.changed_at
|
|
236
250
|
|
|
237
251
|
runtime = self._runtime_var.get()
|
|
252
|
+
if runtime is not None:
|
|
253
|
+
for frame in runtime.stack:
|
|
254
|
+
if frame.key == key:
|
|
255
|
+
return snapshot.revision
|
|
256
|
+
|
|
238
257
|
if runtime is None:
|
|
239
258
|
self._run_in_runtime(
|
|
240
259
|
RuntimeState(
|
|
@@ -280,17 +299,50 @@ class Evaluator:
|
|
|
280
299
|
runtime.stack.append(frame)
|
|
281
300
|
start = self._store.monotonic_seconds()
|
|
282
301
|
self._store.trace_event("recompute_start", key)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
302
|
+
|
|
303
|
+
with self._store.lock:
|
|
304
|
+
old_memo = self._store.memos.get(key)
|
|
305
|
+
if old_memo is not None and old_memo.cycle_nodes:
|
|
306
|
+
for k in old_memo.cycle_nodes:
|
|
307
|
+
if k != key:
|
|
308
|
+
self._store.drop_memo_locked(k)
|
|
309
|
+
|
|
310
|
+
while True:
|
|
311
|
+
try:
|
|
312
|
+
self.check_cancelled(runtime.cancel_epoch)
|
|
313
|
+
result = fn(*key[2])
|
|
314
|
+
self.check_cancelled(runtime.cancel_epoch)
|
|
315
|
+
except BaseException:
|
|
316
|
+
runtime.stack.pop()
|
|
317
|
+
raise
|
|
318
|
+
|
|
319
|
+
if frame.is_cycle_root:
|
|
320
|
+
if self._store.stable_hash(result) != self._store.stable_hash(frame.cycle_guess):
|
|
321
|
+
frame.cycle_guess = result
|
|
322
|
+
frame.is_cycle_root = False
|
|
323
|
+
with self._store.lock:
|
|
324
|
+
self._store.revision += 1
|
|
325
|
+
runtime.snapshot = Snapshot(revision=self._store.revision)
|
|
326
|
+
for k in frame.cycle_nodes:
|
|
327
|
+
if k != key:
|
|
328
|
+
self._store.drop_memo_locked(k)
|
|
329
|
+
frame.deps.clear()
|
|
330
|
+
frame.effects.clear()
|
|
331
|
+
continue
|
|
332
|
+
|
|
288
333
|
runtime.stack.pop()
|
|
334
|
+
break
|
|
335
|
+
|
|
289
336
|
elapsed = self._store.monotonic_seconds() - start
|
|
290
337
|
duration_ms = elapsed * 1000.0
|
|
291
338
|
|
|
292
339
|
frozen_effects = {name: tuple(items) for name, items in frame.effects.items()}
|
|
293
340
|
value_hash = self._store.stable_hash(result)
|
|
341
|
+
|
|
342
|
+
if frame.cycle_nodes:
|
|
343
|
+
for k in frame.cycle_nodes:
|
|
344
|
+
frame.deps.pop(k, None)
|
|
345
|
+
|
|
294
346
|
with self._store.lock:
|
|
295
347
|
previous = self._store.memos.get(key)
|
|
296
348
|
if previous is not None and previous.value_hash == value_hash:
|
|
@@ -307,6 +359,7 @@ class Evaluator:
|
|
|
307
359
|
deps=frame.deps,
|
|
308
360
|
effects=frozen_effects,
|
|
309
361
|
last_access=self._store.next_access_id,
|
|
362
|
+
cycle_nodes=tuple(frame.cycle_nodes),
|
|
310
363
|
)
|
|
311
364
|
self._store.drop_memo_locked(key)
|
|
312
365
|
self._store.memos[key] = memo
|
|
@@ -7,11 +7,16 @@ from typing import Any
|
|
|
7
7
|
from ._state import QueryKey, Snapshot
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
UNSET = object()
|
|
11
|
+
|
|
10
12
|
@dataclass
|
|
11
13
|
class RuntimeFrame:
|
|
12
14
|
key: QueryKey
|
|
13
15
|
deps: dict[QueryKey, int] = field(default_factory=dict)
|
|
14
16
|
effects: dict[str, list[Any]] = field(default_factory=lambda: defaultdict(list))
|
|
17
|
+
cycle_guess: Any = UNSET
|
|
18
|
+
is_cycle_root: bool = False
|
|
19
|
+
cycle_nodes: set[QueryKey] = field(default_factory=set)
|
|
15
20
|
|
|
16
21
|
|
|
17
22
|
@dataclass
|
|
@@ -44,6 +44,7 @@ class GraphStore:
|
|
|
44
44
|
|
|
45
45
|
self.inputs: dict[InputKey, list[InputVersion]] = {}
|
|
46
46
|
self.queries: dict[str, Callable[..., Any]] = {}
|
|
47
|
+
self.query_fixed_points: dict[str, Any] = {}
|
|
47
48
|
self.input_fns: dict[str, Callable[..., Any]] = {}
|
|
48
49
|
self.memos: dict[QueryKey, MemoEntry] = {}
|
|
49
50
|
self.dependents: dict[QueryKey, set[QueryKey]] = defaultdict(set)
|
|
@@ -101,14 +102,23 @@ class GraphStore:
|
|
|
101
102
|
sk = self.key_to_str(key)
|
|
102
103
|
self._stats_by_key[sk] = self._stats_by_key.get(sk, 0.0) + seconds
|
|
103
104
|
|
|
104
|
-
def register_query(self, query_id: str, fn: Callable[..., Any]) -> None:
|
|
105
|
+
def register_query(self, query_id: str, fn: Callable[..., Any], *, fixed_point: Any = None, has_fixed_point: bool = False) -> None:
|
|
105
106
|
with self.lock:
|
|
106
107
|
self.queries[query_id] = fn
|
|
108
|
+
if has_fixed_point:
|
|
109
|
+
self.query_fixed_points[query_id] = fixed_point
|
|
107
110
|
|
|
108
111
|
def lookup_query(self, query_id: str) -> Callable[..., Any]:
|
|
109
112
|
with self.lock:
|
|
110
113
|
return self.queries[query_id]
|
|
111
114
|
|
|
115
|
+
def lookup_query_fixed_point(self, query_id: str) -> tuple[bool, Any]:
|
|
116
|
+
"""Returns (has_fixed_point, fixed_point_value)."""
|
|
117
|
+
with self.lock:
|
|
118
|
+
if query_id in self.query_fixed_points:
|
|
119
|
+
return True, self.query_fixed_points[query_id]
|
|
120
|
+
return False, None
|
|
121
|
+
|
|
112
122
|
def register_input(self, input_id: str, fn: Callable[..., Any]) -> None:
|
|
113
123
|
with self.lock:
|
|
114
124
|
self.input_fns[input_id] = fn
|
|
@@ -419,6 +429,7 @@ class GraphStore:
|
|
|
419
429
|
deps: dict[QueryKey, int],
|
|
420
430
|
effects: dict[str, tuple[Any, ...]],
|
|
421
431
|
last_access: int,
|
|
432
|
+
cycle_nodes: tuple[QueryKey, ...] = (),
|
|
422
433
|
) -> MemoEntry:
|
|
423
434
|
return MemoEntry(
|
|
424
435
|
value=value,
|
|
@@ -428,4 +439,5 @@ class GraphStore:
|
|
|
428
439
|
deps=tuple(Dependency(dep_key, observed) for dep_key, observed in deps.items()),
|
|
429
440
|
effects=effects,
|
|
430
441
|
last_access=last_access,
|
|
442
|
+
cycle_nodes=cycle_nodes,
|
|
431
443
|
)
|
|
@@ -218,10 +218,15 @@ class Engine:
|
|
|
218
218
|
self._store.register_input(handle.id, fn)
|
|
219
219
|
return handle
|
|
220
220
|
|
|
221
|
-
def query(self, fn: Callable[..., Any]) ->
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
def query(self, fn: Callable[..., Any] | None = None, *, fixed_point: Any = _UNSET) -> Any:
|
|
222
|
+
def decorator(f: Callable[..., Any]) -> _QueryHandle:
|
|
223
|
+
handle = _QueryHandle(self, f)
|
|
224
|
+
has_fixed_point = fixed_point is not _UNSET
|
|
225
|
+
self._store.register_query(handle.id, f, fixed_point=fixed_point, has_fixed_point=has_fixed_point)
|
|
226
|
+
return handle
|
|
227
|
+
if fn is not None:
|
|
228
|
+
return decorator(fn)
|
|
229
|
+
return decorator
|
|
225
230
|
|
|
226
231
|
def accumulator(self, name: str) -> Accumulator:
|
|
227
232
|
return Accumulator(self, name=name)
|
|
@@ -29,6 +29,7 @@ tests/test_engine_private.py
|
|
|
29
29
|
tests/test_engine_private2.py
|
|
30
30
|
tests/test_evaluator_and_synthetic_coverage.py
|
|
31
31
|
tests/test_examples.py
|
|
32
|
+
tests/test_fixed_point.py
|
|
32
33
|
tests/test_internal_invariants.py
|
|
33
34
|
tests/test_performance.py
|
|
34
35
|
tests/test_repro_touch_memo_keyerror.py
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from cascade.engine import Engine, CycleError
|
|
3
|
+
|
|
4
|
+
def test_fixed_point_basic():
|
|
5
|
+
engine = Engine()
|
|
6
|
+
|
|
7
|
+
@engine.input
|
|
8
|
+
def input_val():
|
|
9
|
+
return 1
|
|
10
|
+
|
|
11
|
+
@engine.query(fixed_point=0)
|
|
12
|
+
def cell_a():
|
|
13
|
+
return cell_b() + input_val()
|
|
14
|
+
|
|
15
|
+
@engine.query
|
|
16
|
+
def cell_b():
|
|
17
|
+
return cell_a() // 2
|
|
18
|
+
|
|
19
|
+
assert cell_a() == 1
|
|
20
|
+
assert cell_b() == 0
|
|
21
|
+
|
|
22
|
+
input_val.set(value=4)
|
|
23
|
+
assert cell_a() == 7
|
|
24
|
+
assert cell_b() == 3
|
|
25
|
+
|
|
26
|
+
def test_fixed_point_no_default_raises():
|
|
27
|
+
engine = Engine()
|
|
28
|
+
|
|
29
|
+
@engine.query
|
|
30
|
+
def cell_a():
|
|
31
|
+
return cell_b()
|
|
32
|
+
|
|
33
|
+
@engine.query
|
|
34
|
+
def cell_b():
|
|
35
|
+
return cell_a()
|
|
36
|
+
|
|
37
|
+
with pytest.raises(CycleError):
|
|
38
|
+
cell_a()
|
|
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
|