Skip to content

fix: pin multi-concurrent worker start method to fork - #232

Closed
anzheyazzz wants to merge 2 commits into
mainfrom
anzhey/pin-fork-start-method
Closed

anzheyazzz wants to merge 2 commits into
mainfrom
anzhey/pin-fork-start-method

Conversation

@anzheyazzz

Copy link
Copy Markdown
Contributor

Issue #, if available: n/a

Description of changes:

In multi-concurrent (Lambda Managed Instances) mode MultiConcurrentRunner.run_concurrent creates workers with multiprocessing.Process, which uses the interpreter's default start method. That default is fork on Python 3.13 and earlier and forkserver on 3.14 and later, so the same RIC produces two different process models depending on the runtime:

3.13 (fork) 3.14+ (forkserver)
Worker's parent the RIC main process an extra forkserver process the RIC does not manage
Worker memory copy-on-write image of the RIC process at fork time fresh interpreter; nothing from the RIC process is inherited
Extra processes / fds none forkserver process plus its control pipes

This PR pins the start method to fork via multiprocessing.get_context("fork"), so every supported runtime behaves the way 3.13 does today. fork is chosen over spawn/forkserver because it keeps the worker a direct child of the RIC process with its memory image, which is what makes copy-on-write sharing of anything loaded in the parent possible later; spawn/forkserver would close that door. get_context is used instead of set_start_method so the pin applies only to the processes the RIC creates and does not change the default for customer code that uses multiprocessing inside the handler.

The pin introduces two invariants that the parent must keep before run_concurrent forks, both true today and now asserted by tests: the parent is single-threaded (fork copies only the calling thread, and CPython 3.12+ warns on fork from a multi-threaded process), and the parent has not imported customer code (under fork a module already in sys.modules is not re-executed in the worker, which is the mechanism behind the 4.1.0 regression that 4.2.1 reverted). Any future pre-fork feature has to respect both.

Behavior change: none on 3.13 and earlier. On 3.14+ the forkserver process disappears and workers become direct children of the RIC process; worker os.getppid() changes accordingly. No change on the standard on-demand path.

Tests:

  • test_run_concurrent_pins_fork_start_method: get_context is called with "fork" and workers are created from that context.
  • test_workers_are_forked_children_regardless_of_default_start_method (real processes): with the interpreter default forced to forkserver, every worker still reports the test process as its parent, the parent had one thread at fork time, and a module placed in sys.modules before the fork is present in each worker. Fails without the pin ([0, 0, 0] != [1, 1, 1] on the inherited-module check).
  • Existing run_concurrent tests updated to patch multiprocessing.get_context instead of multiprocessing.Process.
  • Unit tests pass on 3.13 and 3.14 images.
  • Deployed to Lambda Managed Instances (3.13, 3.14, 3.15-preview public images with this file overlaid) across 8 init-pattern functions each (standard logging, custom root logger, Powertools logger, background thread, thread pool, canary thread, fixed port bind, file lock): all 24 functions pass every invoke (283/283 successful responses); on all three versions the worker's parent is the RIC main process (/var/runtime/bootstrap.py), no forkserver process exists, and the parent has exactly one thread at fork time. On the shipped 4.1.0 the 3.14 worker's parent was a forkserver process.

Target (OCI, Managed Runtime, both): both (multi-concurrent mode only)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.


Proposed RELEASE.CHANGELOG.md entry (not included in this PR; left for the release bump):

4.2.2

  • Pin the worker process start method to fork in multi-concurrent (Lambda Managed Instances) mode instead of inheriting the interpreter default, which changed from fork to forkserver in Python 3.14. Workers are now direct children of the runtime process on every supported Python version. No behavior change on 3.13 and earlier; no impact on the standard on-demand path.

multiprocessing.Process used the interpreter default start method,
which is fork on 3.13 and forkserver on 3.14+, so the worker process
model differed by runtime. Pin "fork" via get_context so workers are
direct children of the runtime process on every version and the parent
default is left untouched for customer code.
@anzheyazzz
anzheyazzz marked this pull request as ready for review October 15, 2026 08:05
@anzheyazzz anzheyazzz closed this Oct 15, 2026
@anzheyazzz
anzheyazzz deleted the anzhey/pin-fork-start-method branch October 15, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant