Plugins: each plugin decision costs two queries, so a company page spends 21 of its 69 on policy #1112

Closed
opened 2026-10-31 03:04:05 +00:00 by tiagoagueda · 0 comments
Owner

What exists today

src/postulo/plugins/policy.py:312-318 decide() memoises one answer per (plugin, person) for the request, keyed by the record stamp (_answers(), :300). But each first answer in _decide() (:321) runs its own PluginPolicy.objects.filter(plugin=plugin_name).filter(<person or default>) (:345) and, when no administrator row for the person settles it, access_for() (:157, the query at :169, called at :360), which is a second query on PluginAccess joined to the person's groups. Parents and needs (_unmet_needs) call decide() for further plugins, each again two queries.

Measured with CaptureQueriesContext on the second GET of jobs:company_detail for a user with one company, under postulo.config.settings.test (probe audit/m_scripts/test_probe_queries.py, run as a single file): 69 queries, of which plugins_pluginpolicy 11 and plugins_pluginaccess 10. The count does not grow with the person's data, so tests/test_query_budget.py does not catch it; it grows with the number of plugins and modules installed.

What would change

On the first decision of a request, read every PluginPolicy row for the person and the instance default (person_id = pk OR person_id IS NULL), and every PluginAccess row for the person or their groups, in two queries, and keep them in the same memo.decisions holder as the answers, under the same stamp. _decide() and access_for() then read from those dicts. The rules stay exactly as written (person over group, most permissive group per right, administrator row over access list); only where the rows come from changes. The admin views that call access_for() directly (:197) can keep the single-plugin query.

A budget test beside tests/test_query_budget.py that the company page and the dashboard make at most N policy queries whatever the number of plugins registered guards it.

What it saves or protects

medium: 21 of 69 queries on a company's page (measured), down to 2, on every page that asks what is switched on

Accessibility

none

Decisions

  • Load all rows per request, or cache across requests? Recommended: per request, under the existing stamp; a cross-request cache would need invalidation on every policy and access edit for a saving of two queries.

Found by the efficiency and reliability audit of 2026-10-30 (finding Q1; index postulo/postulo#1108). Checked against main at daee30b15 (2026-10-30).

## What exists today `src/postulo/plugins/policy.py:312-318` `decide()` memoises one answer per (plugin, person) for the request, keyed by the record stamp (`_answers()`, `:300`). But each first answer in `_decide()` (`:321`) runs its own `PluginPolicy.objects.filter(plugin=plugin_name).filter(<person or default>)` (`:345`) and, when no administrator row for the person settles it, `access_for()` (`:157`, the query at `:169`, called at `:360`), which is a second query on `PluginAccess` joined to the person's groups. Parents and needs (`_unmet_needs`) call `decide()` for further plugins, each again two queries. Measured with `CaptureQueriesContext` on the second GET of `jobs:company_detail` for a user with one company, under `postulo.config.settings.test` (probe `audit/m_scripts/test_probe_queries.py`, run as a single file): 69 queries, of which `plugins_pluginpolicy` 11 and `plugins_pluginaccess` 10. The count does not grow with the person's data, so `tests/test_query_budget.py` does not catch it; it grows with the number of plugins and modules installed. ## What would change On the first decision of a request, read every `PluginPolicy` row for the person and the instance default (`person_id = pk OR person_id IS NULL`), and every `PluginAccess` row for the person or their groups, in two queries, and keep them in the same `memo.decisions` holder as the answers, under the same stamp. `_decide()` and `access_for()` then read from those dicts. The rules stay exactly as written (person over group, most permissive group per right, administrator row over access list); only where the rows come from changes. The admin views that call `access_for()` directly (`:197`) can keep the single-plugin query. A budget test beside `tests/test_query_budget.py` that the company page and the dashboard make at most N policy queries whatever the number of plugins registered guards it. ## What it saves or protects medium: 21 of 69 queries on a company's page (measured), down to 2, on every page that asks what is switched on ## Accessibility none ## Decisions - **Load all rows per request, or cache across requests?** Recommended: per request, under the existing stamp; a cross-request cache would need invalidation on every policy and access edit for a saving of two queries. --- Found by the efficiency and reliability audit of 2026-10-30 (finding Q1; index postulo/postulo#1108). Checked against `main` at `daee30b15` (2026-10-30).
tiagoagueda added this to the 0.6.0 milestone 2026-10-31 03:04:05 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#1112
No description provided.