Repository navigation
refactor: Run the graph on the caller's thread - #321
Merged
binaryaaron merged 5 commits intoOct 22, 2026
Merged
binaryaaron merged 5 commits into
binaryaaron merged 5 commits into
Conversation
… path Substitute's replacement map now runs on DataDesigner's own transport, retries and scheduling on every route, as it already did when the model skipped its health check and as 0.4.0 does. The graph accounts the map like Redact, Annotate, Hash and Rewrite: one opaque invocation with unknown physical requests. Rows, failed records, retries and cancellation follow DataDesigner. With no native map left, the per-request bridge has no consumer: - delete the native map module, the request bridge and physical transport, and the kernel's native transport acquisition and branches; - delete the facade-unreachable qualified detection/Redact profile and the Redact assessment it published, which were the bridge's other consumer; - drop the request_bridge keyword from the adapter and detection workflow, so caller detectors no longer receive it (0.4.0 never passed it); - keep only the source-frame capability checks of the DataDesigner binding. Tests that pinned per-request receipts now assert an empty request inventory; tests of the deleted profile, bridge and native transport go. Measured (src): 16 files, +52 / -1774, net -1722. Tests: 16 files, +83 / -2283. Docs: 1 file, +7 / -26. Gate: full suite pandas 2.3.3 and 3.0.6 5555 passed each; make format-check and make typecheck clean; parity corpus vs main f4f2b73 0 BLOCKING (both pandas), R0 0 BLOCKING. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
…g to the owner loop The facade's bodies and DataDesigner's per-row hooks no longer call back into the graph's owner loop and wait for it. Their bookkeeping (enter, complete, ready, result, authorize) never awaits anything that can suspend, so it now runs to completion on the calling thread under one runtime lock; `drive()` raises if an owner path ever suspends, which turns the "no owner path suspends" claim into a runtime check. This applies to every owner without physical children, which after the opaque Substitute map is every facade invocation. An owner with physical children (the SDK's worker/foreign-transport path) keeps its owner-loop gate, so in-flight children still serialize bookkeeping as before. One dependency was not visible in the owner methods: Rewrite's repair loop needs the scheduler to start the next iteration's subgraph between two bookkeeping calls of the same body. After a synchronous call that changed the ledger, the runtime applies one scheduler frontier without launching work (subgraph starts and closures only); launches stay on the owner loop. Scheduler state transitions and synchronous bookkeeping hold the same runtime lock, and the scheduler is woken thread-safely. The bindings' loop fields and the hop sites' loop arguments go with the hops. The owner-plural yields (asyncio.sleep(0) every 64 items) go too. One validation-plugin test case injected a suspending _authorize_source; that interleaving no longer exists and the case is removed. Measured (src): 9 files, +140 / -105, net +35 (the design estimated about -50; the SDK child path keeps its owner-loop gate and the frontier step is new). Tests: 1 file, +2 / -17. Gate: full suite pandas 2.3.3 and 3.0.6 5554 passed each; make format-check and make typecheck clean; parity corpus vs main f4f2b73 0 BLOCKING (both pandas), R0 0 BLOCKING. Runtime probe (tests/interface): 10,038 synchronous drives, 0 owner-loop hops, 0 suspensions. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
… thread With no bookkeeping hop left, nothing needs the graph's event loop to keep running while a synchronous body runs. Each public run, preview and evaluate now runs its graph on a private event loop on the caller's own thread (new_event_loop + run_until_complete, no Runner and so no SIGINT handler), and every body runs inline on that thread. The per-invocation host thread, the caller-thread queue and its serve loop, the handoff plumbing, and the settle/drain/cancel_host interrupt machinery are gone. A caller that already runs a loop (Jupyter, asyncio.run) is blocked by the synchronous call anyway: its running-loop marker is set aside for the private loop and restored around each body and on return. Python 3.14 tracks the executing task per thread, so on 3.14 the caller's current task is swapped with the marker. A caller that is itself a BlockingPool worker may start an invocation: it uses no pool, so it cannot wait on its own. Ctrl-C now lands where the body waits, inside DataDesigner, as in 0.4.0. A KeyboardInterrupt or SystemExit raised in a body is kept, the invocation is cancelled and settled, and the same exception is re-raised with the execution retained by the traceback. Bookkeeping, nested invocations and DataDesigner calls all run on the caller's thread; only DataDesigner's own engine threads remain. Tests: the queue-topology interrupt test (test_interrupt_handoff.py) and the nested-handoff test go; route rows stop pinning the handoff; the threads-started test now rejects any anonymizer-graph thread, including the former host. Measured (src): 9 files, +135 / -240, net -105 (design estimate about -130). Tests: 6 files, +19 / -136. Docs: 2 files, +5 / -6. Gate: full suite pandas 2.3.3 and 3.0.6 5547 passed each; make format-check and make typecheck clean; parity corpus vs main f4f2b73 0 BLOCKING (both pandas), R0 0 BLOCKING; test_thread_placement.py green; threads started per run (5 methods x builtin/delegating detector): no anonymizer thread. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
A user relies on one thing: Ctrl-C during run() or preview() stops promptly, cancels the in-flight model requests, raises KeyboardInterrupt and does not hang. test_interrupt.py checks exactly that in a subprocess with a real Anonymizer and DataDesigner whose HTTP transport holds one kind of request open, and real SIGINTs: - plain: Redact run(), detection request held; one Ctrl-C; - asyncio_run: Substitute preview() called from inside asyncio.run; the Runner takes the first Ctrl-C, a second one stops the run; - second: Rewrite run() with a request that ignores cancellation, so DataDesigner's post-cancel wait holds until a second Ctrl-C. Each case asserts the exit status, the time after the last SIGINT and the exact event sequence. The script uses only the public API and DataDesigner's transport hook; the sequences were recorded on main f4f2b73, and the unchanged file passes there. Removed: the remaining in-process and subprocess interrupt tests that pinned the old host topology or graph internals (DataDesigner compat interrupt cases, the injected-redact and evaluate interrupt cases). The queue, native-map and qualified-profile interrupt tests went with the code they tested in the previous commits. Main's telemetry CANCELED test stays. Measured: tests 4 files, +228 / -237; src unchanged. Gate: full suite pandas 2.3.3 and 3.0.6 5538 passed each; make format-check and make typecheck clean; test_interrupt.py 3/3 against main f4f2b73, the graph-SDK line head e0d8b31 and this branch (three runs each); src identical to the previous commit, whose corpus run was 0 BLOCKING. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
The thread paragraph in graph-sdk.md and the cutover note said all graph bookkeeping runs on the caller's thread. Bodies and the bookkeeping they perform do, but the bookkeeping that DataDesigner's per-row hooks trigger is driven synchronously on DataDesigner's own threads, under the runtime lock (_InvocationRuntime.lock, _execution_runtime.py:200; held by _DelegatedOwner._drive_call, _execution_delegated.py:914-936). Both texts now say so. Also removes the paragraph on the qualified development detection/Redact profile, deleted earlier on this branch, and the stale request_bridge mentions in the detector-signature test docstrings (text only). Measured: docs 2 files, +7 / -12; tests 1 file, +2 / -2; src unchanged. Gate: mkdocs build --strict passes; make format-check clean; test_detector_signature_compat.py and test_thread_placement.py 52 passed on pandas 2.3.3 and 3.0.6. 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
binaryaaron
marked this pull request as ready for review
October 22, 2026 18:48
binaryaaron
merged commit Oct 22, 2026
7c386a0
into
aagonzales/graph-sdk/feat/main
10 of 13 checks passed
Contributor
[Medium impact] The PR appears safe to merge based on the reviewed changes. SummaryThe PR moves synchronous graph execution onto a private event loop on the caller’s thread.
Acknowledged by
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Branch
binaryaaron/graph-sdk/refactor/sync-core, five commits on the graph SDK line (PR #319). Branched ate0d8b317; it merges cleanly onto the current line headdddd02fa, which is one docs commit ahead.Summary
The synchronous facade (
run(),preview(),evaluate()) no longer starts a thread per invocation. Each invocation runs its graph on a private event loop on the caller's own thread, and every body runs inline on that thread. Ctrl-C therefore lands where 0.4.0 put it: inside DataDesigner's wait on the caller's thread.Three steps make that possible, one commit each, plus one commit of tests and one of docs:
drive()raises if it ever suspends. Rewrite's repair loop needs the scheduler to start the next iteration between two bookkeeping calls, so a synchronous call that changed the ledger applies one scheduler step that starts subgraphs and closes blocked work but launches nothing. Owners with physical children (the SDK's worker path) keep their event-loop gate.new_event_loop()+run_until_complete, with noasyncio.Runnerand so no SIGINT handler. A caller already inside a running loop (Jupyter,asyncio.run) is blocked by the synchronous call anyway. Its running-loop marker is set aside for the private loop and restored around each body. On Python 3.14, which tracks the executing task per thread, the caller's current task is swapped with the marker. The host thread, the caller-thread queue,handoffand the interrupt settle/drain code are gone.main).Behaviour
run()anonymizer-graph-hostper invocation (two or three with a delegating detector)run()/preview()KeyboardInterruptasyncio.run**kwargsrequest_bridge=Nonerequest_bridgekeywordrequest_bridgekeywordRows, failed records, retries and dtypes show no differences against 0.4.0 beyond the line head's 102 accepted (allowlisted) rows: the parity corpus (166 cases, pandas 2 and 3) reports 0 blocking differences at every commit, with the same allowlisted rows as the line head.
Context variables: copy-in holds as before (values set before the call are visible inside); copy-out still differs from 0.4.0, where a value set inside caller code was visible after
run()returned. That is the accepted delta recorded in the PR #319 status block, unchanged by this PR.Not a compatibility contract
Until the graph SDK is published (REQ 13), nothing about
anonymizer.graphor the engine modules behind it is a compatibility contract: names, export order, signatures, defaults, exception constructors, class pickles, thread names, the graph's request inventory and import cost may change. Tests that pin them are refactor safety nets, re-recorded with the change that moves them. What users can rely on is the shippedAnonymizer/config API, its result columns and errors, and Ctrl-C behaviour; this PR keeps those as the parity corpus and the subprocess Ctrl-C test show.anonymizer.graph.__all__(228 names) and every public signature are byte-identical to the base.Removed engine names and DataDesigner private hooks
Removed engine-level names (none re-exported by
anonymizer.graph):harness.kernel.{acquire_native_transport, qualify_detection_redact, ensure_async_engine_loop, NATIVE_TRANSPORT_GATE_TIMEOUT_NS};detection._graph_profile.{execute_detection_redact, ensure_async_engine_loop}and therun_detection_redact(limits=, opaque=)keywords;DetectionRedactInvocation.submitand its fieldsphysical, slots, execution, semantic_loop, handoff;DetectionBinding.check_outcomes;ndd._graph_requests.{NddPhysicalTransport, NddRequestBridge, SingleAttemptTransportError, require_binding_capabilities, scheduler_salvage_rounds, skip_health_checks_env}; the native-map route inndd._route; the modulereplace/_native_map_requests.py. A caller detector declared with**kwargsno longer receivesrequest_bridge, which matches 0.4.0.DataDesigner private dependencies removed:
DataDesigner._create_resource_provider;ModelRegistry(model_facade_factory=)andModelRegistry._model_facade_factory;ModelFacade.agenerateas a bridge hook;models.clients.retry.create_retry_transport; the_HTTP_CLIENT_FIELDScoupling;readiness._SKIP_MODEL_HEALTH_CHECKS_ENV;AsyncTaskScheduler(salvage_max_rounds=); the bridge's use ofparsers.errors.ParserException(the judge's use remains, as on main).Private hooks that remain:
DataDesigner._create_dataset_builder,BatchStage,SDG_CONFIG_FILENAMEand DatasetBuilder/ArtifactStorage internals (engine/ndd/_graph_requests.py:51-82), and thegenerator.ageneratepatch inengine/ndd/_source_frame.py.Size
Tests: +334 / -2,673. Docs: +12 / -32.
Testing
make format-check,make typecheckandmkdocs build --strictpass.mainat every commit: 0 blocking (pandas 2 and 3), plus the main+fix(interface): only pass entity_label_examples when the detector accepts it #311 reference cases.tests/interface/test_interrupt.py: three cases (plain; insideasyncio.run; second Ctrl-C), each passing onmain, the line head and this branch with identical event sequences.tests/interface/test_thread_placement.pygreen. It now also rejects anyanonymizer-graph*thread started during a run.Not verified
nest_asynciowith the real library.graph_sdk/_foreign_transport.pystays. It has no Anonymizer consumer left, but removing it is a separate SDK-surface decision.🤖 Generated with Claude Code