Skip to content

[2.x] fix: Batch policy state loading to avoid one pivot query per serialized user - #85

Merged
imorland merged 2 commits into
2.xfrom
im/policy-state-batching
Jul 26, 2026
Merged

[2.x] fix: Batch policy state loading to avoid one pivot query per serialized user#85
imorland merged 2 commits into
2.xfrom
im/policy-state-batching

Conversation

@imorland

@imorland imorland commented Jul 26, 2026

Copy link
Copy Markdown
Member

Problem

On endpoints that serialize many users (most visibly a discussion list viewed by a privileged actor), fof/terms issued one fof_terms_policy_user pivot query per distinct user on the page (~30 queries for a 20-discussion list on a busy forum). Two separate paths caused this:

  1. The per-user policy state fields (fofTermsPoliciesState, fofTermsPoliciesHasUpdate, fofTermsPoliciesMustAccept) each lazy-loaded the user's accepted policies.
  2. The permission group processor: any hasPermission()/can() call on a serialized user (e.g. another extension's per-user serializer flag) computes that user's permission groups, and the processor then checks that user's acceptance state to apply the guest downgrade — one lazy pivot query per user.

Fix

  1. The state fields now defer their values (Flarum's deferred field resolution) and buffer users via a new PolicyStateBuffer; the first resolved value loads the accepted policies of all buffered users in a single query — mirroring core's EloquentBuffer approach.
  2. The discussion Index/Show, posts Index, and users Index endpoints now eager load fofTermsPolicies alongside the users they already load, so the permission group processor finds the relation in place. (posts.user.* is deliberately not eager loaded on discussion Show: endpoint eager loads apply to primary models via loadMissing(), which would load a discussion's entire posts relation. Post stream users are covered by the posts Index endpoint.)

Measured

Real forum, admin actor, /api/discussions?page[limit]=20: extension queries 32 → 3 (constant, regardless of page size), total request queries 149 → 120.

Tests

New PolicyStateQueryCountTest pins the constants: 2 pivot queries for a privileged actor over 5 distinct users, +1 for the actor's own enforcement check, and a test registering an ad-hoc per-user hasPermission() field to simulate the second path.

Related: the core half of this investigation was flarum/framework#4839.

Fixes #49.

Two paths lazy-loaded each user's accepted policies individually:

- The per-user policy state fields on the user resource. These now defer
  their values and load the accepted policies of every buffered user in
  one query when the first value is resolved.
- The permission group processor, which runs whenever a permission check
  is made on a user (e.g. another extension's per-user serializer flag)
  and checked that user's acceptance state. The relevant endpoints now
  eager load the relation alongside the users they already load.

On a discussion list of 20 discussions viewed by an admin, this reduces
the extension's queries from one per distinct user on the page (~30) to
a constant 3.
@imorland
imorland requested a review from a team as a code owner July 26, 2026 13:54
@imorland imorland changed the title Batch policy state loading to avoid one pivot query per serialized user [2.x] fix: Batch policy state loading to avoid one pivot query per serialized user Jul 26, 2026
@imorland
imorland merged commit 194470c into 2.x Jul 26, 2026
20 checks passed
@imorland
imorland deleted the im/policy-state-batching branch July 26, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance issue when loading topic

2 participants