Repository navigation
fix: keep failed CRISPR-Copies and Mutagenesis logs by failing the Job in post-job - #169
matthewberry wants to merge 6 commits into
Conversation
…b in post-job Both commands ended `|| (touch error && false)`, so a failed run exited the `job` container non-zero. That container is an init container, so post-job never ran and the log and `error` never reached MinIO. Apply the same change #161 made for CLEAN: write `error` and exit 0, so post-job uploads the outputs and then fails the Job on `error`. The CLEAN exit-code test now covers all three tools, using the known-good CRISPR-Copies and Mutagenesis payloads from test_tool_registry.py, and is renamed to match.
matthewberry
left a comment
There was a problem hiding this comment.
I found no blocking problems.
Checked
- The diff: only the two command builders and their comments change, plus the test file, which is renamed and extended to cover all three tools.
postjob.pyis unchanged from #161. - A failed run is still marked
error:postjob.pyexits 1 after the upload whenevererrorexists.- Jobs are created with
backoffLimit: 0(kubejob_service.py:892) andrestartPolicy: Never, so a failingpost-jobends the Job without a retry. _derive_phasemaps a failed Job toERROR.- Nothing in
app/depends on which container failed. The only reader of container status is the image-digest lookup.
- Failed runs now upload
finished,loganderror: I greppedapp/for code that reads thefinishedorerrorfiles. Onlypostjob.pydoes. Both result routes read only their own output CSV, so a failed run still returns 404 from them, as before. I didn't check the CRISPR-Copies or Mutagenesis frontends. - Tests:
test_tool_job_exit_code.pyandtest_postjob_exit_code.pypass in theDockerfile.testimage (13 passed).- With
&& falserestored in both builders, exactly the four CRISPR-Copies and Mutagenesis failure cases fail. That matches the description.
- The
teeclaim: a word search forteeacrossapp/andchart/, skipping comments, finds only these two commands and CLEAN. - #162: this PR doesn't touch
clean_service.py, so I don't expect a conflict. I didn't test the merge.
Notes, not for this PR
- Deploy order is the main risk:
post-jobalways runsmmli-backend:main. If this branch is deployed before:mainhas #161'spostjob.pycheck, failed runs will be markedcompleted. The description already says this. - #163: failed CRISPR-Copies and Mutagenesis runs will now put
log,errorandfinishedin the v1 artifact listing. #163 hides those by default. - Leaving these out is reasonable: the Mutagenesis
;join and failures in CRISPR-Copies'fetch_organism.pystep both predate this PR.
Not verified: the real tool images, and a run on the cluster. That's the same gap the description states.
|
Closing the two gaps the review left open. Neither needs a change here.
|
…ops it
The command started `set -o pipefail; `. The job template runs it inside
`ls -al ${JOB_INPUT_DIR} && <command> && ...`, so the `;` ended that chain: if listing
the input directory failed, the tool still ran, and without pipefail. CRISPR-Copies and
CLEAN already join with `&&`.
New test: for each tool, with no input directory the tool must not run. It fails for
Mutagenesis with the `;` restored.
|
Following up on the review's note about the Mutagenesis
|
fetch_organism.py and convert_annotations.py run as the crispr-copies prejob_command in the backend's init container. A failure there failed that container, so the job container and post-job never ran and the traceback reached no one. - prejob_command now tees the setup scripts' output to `log` and touches `error` on failure, exiting 0 (in chart/values.yaml, app/cfg/config.yaml and local.config.yaml). - The CRISPR-Copies command skips the tool when `error` already exists, and appends to `log` (tee -a) so the setup output is kept on every path. - post-job then uploads `log` and `error`, and #161's check fails the Job. Tests run the rendered init container and then the job container, sharing one output directory: a failed fetch or convert keeps its traceback and skips the tool; a successful or failed tool run keeps the setup output too. Reverting either the prejob_command or the command fails four of them. A further test checks the chart's prejob_command matches app/cfg/config.yaml.
|
Following up on the review's note about CRISPR-Copies'
Tests: these run the rendered
Reverting either the Still not covered: a failure in |
Reverts e1eacb7. The setup scripts (fetch_organism.py, convert_annotations.py) import config, which logs the merged app_config, including secret.yaml's MinIO keys, database password and hCaptcha secret, at import. The init container mounts secret.yaml, so tee'ing the scripts' output into $JOB_OUTPUT_DIR/log put those secrets in the job log on every CRISPR-Copies run, and post-job uploads that log to MinIO, where /v1/jobs/{id}/artifacts/log serves it. Reproduced in the Dockerfile.test image with marker secrets: all four reached out/log on a successful run. The setup-step fix needs to write only the scripts' own messages to the log.
|
Correction to my previous comment: the setup-step change in e1eacb7 leaked secrets, so I've reverted it in 518fdfc. The Mutagenesis join fix (539f2a7) stays. What went wrong
Reproduced in the Status: none of this was merged or deployed. The branch's tree is now identical to 539f2a7, and the full suite passes (639). Next: the setup-step fix will be redone so that only the scripts' own messages reach the log, not their raw output. |
…secrets fetch_organism.py and convert_annotations.py run as the crispr-copies prejob_command in the backend's init container. A failure there failed that container, so the job container and post-job never ran and nothing was uploaded. The reverted e1eacb7 tee'd the scripts' raw output into the job log. That output includes config.py's import-time log of app_config with secret.yaml merged in, so it published the MinIO keys, database password and hCaptcha secret. This version writes only the scripts' own messages: - app/job_log.py: run_with_job_log() adds a file handler for $JOB_OUTPUT_DIR/log to the script's own logger only, and logs an uncaught exception with its traceback before exiting 1. Both scripts use it when run as scripts (not on import). - prejob_command captures no output. On failure it appends a fixed line to the log and touches `error`, exiting 0 (chart/values.yaml, app/cfg/config.yaml, local.config.yaml). - The CRISPR-Copies command skips the tool when `error` exists and appends to the log (tee -a), as in e1eacb7. Tests run the real prejob_command and scripts with marker secrets: a failed GFF3 conversion leaves its reason in the log and no secrets, and so does a successful run. The pod-level tests now use stand-in scripts that also print output that must stay out of the log. Putting back e1eacb7's tee fails six tests; the original command three; dropping the handler one; the old tool command four.
|
The CRISPR-Copies setup-step fix is redone in cd66c36, without the leak from e1eacb7. How it works
Tests
Gap: a crash before a script sets up its logger, such as a failed import, leaves only the fixed line in the log. The PR description lists this and the other remaining gaps. |
matthewberry
left a comment
There was a problem hiding this comment.
Reviewed at cd66c36. One finding to fix before merge; everything else I checked holds up.
Finding: if the MinIO input sync fails, the log blames the wrong script, and the description gets this case wrong
The rendered init container script is:
mkdir -p … && mkdir -p … && ./prejob.py && pwd && ls -al ${JOB_INPUT_DIR} && (python fetch_organism.py && python convert_annotations.py) || (echo 'CRISPR-Copies setup step failed …' >> log; touch error)
In bash, && and || have equal precedence and group left to right. So the new || (…) applies to the whole chain, not just the setup step. A failure in prejob.py, mkdir or ls also lands in the fallback.
Reproduced in the Dockerfile.test image, using this PR's harness with the stand-in prejob.py changed to exit 1:
- The
initcontainer exits 0. - The output directory holds
errorandlog. - The log contains only "CRISPR-Copies setup step failed (fetch_organism.py or convert_annotations.py)."
- The
jobcontainer skips the tool and exits 0 witherror,finishedandlog.
The job still ends up marked error, so job status is unaffected. Two things are wrong, though:
- The log blames the wrong script. The sync's own output isn't in the log, and the message names two scripts that never ran.
- The description says the opposite. Under "Still not covered" it says a
prejob.pyfailure "still fails theinitcontainer before anything writeslog". That's false.
Why the tests miss it: _run_crispr_pod hardcodes prejob.py to exit 0, and _run_real_setup_step runs prejob_command on its own, outside the template.
Suggested fix: wrap the whole value as one group, { (… && …) || (…; touch …); }, in all three config files. Then add a pod test where prejob.py fails and the init container exits non-zero. If the current behavior is actually preferred, change the message so it doesn't name those two scripts, and correct the description instead.
Checked and cleared
- Rendering: the folded YAML renders the new
prejob_commandas one line, and the quoting survives. - Tests:
test_tool_job_exit_code.pyandtest_postjob_exit_code.pypass at cd66c36 (24 passed). I did not rerun the full suite. - No secrets from other modules: the file handler goes on
__main__'s logger, the logger both scripts use when run directly, and nothing logs to a child of it. Soconfig.py'sapp_configdump can't reach the file. - Messages are logged before exiting: both scripts'
sys.exit(1)paths calllog.errorfirst. Neither usessys.exit("message"). - Exception text: the scripts log MinIO
S3Errors andCalledProcessErrors. Their text (bucket and object names, thedatasetscommand line) contains no credentials. Arbitrary exceptions from other dependencies I didn't check; the description already says so. - File permissions: CRISPR-Copies'
securityContextisrunAsUser: 0, which applies to every container in the pod. So the job container'stee -acan write to the root-ownedlogtheinitcontainer created. - Merges: a
git merge-treeagainst #162 (1e00d2c) is still clean after cd66c36, which the earlier check predated. Other open PRs touching these config files: #171, #164 and #132. I didn't merge-test those. - Prior review notes: the Mutagenesis
;join and the frontend gap were both addressed in follow-ups. The secret leak was caught and reverted before anything was merged.
Note, not blocking
The init container image is hardcoded to mmli-backend:main, like post-job. So job_log.py and the scripts' new entry point only take effect once :main has them. Until then a failure leaves just the fixed line in the log. The existing "don't deploy before :main has #161" note covers the risky ordering, but this is worth one more sentence in the description.
Review on #169: the template splices prejob_command into `mkdir ... && ./prejob.py && pwd && ls ... && <prejob_command>`. bash groups `&&` and `||` left to right, so the new `|| (echo ...; touch error)` applied to the whole chain: a failed MinIO input sync (prejob.py) let the init container exit 0 and logged that fetch_organism.py or convert_annotations.py had failed. Wrap the value in `{ ...; }` in all three config files, so a prejob.py failure fails the init container as before. The pod harness now stops after a failed init container, as the pod does, and takes a prejob.py body. New test: a failed input sync fails the init container, writes no `error` and logs nothing about the setup scripts. It failed before this change.
|
Thanks, confirmed and fixed in 759cf29.
|
Stacked on #161 (itself stacked on #160). It relies on the
postjob.pycheck #161 adds, which fails the Job whenerrorexists. Merge #160 and #161 first.Problem
When a CRISPR-Copies or Mutagenesis run fails, its log never reaches MinIO. Both commands end:
The
&& falsemakes thejobcontainer exit non-zero. Injob.tpl.yaml,jobis an init container andpost-job(which uploads the output directory) is the pod's main container. Kubernetes starts main containers only after every init container succeeds, sopost-jobnever runs, andloganderrorstay on the shared volume. The job is still markederrorcorrectly; only the log is lost.CLEAN had the same problem, and #161 fixed it.
Change
|| touch $JOB_OUTPUT_DIR/error, without&& false. A failed run writeserrorand exits 0; the template then touchesfinished, andpost-jobuploadserror,finishedandlog.postjob.py(from fix: a failed CLEAN run was reported as completed with no results #161) then exits 1 becauseerrorexists, so the Job fails and the watcher marks the joberror, as before.pipefailis unchanged in both.teeacrossapp/andchart/finds only these two commands and CLEAN, all using the same pattern.Testing
tests/test_clean_job_exit_code.pyis renamed totests/test_tool_job_exit_code.pyand parametrized over CLEAN, CRISPR-Copies and Mutagenesis. For each tool it renders the real job template with that tool's real command, using the known-good payloads fromtests/test_tool_registry.py, and runs thejobcontainer's script with a stand-inpython:error,finishedandlogin the output directory.finished,logand the result file, and noerror.&& falserestored in both builders, the four CRISPR-Copies and Mutagenesis failure cases fail.Dockerfile.testimage: 636 passed.Deploy note
Same as #161:
post-jobalways runsmoleculemaker/mmli-backend:main(hardcoded injob.tpl.yaml). A backend running these commands against a:mainimage without #161'spostjob.pycheck would mark failed runscompleted. Don't deploy this branch until:mainhas that check.The backend's
initcontainer is hardcoded to:maintoo. Sojob_log.pyand the scripts' new entry points take effect only once:mainhas them. Until then, a setup failure leaves only the fixed line in the log.Added after review
&&(539f2a7). Its command startedset -o pipefail;. In the template'sls -al ${JOB_INPUT_DIR} && <command> && …chain, the;let the tool run, withoutpipefail, after an earlier step failed. A new test, run for all three tools, checks that the tool doesn't run when there's no input directory.fetch_organism.pyandconvert_annotations.pyrun as thecrispr-copiesprejob_commandin the backend'sinitcontainer. A failure there failed that container, so neither the tool norpost-jobran and nothing was uploaded.app/job_log.py:run_with_job_log()adds a file handler for$JOB_OUTPUT_DIR/logto the script's own logger only, and logs an uncaught exception with its traceback before exiting 1. Both scripts use it when run as scripts, not when imported.prejob_commandcaptures no output. On failure it appends a fixed line tolog, toucheserrorand exits 0. It's changed inchart/values.yaml,app/cfg/config.yamlandlocal.config.yaml.{ …; }because the template splices it into… && ./prejob.py && … && <prejob_command>. Without the group, a failed MinIO input sync also hit the fallback and was logged as a setup-script failure.errorexists, and appends tolog(tee -a).post-jobuploads the log, and fix: a failed CLEAN run was reported as completed with no results #161's check fails the Job.config, which logsapp_configwithsecret.yamlmerged in, so every run's log carried the MinIO keys, the database password and the hCaptcha secret. It was never merged or deployed. That's whyjob_log.pyattaches its handler to each script's own logger and nothing captures raw output.Testing (setup step)
prejob_commandand scripts, run with marker secrets:initandjobcontainers, run in order, with stand-in scripts that write their own message and also print output that must stay out of the log:run_with_job_log: an uncaught exception is logged with its traceback, and another logger's message stays out.prejob.py: theinitcontainer fails, nothing writeserror, and the log says nothing about the setup scripts. The harness stops after a failedinitcontainer, as the pod does. This test failed before 759cf29.tee: 6 tests fail, including both secret checks;prejob_commandmatchesapp/cfg/config.yaml.Still not covered
prejob.pyitself (the MinIO input sync) still fails theinitcontainer before anything writeslog. So does anything that kills a whole container, like an OOM kill or the deadline.