fix(_app): a logger bound at module scope no longer discards every record - #175
Draft
nilsmechtel wants to merge 1 commit into
Draft
fix(_app): a logger bound at module scope no longer discards every record#175nilsmechtel wants to merge 1 commit into
nilsmechtel wants to merge 1 commit into
Conversation
…rted An app that binds `logger = bioengine.logger` at module scope got the `bioengine.app` fallback, which has no level and no handler, so every INFO record it was given was discarded. The env var that selects the replica branch was only set in-process by `_setup_replica`, i.e. after the import inside `cloudpickle.loads` had already run. Set it in the replica's runtime_env instead, stop caching the branch decision, and configure the fallback logger so it degrades rather than goes silent.
This was referenced Sep 7, 2026
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.
An app that writes
logger = bioengine.loggerat the top of its module gets a logger that silently discards everything it is given. The same line inside a method works. Nothing in the API distinguishes the two, and nothing warns — the records simply never appear in the replica log.Tracked as
#0022in the session backlog.Why the placement decides whether logging works
bioengine.loggerresolves through_get_logger(), which picksray.serveinside a replica andbioengine.appeverywhere else. The branch is decided byBIOENGINE_REPLICA, and the only thing that ever set it wasmixin.py:40, inside_setup_replica— which runs from the user's wrapped__init__.By then the module has long been imported. Ray Serve reconstructs the deployment class in
cloudpickle.loads, which imports the user module, which evaluates module-scope statements.BIOENGINE_REPLICAis unset at that instant, so the fallback branch is taken — and the result was cached in_logger_singletonfor the life of the process, so the later in-processos.environwrite could never repair it.The fallback is not a quieter logger, it is a mute one.
bioengine.appis never configured anywhere: no level, so it inherits the root'sWARNINGandisEnabledFor(INFO)is false; no handler, so anything that did pass the level check would fall tologging.lastResortand print unformatted to stderr rather than into the replica log file.Confirmed on two live replicas —
BIOENGINE_REPLICAwas absent from/proc/<pid>/environin both.Three changes, in order of what they fix
Set the env var where
execcan see it.build_and_run_applicationnow addsBIOENGINE_REPLICA=1toreplica_env_vars, which is merged into every deployment'sruntime_env.env_vars— the normal branch, the container branch, and the proxy. Being in the runtime_env means it is true from the replica process's first line, before any import.It is added after the loop that re-applies
replica_env_varsto the build task's ownos.environ, deliberately: the build task callsserve.run, it is not itself a replica, and claiming otherwise would be a lie the accessor believes.It is also not added in
AppBuilder._build_env_vars, which looks like the natural home. That dict is passed both to the replicas and to the introspection Ray task (builder.py:673), which imports the user's package outside any replica and must keep taking the fallback branch.Stop caching the branch.
_get_loggerre-reads the env var on every call.logging.getLoggeris itself a cached dict lookup, so there is nothing to memoise; the singleton only ever preserved a stale answer. This is what makes_setup_replica's in-process write meaningful as a backstop rather than dead code.Note this does not rescue a module-scope binding on its own:
logger = bioengine.loggercaptures the object, so re-evaluating the branch afterwards changes nothing for that name. The env var has to be right at import time, which is what the first change does. Removing the cache matters for every later access in a process where the var flipped.Make the fallback degraded, not silent. The
bioengine.appbranch now runs it throughcreate_loggeron first use, so it hasINFOand a formatted stream handler. If some path still lands there — the introspection task does, by design — the output is visible rather than swallowed.Verification
tests/_app/test_replica_logger.py, 6 tests. Both behavioural halves have a positive control; the fix was neutered one edit at a time and the failure was checked to be the production symptom, not just a red test:create_loggercall failedtest_the_fallback_logger_actually_emits_infowithassert False where False = <Logger bioengine.app (WARNING)>.isEnabledFor(20)— the mute logger itself, named in the assertion_logger_singletoncache failedtest_the_branch_is_re_evaluated_not_cachedwithassert 'bioengine.app' == 'ray.serve'after the env var was set — the exact stale-branch mechanismThe runtime_env injection has no unit test.
build_and_run_applicationcannot be driven without a live Ray and Serve, and the existingtests/_app/test_env_vars_propagation.pygets around that by re-implementing the merge in the test file, which would pass whether or not the source changed. I would rather say it is unverified than add a test that mirrors the code. It is verified by inspection, and the live check below is the real one.102 tests in
tests/_apppass; 183 acrosstests/_app,tests/appsandtests/worker.Not yet run on a live cluster
The check the issue asks for is a deploy of an app whose module scope,
__init__and one method each emit a distinct probe atINFO, confirming all three reach the replica log — with the module-scope one being the probe that fails today. That needs a dev image on a real worker and has not been done.