Installing a plugin: the request may run past gunicorn's 120 s timeout, and a killed worker skips the restore #604
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#604
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, by comparing the install path's time budgets with the image's gunicorn settings and following what runs when the worker is aborted.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atplugins/installing.py:89-93(the two budgets),plugins/installing.py:725-814(install_wheel:take_snapshot()at:756,except InstallErrorat:761,verify_importsat:773,write_recordat:810), andcore/server_views.py:1369-1406(_confirm) and:1438-1453(_install);plugins/catalogue.py:296anddocker/Dockerfile:354have not moved.Confirming an upload or installing from a catalogue runs the whole install inside a sync gunicorn worker: a wheel download allowed 120 s per read, a snapshot copy of the whole plugins directory, an installer subprocess allowed 300 s (it resolves and downloads the dependency tree from PyPI), and an import check allowed 60 s. The image kills a worker whose request passes 120 s. The 300-second budget can therefore never be used, and when the worker is killed the install's own clean-up does not run.
plugins/installing.py:76-80(INSTALL_TIMEOUT = 300,VERIFY_TIMEOUT = 60);plugins/installing.py:899-988(install_wheel:take_snapshot()at :930,except InstallErrorat :935,verify_importsat :947,write_recordat :984);plugins/catalogue.py:296(download,timeout=120.0);core/server_views.py:1368-1405(_confirm) and:1437-1452(_install);docker/Dockerfile:354(GUNICORN_CMD_ARGS="--timeout 120 ...").SIGABRT, which gunicorn turns intosys.exit(1), and thenSIGKILL.SystemExitis not anInstallError, so neitherrestore_snapshot()norgive_back_snapshot()runs;subprocess.runkills the half-finished installer on its way out. Depending on the moment:.previous.held(the way back to the state before the last successful install) is left behind and is deleted by the nexttake_snapshot();plugins.json. Every worker started afterwards loads it atready()(the registry reads every entry point on the path, not the record), while Disable and Remove answer "is not installed", because both look it up in the record.The administrator sees only a 502 or a reset connection.
django-tasks-dbtask, as #247 did for other slow work, with a "working…" state on the page and the in-request path only for an instance without a worker. Until then, keep the in-request budgets well under the worker timeout (or derive them from it), and makeinstall_wheelrestore and give back the snapshot on anyBaseExceptionbefore re-raising.Related to #220 and #247 (both closed), which moved slow work out of requests but did not list plugin installs.
Done when
tests/test_plugin_install.pythat makesrun_install(and, separately,verify_imports) raiseSystemExitand asserts the plugins directory,.previousandplugins.jsonare as they were before the attempt.INSTALL_TIMEOUT + VERIFY_TIMEOUTplus the download budget stays below the worker timeout set indocker/Dockerfile, or that installs are queued rather than run in the request.