Skip to content

[2.x] fix: Batch ip_info loading to avoid per-post queries during serialization - #100

Merged
imorland merged 1 commit into
2.xfrom
im/batch-ip-info-loads
Jul 26, 2026
Merged

[2.x] fix: Batch ip_info loading to avoid per-post queries during serialization#100
imorland merged 1 commit into
2.xfrom
im/batch-ip-info-loads

Conversation

@imorland

Copy link
Copy Markdown
Member

Problem

Every serialized post cost one ip_info query for actors who can see IP data: 20 per discussion list, one per post on the post stream. Backtracing showed two compounding mechanisms:

  1. Per-post include resolution: included to-one relations are resolved one post at a time during document building, so firstPost.ipInfo / ipInfo loaded individually per post.
  2. withDefault on the relation: Laravel's eager loading calls getDefaultFor() for every parent before the batched relation query runs, and the default closure ran a getSaved() DB lookup each time. This alone made the relation impossible to batch — endpoint eager loads only halved the count until it was removed.

Fix

  • The discussion endpoints eager load firstPost.ip_info / lastPost.ip_info when those posts are included, and the post endpoints eager load ip_info — mirroring core's mentions extension (mentionsTags). One batched query per relation path, regardless of page size.
  • withDefault is removed from the relationship. The "retrieve missing IPs on view" behaviour moves to serialization time, where a genuine miss is observable on the loaded relation: a new GeoIPRepository::queueLookupForPost() queues the RetrieveIP job (never queries the DB itself). Queue semantics are unchanged on sync and async (redis/horizon/database) drivers, and on the sync driver freshly retrieved info still serializes in the same request.
  • Side-effect fix: posts without stored ip_info no longer serialize linkage to empty withDefault placeholder models — only real rows are linked.

Measured

  • Integration tests: discussion list with 15 discussions × 2 posts: 60 → 2 ip_info queries; post stream: 3 → 1.
  • Real forum, admin actor, /api/discussions?page[limit]=20: total request queries 62 → 43, with payload verified (only genuine ip_info rows serialize).

Tests

New IpInfoLoadQueryCountTest: pins one batched load per relation path on both endpoints, and that a post with a missing ip_info row still queues a RetrieveIP lookup when serialized (the job's cache guard is primed in the test so no external call happens).

Deployment note

Restart queue workers (horizon) after updating so they pick up the changed job/repository code.

Part of the same query-count effort as flarum/framework#4839, FriendsOfFlarum/terms#85, FriendsOfFlarum/moderator-warnings#7, and FriendsOfFlarum/moderator-notes#45.

Two mechanisms made every serialized post cost an ip_info query:

- Included to-one relations are resolved one post at a time, so the
  discussion list and post stream loaded ip_info individually per post.
  The relevant endpoints now eager load the relation alongside the
  posts, one batched query per relation path.
- The relationship's withDefault closure looked the address up in the
  database. Laravel's eager loading invokes getDefaultFor() for every
  parent before the batched relation query runs, so the closure fired
  once per post on every list — this also made the relation impossible
  to batch. The closure is removed; missing lookups are queued at
  serialization time instead, where a genuine miss is observable on the
  loaded relation. Queue semantics are unchanged on both sync and async
  drivers, and freshly retrieved info (sync driver) still serializes in
  the same request.

As a side effect, posts without stored ip_info no longer serialize
linkage to empty withDefault placeholder models.

On a 20-discussion list this removes all 20 per-post queries; the query
count no longer scales with page size.
@imorland
imorland requested a review from a team as a code owner July 26, 2026 15:36
@imorland imorland changed the title Batch ip_info loading to avoid per-post queries during serialization [2.x] fix: Batch ip_info loading to avoid per-post queries during serialization Jul 26, 2026
@imorland
imorland merged commit f08a5af into 2.x Jul 26, 2026
23 checks passed
@imorland
imorland deleted the im/batch-ip-info-loads branch July 26, 2026 15:38
imorland added a commit that referenced this pull request Aug 1, 2026
…105)

The memoizing AuthorFlagPreferenceResolver from #93 survived the #100
restructure but lost the thing that made it batch: nothing loads the
authors ahead of it any more, so with showFlag enabled every distinct
post author costs one preference query during serialization. On a
15-discussion list with firstPost and lastPost included that is 30
single-row user fetches — the exact N+1 #93 fixed.

The resolver now prefers the post's eager-loaded `user` relation, which
costs no query at all, and only falls back to a lookup for a post whose
author was not loaded alongside it. The discussion endpoints eager load
firstPost.user / lastPost.user whenever those posts are included, gated
on the showFlag setting — with the flag off the visibility decision
never consults the author, so nothing extra is loaded. The post stream
needs no new loads: core already eager loads post authors there.

Same request, showFlag on, 15 discussions with 30 distinct authors:
76 queries before, 48 after; the 30 author singles become 2 batched
whereIn loads. The flag-off path is unchanged (covered by the existing
guard test).

The remaining repeated shape the query guard reports (discussions
fetched twice per row) comes from core materialising included first/last
posts without their discussion relation — that fix belongs in core, not
here.
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.

1 participant