Skip to content

[19.0][FIX] base_tier_validation: systray recount cost grows with the reviewer's backlog - #59

Open
bosd wants to merge 1 commit into
OCA:19.0from
bosd:19.0-fix-base_tier_validation-review-count-queries
Open

[19.0][FIX] base_tier_validation: systray recount cost grows with the reviewer's backlog#59
bosd wants to merge 1 commit into
OCA:19.0from
bosd:19.0-fix-base_tier_validation-review-count-queries

Conversation

@bosd

@bosd bosd commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

The problem

res.users.review_user_count is what the systray badge asks for, and its cost
grows with the reviewer's backlog: a handful of SQL queries per pending
review, on every call
. The reviewer with the most to approve pays the most,
every time the badge refreshes.

On a production database (Odoo 19, ~56 users) a single call was measured at
~300 queries and 5–12 seconds, and the query count fell by exactly three for
each review that got approved.

Measured here

The new test grows one reviewer's backlog from 2 to 22 pending documents and
counts the queries review_user_count issues:

2 documents 22 documents
before 26 52
after 22 8

Three per-record read patterns

1. tier.review._can_review_value browses its own document.
self.env[self.model].browse(self.res_id) has a prefetch set of exactly one
id, so every review pays a fresh read of its document's review_ids. Called
from _compute_can_review's per-record loop, that is the ~3 queries per
review. Reading the relation once per model up front puts the values in the
environment cache, where the per-record browse then finds them for free.

2. review_user_count put can_review in the document domain.
_search_can_review re-searches the reviewer's entire backlog on that model
and evaluates the field in Python over all of it — work that is thrown away,
because the candidate set was already narrowed to the pending reviews read
just above. Evaluating the field on those candidates gives the same answer
from one prefetched batch instead of a second search.

3. _get_sequences_to_approve reads reviewer_ids after a filtered().
filtered returns self.browse(kept_ids), whose prefetch set is narrowed to
the records it kept — so the many2many is read one document at a time. This
one is invisible until you dump the SQL: 22 of 29 remaining queries were the
same m2m read repeated per document. Reading it once for the batch, before the
loop narrows anything, is what makes the endpoint genuinely flat.

While there, the cancelled-record filter moves into the domain whenever the
state field is a real column, so PostgreSQL discards those rows instead of
Python discarding them after they have been read; and the "model was
uninstalled" guard the comment already promised is made real (self.env[model]
was evaluated before the check, so an orphaned review raised KeyError).

Test

test_16c_review_user_count_cost_flat_in_backlog pins the property rather than
a magic number: twenty more pending documents may not cost twenty more round
trips. It fails on 19.0 (52 vs a bound of 29) and passes here.

Note for reviewers of #53

19.0-fix-sequential-premature-promotion adds a per-document browse of its own
in _update_review_status:

for model, res_id in {(rev.model, rev.res_id) for rev in reviews}:
    open_reviews = self.env[model].browse(res_id).review_ids.filtered(...)

review_user_count calls that method on every invocation, so merging it as-is
reintroduces the same cost. _prefetch_resource_reviews added here batches it
in one line.

Relation to #52

Independent. #52 bounds how often the systray recounts; this bounds what a
recount costs. Either can merge first.

``res.users.review_user_count`` is what the systray badge asks for, and it
costs a handful of SQL queries *per pending review* -- so the reviewer with
the largest backlog pays the most, every time the badge refreshes. On a
production database a single call was measured at ~300 queries and 5-12
seconds, and the count fell by exactly three for each review that got
approved. Two independent causes:

- ``tier.review._can_review_value`` browses its own document to find the
  lowest pending sequence. Called from ``_compute_can_review``'s per-record
  loop, that browse has a prefetch set of exactly one id, so each review pays
  a fresh read of its document's ``review_ids`` instead of sharing one. Read
  the relation once per model up front; the per-record browse then finds the
  values in the environment cache and issues no query at all. This is the
  ~3 queries per review.

- ``review_user_count`` put ``can_review`` in the *document* domain. Its
  search method re-searches the reviewer's entire backlog on that model and
  then evaluates the field in Python over all of it -- work that is thrown
  away, because the candidate set was already narrowed to the pending reviews
  read just above. Evaluate the field on those candidates instead: same
  answer, one prefetched batch instead of a second search.

While there, the cancelled-record filter moves into the domain whenever the
state field is a real column, so PostgreSQL discards those rows rather than
Python discarding them after they have been read.

The new test pins the property rather than a magic number: growing a
reviewer's backlog from 2 to 12 pending documents may not grow the query
count by more than a constant.

The "model was uninstalled" guard the comment already promised is also made
real: ``self.env[model]`` was evaluated before the check, so an orphaned review
raised a KeyError instead of being skipped.

A third per-record read hid behind the same pattern on the document side:
``_get_sequences_to_approve`` filters ``review_ids`` and then reads
``reviewer_ids`` on what is left, and ``filtered`` returns a recordset whose
prefetch set is narrowed to the records it kept -- so the many2many was read
one document at a time. Reading it once for the batch, before the loop narrows
anything, is what makes the endpoint genuinely flat.
@OCA-git-bot

Copy link
Copy Markdown
Contributor

Hi @LoisRForgeFlow,
some modules you are maintaining are being modified, check this out!

@OCA-git-bot OCA-git-bot added mod:base_tier_validation Module base_tier_validation series:19.0 labels Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:base_tier_validation Module base_tier_validation series:19.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants