Skip to content

Guard against homeservers stuck in a /messages pagination loop#903

Open
gamesguru wants to merge 1 commit into
matrix-org:mainfrom
gamesguru:guru/fix/pagination-loop-guard
Open

Guard against homeservers stuck in a /messages pagination loop#903
gamesguru wants to merge 1 commit into
matrix-org:mainfrom
gamesguru:guru/fix/pagination-loop-guard

Conversation

@gamesguru

Copy link
Copy Markdown

Fixes a couple 1 hour deadlock scenarios observed in Complement during a federation/timeline refactor of homeserver code.

Enhances checks and emits more helpful failure messages.

Additionally asserts no element is served twice (lessening the chances of future regressions or TOCTOU bugs).


Pull Request Checklist

Signed-off-by: Shane Jaroch <chown_tee@proton.me>
@gamesguru
gamesguru requested review from a team as code owners July 17, 2026 11:45
fromToken = endTokenRes.Str

if seenTokens[fromToken] {
t.Fatalf("homeserver returned a pagination token (%s) we've already seen -- it appears to be stuck in loop or repeating elements", fromToken)

@MadLittleMods MadLittleMods Jul 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's valid for a homeserver to return the same pagination token if it doesn't have anything to return yet (still backfilling).

(we should get rid of this check)

Comment on lines +436 to +440
maxIterations := testCase.numberOfMessagesToSend + 1
for iteration := 0; ; iteration++ {
if iteration >= maxIterations {
t.Fatalf("paginated %d times without reaching the start of the room (no `end` token) -- homeserver may be stuck in a pagination loop", iteration)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having maxIterations can make sense 👍

Comment on lines +430 to +434
// Guard against a buggy homeserver that never terminates pagination (e.g. it
// keeps returning an `end` token, possibly oscillating between a small set of
// tokens like `[-1, 1]` -> `[1, -1]` -> ...). Without this, the test would spin
// until the overall test timeout (which can be as long as 3600s) instead of
// failing fast with a useful error.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Guard against a buggy homeserver that never terminates pagination (e.g. it
// keeps returning an `end` token, possibly oscillating between a small set of
// tokens like `[-1, 1]` -> `[1, -1]` -> ...). Without this, the test would spin
// until the overall test timeout (which can be as long as 3600s) instead of
// failing fast with a useful error.
// Guard against a buggy homeserver that never terminates pagination (e.g. it keeps
// returning an `end` token). Without this, the test could spin until the overall test
// timeout instead of failing fast with a useful error.
//
// We chose `testCase.numberOfMessagesToSend + 1` as it allows the homeserver to
// return events one by one with an extra request for good measure (no more events).

// failing fast with a useful error.
seenTokens := map[string]bool{fromToken: true}
maxIterations := testCase.numberOfMessagesToSend + 1
for iteration := 0; ; iteration++ {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for iteration := 0; ; iteration++ {
for paginationAttempt := 0; ; paginationAttempt++ {

// until the overall test timeout (which can be as long as 3600s) instead of
// failing fast with a useful error.
seenTokens := map[string]bool{fromToken: true}
maxIterations := testCase.numberOfMessagesToSend + 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
maxIterations := testCase.numberOfMessagesToSend + 1
maxPaginationAttempts := testCase.numberOfMessagesToSend + 1

maxIterations := testCase.numberOfMessagesToSend + 1
for iteration := 0; ; iteration++ {
if iteration >= maxIterations {
t.Fatalf("paginated %d times without reaching the start of the room (no `end` token) -- homeserver may be stuck in a pagination loop", iteration)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
t.Fatalf("paginated %d times without reaching the start of the room (no `end` token) -- homeserver may be stuck in a pagination loop", iteration)
t.Fatalf(
"paginated %d times without reaching the start of the room (no `end` token) "+
"(saw %d out of %d expected events) -- homeserver may be stuck in a pagination loop",
iteration,
len(filterEventIDs(t, actualEventIDsFromRequest, eventIDs)),
len(eventIDs),
)

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.

2 participants