fix(backtest): scope job sweep to this terminal, stop leaking tester processes, tail the real log - #13
Open
Marinski wants to merge 1 commit into
Conversation
Marinski
force-pushed
the
fix/backtest-job-scoping-and-process-lifecycle
branch
2 times, most recently
from
August 9, 2026 05:56
8594627 to
41e9996
Compare
…il the real log Three defects found while investigating backtests reported as failed on terminals that had in fact run them. They compound each other, so they are easier to read together than apart. Startup sweep failed other terminals' running jobs -------------------------------------------------- Every backtest API on a host shares one logs/backtest-jobs directory, and sibling terminals also share broker and account — they differ only by instance, which the job record did not carry. sweep_orphans() therefore failed every recently-touched in-flight job it found, so restarting one terminal's API marked every other terminal's running backtest as "API restarted before completion" while those runs went on to finish and write valid reports. The job now records its instance and the sweep skips jobs belonging to another terminal. A job that names no terminal is still swept: that is what a single-terminal install writes, and what every job written before this field existed looks like, so installs with nothing to distinguish keep their behaviour exactly. Failure messages carried a months-old MetaEditor tail ------------------------------------------------------ _tail_terminal_log() took the alphabetically last *.log in the terminal's log directory. That is always metaeditor.log — "m" sorts after every "<date>.log" — and it is written once at install and never touched again, so every failure message quoted stale compile output instead of the run that had just died. Not cosmetic: it is why a genuine agent "bind error on 127.0.0.1:3000 [10048]" sat unread in the real log while the failure text showed unrelated MetaEditor lines. Select by mtime, exclude metaeditor.log. Leaked tester processes kept the agent ports bound ---------------------------------------------------- A test runs as terminal64.exe plus one metatester64.exe per agent, and it is the agents that bind the localhost ports MT5 allocates from 3000 up. subprocess.run kills the process it started, so the timeout path looked covered, but it does not touch the agents, nor a terminal MT5 relaunched in place of the one we spawned (the case _await_self_relaunch exists for). Those survivors keep their ports bound for as long as the host stays up, and every later run on that terminal then dies instantly with "bind error [10048]" and no report. The timeout path now clears the whole terminal directory's tester processes, and startup does the same. Terminate first, then kill whatever ignores it. Matching is by terminal directory so a sibling instance is never touched, and the startup pass is gated on backtest mode because that cleanup thread also runs in live mode, where the terminal is meant to stay up. The startup kill is deliberately not conditional on the sweep having found anything: a run whose state file predates the sweep lookback — an API killed while a long test was live — is never swept, and gating the kill on that would leave exactly the process that is holding the ports. Self-heal wickworks sidecar orphaned when the VM container restarts --------------------------------------------------------------------- The wickworks TA sidecar shares the mt5 VM container's netns via network_mode: service:mt5. When the VM container is recreated or restarted, Docker gives it a fresh netns but leaves the sidecar running in the old, now-orphaned one. The sidecar's own loopback /health keeps answering, so the image's built-in healthcheck stays green while the Windows VM can no longer reach wickworks — every /rates/ta call then fails with connection refused and surfaces as a 502. Add a self-heal healthcheck (scripts/wickworks-healthcheck.py) that probes the dockurr gateway services (20.20.20.1:445/139/5900/5700), which only exist while the sidecar still shares the LIVE mt5 netns, and kills the uvicorn child when they stop answering, letting `restart: unless-stopped` recreate it into the current netns. Wired into the j2 compose template and the example via a read-only volume mount plus a healthcheck with a 30s start_period so a slow boot is not mistaken for orphaning. No configuration changes are required and single-terminal installs behave as before. 25 new tests; the full suite passes in the container test image.
Marinski
force-pushed
the
fix/backtest-job-scoping-and-process-lifecycle
branch
from
August 10, 2026 10:41
b3cd464 to
71cb801
Compare
Marinski
marked this pull request as ready for review
August 10, 2026 10:45
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.
Three defects found while investigating backtests reported as failed on terminals that had in fact run them. They compound each other, so they ship together. No configuration changes are required and single-terminal installs behave exactly as before.
1. The startup sweep failed other terminals' running jobs
Every backtest API on a host shares one
logs/backtest-jobsdirectory — thesweep_orphans()docstring already says so. Sibling terminals also sharebrokerandaccount; they differ only byinstance, which the job record did not carry. So the sweep failed every recently-touched in-flight job it found, and restarting one terminal's API marked every other terminal's running backtest as"API restarted before completion"while those runs went on to finish and write valid reports.The job now records its
instance, and the sweep skips jobs belonging to another terminal. A job that names no terminal is still swept — that is what a single-terminal install writes, and what every job written before this field existed looks like — so installs with nothing to distinguish are unaffected.2. Failure messages carried a months-old MetaEditor tail
_tail_terminal_log()took the alphabetically last*.login the terminal's log directory. That is alwaysmetaeditor.log("m"sorts after every"<date>.log"), and it is written once at install and never touched again. So every backtest failure message quoted stale compile output instead of the run that had just died.Not cosmetic: it is why a genuine agent
bind error on 127.0.0.1:3000 [10048]sat unread in the real log while the failure text showed unrelated MetaEditor lines. Now selected by modification time, withmetaeditor.logexcluded.3. Leaked tester processes kept the agent ports bound
A test runs as
terminal64.exeplus onemetatester64.exeper agent, and it is the agents that bind the localhost ports MT5 allocates from 3000 upward.subprocess.runkills the process it started, so the timeout path looked covered — but it does not touch the agents, nor a terminal MT5 relaunched in place of the one we spawned (the case_await_self_relaunchexists for). Those survivors keep their ports bound for as long as the host stays up, and every later run on that terminal then dies instantly withbind error [10048]and no report.The timeout path now clears the whole terminal directory's tester processes, and startup does the same. Terminate first, then kill whatever ignores it. Matching is by terminal directory, so a sibling instance is never touched, and the startup pass is gated on
mode: backtestbecause that cleanup thread also runs in live mode, where the terminal is meant to stay up.The startup kill is deliberately not conditional on the sweep having found anything: a run whose state file predates the sweep lookback — an API killed while a long test was live — is never swept, and gating the kill on that would leave exactly the process that is holding the ports.
Testing
20 new tests across
tests/test_backtest_jobs.py,tests/test_backtest_log_tail.pyandtests/test_backtest_process_cleanup.py, covering sibling-terminal isolation, the legacy job shape, instance normalisation,metaeditor.logexclusion and mtime selection, kill escalation, the live-mode guard, and the not-swept-but-still-leaking case.Full suite in the container test image (
Dockerfile.test, the same onemake testbuilds): 399 passed, 2 skipped, 1 xfailed.Scope
These fixes do not attempt to stop concurrent terminals colliding on agent ports — only the persistent damage, where a leaked process keeps ports bound and every later run on that terminal fails until the host is rebooted.
Worth knowing if you are reading
[10048]lines while chasing this: MT5 appears to retry upward when a port is taken. Terminals here have accumulatedTester/Agent-127.0.0.1-<port>directories spanning 3000–3332, the newest of them well above the base, so a single bind error is not on its own evidence that a run died of a collision.4. The wickworks TA sidecar silently orphans when the VM container restarts
The TA sidecar shares the VM container's network namespace via
network_mode: service:mt5. When the VM container is restarted or recreated, Docker leaves the sidecar in the old, now-orphaned namespace. Its own loopback/healthkeeps answering, so the image's built-in healthcheck stays green while the VM can no longer reach wickworks — every/rates/tacall then fails with connection refused and surfaces as a 502 ("wickworks unreachable").This shipped as commit
b3cd464. It addsscripts/wickworks-healthcheck.py, a self-heal healthcheck that probes the dockurr gateway services (20.20.20.1:445/139/5900/5700) which only exist while the sidecar still shares the LIVE mt5 netns, and kills the uvicorn child when they stop answering — makingrestart: unless-stoppedrecreate the container into the current netns. (kill 1from an exec'd healthcheck is not delivered to the container init, so the uvicorn child is targeted instead.)Wired into
docker-compose.yml.j2anddocker-compose.yml.examplevia a read-only volume mount plus a healthcheck (30s start period so a slow boot is not mistaken for orphaning). No configuration changes required. 5 new tests: 4 unit tests for the healthcheck and 1 compose-generation test asserting the rendered wickworks services carry the mount and healthcheck.