Django-MOJO
MarkdownOpen in ClaudeOpen in ChatGPT

1.15.6 — A deploy stops waiting for a cron tick, and stops lying about the job that did it

Editorial identity incomplete

2026-08-20

A single-node release spent most of its tail doing nothing: after the app was already serving, the deploy killed the job engine that was running it and then waited up to a full minute for a once-a-minute cron entry to start a replacement. That wait is the entire difference between the fastest and slowest releases we measured (49.7s and 95.7s). The update script now restarts the engine itself. It also stopped marking its own successful deploys as failed jobs — the engine was killed with the deploy's job row still open, so the replacement engine's reaper found an abandoned job and failed it. Every deploy now records where its seconds went, so the next slow release names its own slow step. Note the first deploy after upgrading still pays the old wait; the improvement shows on the one after it.

The two waits

A deploy ends by stopping the job engine — necessarily, because that engine is running the very job doing the deploy. Two things then went wrong.

The engine was never restarted by anything but cron. /etc/cron.d/3_mojo_jobs runs once a minute, so a node sat engine-less for anywhere between 0 and 60 seconds before finalization could run and the deployment could reach converged. That jitter accounted for the whole spread between our fastest and slowest observed release.

And the stop always burned its full grace window. Stopping waits for active jobs to drain, but the active job is the deploy, which is blocked on the script, which is blocked on the stop. The drain could never succeed, so the ten-second window ran to exhaustion every time before the process was killed.

update.sh now restarts the engine itself and cron reverts to being the crash backstop. Two safeguards worth knowing about, because both are load-bearing:

Successful deploys are no longer recorded as failures

The engine was killed with the deploy's own job row still marked running and its lease still held, so the replacement engine's reaper found what looked like an abandoned job and marked it failed — on every self-updating node, while the deployment history alongside it said converged. The job now reaches a terminal state before the engine dies, and job history agrees with deployment history.

The job reaper also stops rewriting the recorded error on jobs that had already finished, and no longer claims a job "exceeded max retries" when it was never retried.

Phase timing

Every deploy writes a per-phase timing table that lands on the deployment record, so a slow release identifies its own slow step instead of inviting a guess. This is also the measurement needed before deciding which deploy steps are safe to skip — that work is deliberately not in this release.

Two things to expect

Also in this release

The admin portal now says when an action has finished, and its menus close the way every other menu does.