Plugins pages: every render fetches every catalogue index and signature over the network #602
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#602
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 following whereinstalling.status()gets its provenance and counting the calls in a test.Checked again on 2026-10-01 against
mainat0a5cad587: still present.status()moved fromplugins/installing.pytoplugins/provenance.py:170-230(#248), so the code is now atplugins/provenance.py:212-213(digests = signed_digests() if entries else {}), with the callers atplugins/policy.py:274,accounts/settings_views.py:243and:251,core/server_views.py:961andplugins/management/commands/plugins.py:55(plugins list);signed_digests()(plugins/provenance.py:105-128) andplugins/catalogue.pyhave not moved.When at least one plugin is installed and at least one repository is configured, every view of a person's Settings → Plugins and of Server settings → Plugins downloads and verifies every configured catalogue, inside the request. A page that should cost a file read costs two HTTPS round trips per repository, and it breaks the promise in
plugins/catalogue.py:22-23: "An index is fetched when an administrator opens the page and asks, and never in the background: Postulo makes no request nobody asked for."plugins/installing.py:1301-1302(status():digests = provenance.signed_digests() if entries else {});plugins/provenance.py:105-128(signed_digests()callscatalogue.fetch_all()at :120);plugins/catalogue.py:220-244(fetch,fetch_all: the index and its.sig, one after the other, per repository, with the 10-second default timeout ofhttp.client()); callersplugins/policy.py:283(overview()),accounts/settings_views.py:241(GET) and:249(POST),core/server_views.py:960(Server → Plugins).post()throughoverview(internal=True), and again on the redirected GET. A test that replacedcatalogue.fetch_allwith a counter saw one call peroverview()and one per GET of/settings/plugins/. With a slow or unreachable host each render waits up to the timeouts for each repository in turn and holds a gunicorn worker for that long.marks.get(plugin.name)).core/server_views.py_refresh,catalogue.install) in the cache, keyed by repository and its key, and havesigned_digests()read only that; with nothing cached the answer stays uploaded, as it already is when a repository cannot be reached. Better still, decide an upload's provenance once at install time and store it on the record line, sostatus()needs no catalogue at all.Related to #231 (closed), whose point 6 asked for provenance "cached per record mtime": the record is cached, but the digests it is compared with are still fetched over the network on every render.
Done when
tests/test_plugin_provenance.pythat installs a record line, configures a repository, replacescatalogue.fetch_allwith a counter, and asserts that GET and POST ofsettings:pluginsand GET ofserver:pluginscall it zero times; and one that a Refresh followed by a page view still labels a matching upload as Official or Custom.