Python: every recipe bundle runs in the server's own process - #8752
Draft
knutwannheden wants to merge 3 commits into
Draft
knutwannheden wants to merge 3 commits into
knutwannheden wants to merge 3 commits into
Conversation
A bundle's venv isolates how pip resolves its dependencies; a private module table isolates them at run time, so two bundles' incompatible versions take turns in sys.modules rather than needing a process each. Ownership is by where a module was loaded from, so the engine -- imported from the shared engine root -- matches no bundle and every bundle sees the one instance. With the recipes on the same objects the server already holds, the facade, the per-bundle children and the relay that served each child its own copy of the tree all go: on jd/tenacity that relay was 1,303,024 messages serialized and as many parsed again, in a second process. A venv built by another interpreter is now rebuilt rather than routed around, since there is no child running on its own interpreter to fall back to.
Spawning a Java peer from Python uses the same Content-Length framing, so the connection outlives the bundle children it was written for. What goes with them is the environment they needed: child_command, _child_env and _engine_roots.
Java sends Evict as a JSON-RPC notification, and answering one puts a null-id response on the wire, which fails every request the peer has in flight. The server's own dispatch already guards this; the transport now matches it. Latent until now because the peer was a bundle child, which only ever sent requests. It matters because java_rpc_client drives a Java peer over this.
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.
A
mod runof a pip recipe bundle spawns one Python process per bundle and sends the whole LST to each. The facade deserializes the tree from Java, then re-serializes it per child — onjd/tenacitythat relay is 1,303,024 messages written and as many parsed again, producing no edits.The second process exists to keep two bundles' dependencies apart. A private module table does that in one process, so it isn't needed:
sys.modulesis global, so two versions of one package cannot both besys.modules['dep']at once, but they can take turns. Entering a bundle swaps its entries in and back out.Which modules a bundle owns
Ownership is by where a module was loaded from, not by name. The engine is imported from the shared engine root, so it matches no bundle and every bundle sees the one instance — no list to maintain and no drift.
A dependency the engine declares is already imported and resolves to the engine's copy. That is the same answer a child gives, whose
PYTHONPATHputs the engine roots first.What it costs and what it buys
Two recipe bundles on incompatible versions of one C-extension dependency, in one process, both on the C path rather than falling back to pure Python:
On
jd/tenacitywithUpgradeToPython313, against a CLI built from this branch with the engine installed from a real wheel:Installing a second bundle leaves it at one process, where main spawns two children. Single runs on a contended machine, so read the process counts rather than the timings.
The trade-off
A bundle that dies natively takes the server with it, where before it took only its own child:
A venv built by another interpreter is now rebuilt rather than routed around, since there is no child on its own interpreter to fall back to —
is_usable_venvchecks the recorded version alongside the base it borrows its stdlib from.What goes
facade.py,bundle_children.py, the_hub_*block inserver.pyand the per-child ref tables all existed to move a tree between processes.ChildConnectionstays:java_rpc_clientdrives a Java peer over the same framing, so only the environment its bundle children needed (child_command,_child_env,_engine_roots) goes with them.Tests
test_bundle_overlay.pypins the table itself:Run against the built wheel rather than the source tree. A module deleted from the worktree still resolves from an installed copy in
site-packages, so a source-tree run cannot see deletions at all; the wheel run is what caughtjava_rpc_client's use ofChildConnection.Relationship to Python: a lone recipe bundle runs in the facade's own process #8745
That PR hosts a bundle in-process only while it is the only one, keeping spawn-and-route from the second bundle on. This one removes the spawn path outright.
n = 1; this one pays it at everynand deletes about 1,600 lines.