Commit a9b5d25
[2.x] feat: fail integration tests that run N+1 queries (and fix the first one it found) (#4871)
* feat: fail integration tests that run N+1 queries
Every request sent through the integration TestCase is now inspected for
N+1 query patterns, and the test fails when it finds one. Extension
authors get the feedback while writing the feature rather than when a
forum grows: this session alone, one extension was issuing a query per
post on every page of every discussion, found only by hand-profiling a
live forum.
An N+1 is one query shape executed once per record. The detector groups
a request's query log by normalised SQL — IN lists collapsed, literals
replaced — and fails when a shape repeats past a threshold. Bindings are
counted separately rather than folded into the shape: the same SQL run
for four different users is not the same defect as one query per row,
and conflating them produces false positives (it fooled me on one
extension before this distinction existed).
On by default. A single legitimate shape can be exempted with
allowedRepeatedQueries(); a test case can override
detectsRepeatedQueries(); FLARUM_DETECT_REPEATED_QUERIES=0 disables it
for a whole run.
Verified against core's api suite: identical results with detection on
and off (353 tests, same pre-existing failures, no findings), and
against a real N+1 reintroduced in an extension, where it fails with
'10x (10 distinct bindings)' naming the offending query.
* Apply fixes from StyleCI
* perf: eager load nested includes on the flags endpoint
The flags index declares post.discussion and post.user as default
includes, but never eager loaded what those nested resources need. Core's
DiscussionResource eager loads the actor's discussion state on its own
endpoints; that doesn't carry over when a discussion is included by
another resource. So every flag on the moderation page read
discussion_user on its own, and the flag authors' groups were re-fetched
per flag.
Caught by the N+1 detection added in this branch, on its first CI run.
* feat: warn, rather than fail, when repeated queries don't scale
Running the detector across the bundled extensions turned up 22 findings
beyond the flags N+1, and they were all the same shape: a query repeated
5-10 times for only 1-4 distinct values. That is not an N+1 — five
queries for two users stays five queries whether the forum has two users
or two million. Failing on it would have meant rewriting formatter and
write-path code for no scaling benefit, so the threshold was the thing
that was wrong.
The two cases are now distinguished by the data already being collected.
Roughly as many distinct bindings as executions means one query per
record: that fails, because the work grows with the forum. A handful of
values repeated is wasteful but bounded: that raises a PHP warning, which
PHPUnit attributes to the test without failing the run.
mentions (19 findings) and subscriptions (1) now pass; the flags N+1
still fails when its fix is reverted.
* feat: surface query findings on the pull request, not just in logs
The warning tier was invisible in three separate ways. The trigger_error
call was prefixed with @, so PHPUnit never saw it at all — nothing
appeared even with --display-warnings. Only one of eighteen phpunit
configs in this repo set displayDetailsOnTestsThatTriggerWarnings, so
even a working warning printed no detail. And a developer who relies on
CI rather than local runs would have to read the middle of a job log to
find either.
So: the @ is gone, every integration config displays warning details, and
the detector appends findings to FLARUM_REPEATED_QUERY_LOG when it is
set. The reusable backend workflow points that at a temp file and turns
it into GitHub annotations — errors for N+1s, warnings for non-scaling
repetition — plus a table in the run summary. Annotations attach to the
pull request, which is where the audience that most needs them is
looking.
* feat: tell the developer how to see warning detail
Without displayDetailsOnTestsThatTriggerWarnings PHPUnit prints only a
count — 'Warnings: 11' — which is visible but not actionable. The first
warning of a run now carries the pointer to --display-warnings and the
config setting.
Once per run, not per finding: tests run with processIsolation, so each
test is a separate process and a static flag cannot track 'first'. A
marker file keyed to the project and the hour serves as the shared
signal.
---------
Co-authored-by: StyleCI Bot <bot@styleci.io>1 parent c2e5209 commit a9b5d25
23 files changed
Lines changed: 582 additions & 1 deletion
File tree
- .github/workflows
- extensions
- approval/tests
- audit/tests
- flags
- src/Api/Resource
- tests
- gdpr/tests
- likes/tests
- lock/tests
- mentions/tests
- messages/tests
- nicknames/tests
- package-manager/tests
- realtime/tests
- statistics/tests
- sticky/tests
- subscriptions/tests
- suspend/tests
- tags/tests
- php-packages/testing
- src/integration
- tests/tests
- unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
287 | 287 | | |
288 | 288 | | |
289 | 289 | | |
| 290 | + | |
290 | 291 | | |
291 | 292 | | |
292 | 293 | | |
293 | 294 | | |
294 | 295 | | |
295 | 296 | | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
296 | 327 | | |
297 | 328 | | |
298 | 329 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
99 | 109 | | |
100 | 110 | | |
101 | 111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
8 | 9 | | |
9 | 10 | | |
10 | 11 | | |
| |||
0 commit comments