Looking for feedback: adds interceptor lifecycle hook#602
Conversation
|
A preview of 105da95 is uploaded and can be seen here: ✨ https://burr.dagworks.io/pull/602 ✨ Changes may take a few minutes to propagate. Since this is a preview of production, content with |
|
@skrawcz Thanks for opening this — I really like the direction of making remote execution explicit via lifecycle hooks. Before diving into design feedback, I’m trying to fully understand the current execution flow vs the new interception path. A few things I’m exploring:
I’ll follow up with more concrete observations once I’ve traced the execution path locally. |
|
Hey @skrawcz Running the full test suite locally shows failures related to missing optional integration dependencies (redis, mongodb, asyncpg, langchain, etc.), not interceptor behavior. From what I can see so far, the interceptor lifecycle implementation itself appears stable under 3.11. I’m continuing to look into what might be causing the CI 3.11/3.12 failures — especially around async execution paths or example validation. |
|
@skrawcz I ran the full core test suite locally under Python 3.11. All execution, lifecycle, async, and persistence tests pass (264 passing). The only failure is in test_graphviz_display, which requires the Graphviz system binary (dot) to be installed and available on PATH. This appears to be an environment/dependency issue rather than a behavioral regression in the interceptor or lifecycle implementation. Given that all core execution paths pass under 3.11, I don’t see evidence of a Python-version-specific async or hook-related issue. If CI failures are similar, it may be worth ensuring the Graphviz binary is installed in the CI environment or marking visualization tests as optional. |
|
Hey @skrawcz — quick update from my side: I ran core tests locally on Python 3.11 and they pass (264 passed; only optional Graphviz dot env issue can fail if binary missing). CI is currently failing for py3.11/py3.12 + validate-examples, but the logs expired, so we can’t see root cause. Could you please re-run the failed jobs (or the full workflow) so we get fresh logs? Once logs are available, I’ll triage and propose a fix PR (workflow/env vs code). |
|
@skrawcz — local test update (Windows, Python 3.12):
I did notice one PytestUnraisableExceptionWarning related to SQLitePersister.del, but it does not fail the suite. If there are specific CI failures you’d like me to focus on, I’m happy to investigate further. |
|
@goutamk09 sorry missed all these. will try to find some time this week to respond -- feel free to use the dev@ apache email list next time to discuss ... |
|
Thanks @skrawcz — no worries at all. I’ll use the dev@ list next time for broader discussion. |
This proposes a new hook to allow one to remotely push execution of a Burr Action. For example, if one wants to selectively push remote execution of an action to say Ray, then one can now build an interceptor. This also introduces companion hooks that would run pre & post on the remote to mirror what Burr has today - -I thought it would be simpler to not try to reuse those hooks, and instead make it explicit as to what is going on if you wanted to add something similar. See tests for examples, but open to feedback here. Note: we'd need to think through how this plays with parallelism functionality we have. E.g. currently interceptors aren't propagated to sub applications...
These examples needed to be vetted more. If the interceptors here seem good, we could look to bring them in first class to the library.
4f64587 to
105da95
Compare
|
@skrawcz, I looked at the refreshed CI logs — failures are due to the new examples/remote-execution-ray not being covered in pyproject.toml [tool.flit.sdist] include/exclude, and validate-examples expects init.py + statemachine.png.
|
* fix: CI fixes + Temporal interceptor example - Add missing __init__.py in examples/remote-execution-ray/ - Add examples/remote-execution-ray and remote-execution-temporal to pyproject.toml exclude list (fixes validate-examples CI) - Add Temporal interceptor example validating the pattern works beyond Ray: should_intercept checks for "durable" tag, intercept_run routes to Temporal workflow, worker hooks called on remote side * fix: add remote-execution examples to validate_examples filterlist
The async interceptor base classes (ActionExecutionInterceptorHookAsync, StreamingActionInterceptorHookAsync) declared should_intercept as async, but callers use it inside lambdas that cannot be awaited. An async should_intercept would return a coroutine object (always truthy), silently matching every action regardless of the predicate logic. Also fixes: - StreamingActionInterceptorHookAsync.intercept_stream_run_and_update now correctly declared as async (matches isasyncgenfunction check) - Temporal example: replace nonexistent get_hooks() with call_all_lifecycle_hooks_async(), fix worker hook signatures
|
This PR has had no activity for 94 days. Feel free to reopen when you are ready to continue. |
skrawcz
left a comment
There was a problem hiding this comment.
Internal notes from own review:
Thanks for putting this up for feedback — the interceptor concept is a clean, opt-in way to push action execution to Ray/Temporal, and I like that it's genuinely vendor-neutral (core stays dependency-free, everything Ray/Temporal-specific is in examples/). Backwards compatibility is solid: all hooks/interceptors are additive, the private-function signature changes are defaulted, and the sync_hooks/async_hooks → property change is read-compatible (verified: 137 existing test_application.py tests still pass, and the 34 new tests pass).
Before this can move toward merge, a few things:
- Correctness (blocking for merge): the synchronous execution paths (
_run_function,_run_single_step_action,_run_single_step_streaming_action) don't guard against selecting an async interceptor the way the async paths guard against sync ones. Registering only an async interceptor and callingapp.step()producesTypeError: argument of type 'coroutine' is not iterable(I reproduced it). Please mirror theinspect.iscoroutinefunction/isasyncgenfunctionguard on the sync side and raise a clear error for the sync-vs-async mismatch. A test for this case is needed. - API design: the
__INTERCEPTOR_NEW_STATE__magic key is a smell for a public hook API — consider an explicit return contract before stabilizing. - Scope: the ~5000 lines of example markdown/notebook (excluded from
validate_examples.py, so untested) is a lot for a WIP whose interface may still change. I'd split those out and land the hook + one validated example first.
Also missing: error-path tests (interceptor raises / returns malformed result) and an explicit story (guard or test) for the parallelism/sub-application case you already flagged. Nice work on the decorator validation tests and the serde round-trip test.
Inline notes (line refs approximate — folded into body since this is a body-only draft):
burr/core/application.py(~line 186, in_run_function) —[BLOCKING]
The interceptor predicate here islambda hook: hook.should_intercept(action=function)with no sync/async check, andinterceptor.intercept_run(...)is called synchronously. If a user registered only anActionExecutionInterceptorHookAsync, this calls a coroutine function without awaiting and later fails withTypeError: argument of type 'coroutine' is not iterable. Add anot inspect.iscoroutinefunction(hook.intercept_run)clause to the predicate (mirroring_arun_function), and raise a clear error if a sync step hits an async-only interceptor.burr/core/application.py(~line 355, in_run_single_step_action) —[BLOCKING]
Same async-guard gap as_run_function. This is the exact spot the repro traceback lands (if "__INTERCEPTOR_NEW_STATE__" in result:on a coroutine). Guard the interceptor selection against asyncintercept_run.burr/core/application.py(~line 370, in_run_single_step_action) —[SUGGESTION]
The__INTERCEPTOR_NEW_STATE__sentinel key contract (result.pop(...)) is fragile for a public API. Prefer an explicit return type fromintercept_run(e.g.(result_dict, Optional[State])or a small dataclass) so the state hand-off isn't smuggled through a magic dict key.burr/lifecycle/internal.py(~line 315, inget_worker_adapter_set) —[NIT]
This classifies "worker" adapters by crawling the MRO for a singleSYNC_HOOK/ASYNC_HOOKattribute ending in_worker. Becausebase_hooksets exactly one such attribute per class, a class that mixes a_workerhook with a non-worker hook via multiple inheritance could be mis-detected. Low risk today, but a short comment documenting the assumption would help.burr/lifecycle/internal.py(~line 186, property change) —[NIT]
Convertingsync_hooks/async_hooksto read-only properties is fine for reads (verified against existing tests), but note it silently breaks any external code that assigned to these attributes. Worth a one-line changelog mention sinceLifecycleAdapterSetis semi-public.examples/validate_examples.py(line 47-48) —[SUGGESTION]
Both new example dirs are added to the exclusion list, so the ~5000 lines of new example code/docs are never exercised by CI. Either make at least the core Ray example runnable-and-validated, or trim the speculative guides until the API is stable.tests/integration_tests/test_action_interceptor.py—[SUGGESTION]
Great coverage of happy paths and "first wins." Please add: (a) an async-only interceptor invoked via syncapp.step()(should error clearly, notTypeError), (b) an interceptor that raises, and (c) an interceptor returning a malformed/Noneresult. These are the paths most likely to bite users.burr/lifecycle/base.py(PreStartStreamHookWorker/PostEndStreamHookWorker) —[NIT]
These four stream-worker hooks have empty abstract bodies with no:param:docstrings, unlike the run-step worker hooks above them. Add matching param docs for consistency.
This proposes a new hook to allow one to remotely push execution of a Burr Action.
For example, if one wants to selectively push remote execution of an action to say Ray,
then one can now build an interceptor.
This also introduces companion hooks that would run pre & post on the remote to mirror
what Burr has today - -I thought it would be simpler to not try to reuse those hooks, and
instead make it explicit as to what is going on if you wanted to add something similar.
See tests for examples, but open to feedback here.
Note: we'd need to think through how this plays with parallelism functionality we have.
E.g. currently interceptors aren't propagated to sub applications...
Changes
How I tested this
Notes
This change needs to have some thought and discussion.
Checklist