Skip to content

Cut sequential D1 round trips on the hottest endpoints - #77

Merged
boomzero merged 3 commits into
masterfrom
perf/fewer-round-trips
Oct 9, 2026
Merged

boomzero merged 3 commits into
masterfrom
perf/fewer-round-trips

Conversation

@boomzero

@boomzero boomzero commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

What does this PR aim to accomplish?:

Makes the backend faster. Production D1 stats for the last 7 days show SQL time isn't the problem: the hottest query (the phpsessid lookup, about 38k runs) averages 0.2 ms. The cost is the network round trip for each query, from the edge to the database. Probe requests from mainland China landed in SEA/SJC, and one D1 read added roughly 50–80 ms there. Several endpoints made those round trips one after another, often in loops.

How does this PR accomplish the above?:

Round trips per request, before → after:

Endpoint Before After
GetMailList 2 + 3 per conversation, one after another 1 query (window functions)
GetBBSMentionList 1 + 2 per mention 1 query
GetPosts 2, and the page query read all ~11k replies (window over bbs_reply) 1 batch(), last reply looked up through idx_bbs_reply_post_time
NewReply / NewPost captcha check, then each read in turn captcha check and reads at the same time (allSettled); a failed read can't hide a failed captcha
@user checks and mention writes (NewReply, EditReply) one at a time, often an xmoj fetch each in parallel, at most 4 at a time; recipients de-duplicated first so two writes for the same person can't both insert
DeletePost one DELETE per reply one DELETE for all replies
Legacy session, first check count, then insert one INSERT OR REPLACE (Database.Insert gains an OrReplace flag)
Legacy session, already dead xmoj asked twice asked once; only a failed fetch or non-2xx response is retried

GetPosts still reads its count and page from one snapshot. A D1 batch runs as one transaction, which replaces the shared-session trick the old comment described.

Small behavior changes:

  • A fractional Page in GetPosts now returns a page instead of an SQL error.
  • A user @-mentioned twice in one reply gets one existence check instead of two.

migrations/0007_add_existing_indexes.sql records three indexes that were created by hand in production but never added to migrations/. It uses the same names with IF NOT EXISTS, so it does nothing in production. A fourth production index, idx_phpsessid, is left out because it duplicates the primary key's own index. It makes local and fresh databases match.

Testing:

  • npm test: 155 pass, 0 fail. 7 new tests cover the retry rule (in both directions), the single upsert, mention de-duplication, the one-batch GetPosts, captcha-first error reporting and the 4-at-a-time mention bound. The 4 that test new behavior fail on the old code; the one that checks retries still happen when xmoj doesn't answer passes on both, as intended.
  • The unit tests mock D1, so I checked the rewritten SQL separately. The old and new queries for GetPosts (three filters × three pages), GetBBSMentionList and GetMailList were run side by side on a real SQLite, built from the migrations plus the production indexes and seeded with random data. That includes ties on reply_time/send_time, mentions of deleted posts and replies, and self-conversations. All 168 comparisons matched across 8 seeds. EXPLAIN QUERY PLAN confirms the new GetPosts uses the index for all three subqueries.
  • wrangler deploy --dry-run builds.

Not included: Smart Placement and moving the D1 primary out of APAC. Both depend on where real users' requests land, which should be checked in the Workers dashboard first.

Does this PR need a documentation update?:

No. CLAUDE.md gains a note on avoiding sequential round trips.


By submitting this pull request, I confirm the following:

  1. I have read and understood the contributor's guide, as well as this entire template. I understand which branch to base my commits and Pull Requests against. (dev for XMOJ-Script, master for everything else).
  2. I have commented on my proposed changes within the code.
  3. I have tested my changes.
  4. I am willing to help maintain this change if there are issues with it later.
  5. It is compatible with the GNU General Public License v3.0
  6. I have squashed any insignificant commits. (git rebase)
  7. I have checked that another pull request for this purpose does not exist.
  8. I have considered and confirmed that this submission will be valuable to others.
  9. I accept that this submission may not be used, and the pull request can be closed at the will of the maintainer.
  10. I give this submission freely and claim no ownership to its content.
  11. I have verified that my changes work correctly in all four UI combinations. (Backend only, so there is no UI here. The API's request and response shapes are unchanged.)

  • I have read the above and my PR is ready for review. Check this box to confirm

🤖 Generated with Claude Code

https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A

Summary by Sourcery

Reduce sequential D1 round trips on high-traffic endpoints while preserving response behavior and consistency.

Bug Fixes:

  • Avoid redundant legacy session validation retries when XMOJ has already rejected a session, while retaining retries for failed or non-successful fetches.
  • Handle fractional GetPosts page values without producing an SQL error and de-duplicate repeated user mentions.

Enhancements:

  • Reduce D1 latency across messaging, mentions, post listing, session handling, and deletion by consolidating queries and running independent checks in parallel with bounded concurrency.
  • Preserve consistent GetPosts pagination snapshots while limiting reply lookups to the requested page.
  • Use a single upsert for verified legacy sessions and bulk-delete replies when removing a post.

Documentation:

  • Document guidance for avoiding sequential and N+1 database round trips in edge Workers.

Tests:

  • Add coverage for session retry behavior, session upserts, mention de-duplication and concurrency limits, captcha error precedence, and batched post retrieval.

Chores:

  • Record existing production indexes in a migration so local and fresh databases match production.

Summary by cubic

Cuts sequential D1 round trips on the hottest endpoints, folding per-item N+1 lookups into single queries and running independent reads and fetches in parallel — network round trips cost tens of milliseconds each while the SQL itself runs in well under one. Slightly different behavior: a fractional Page now returns a page instead of an SQL error, and a user @-mentioned twice gets one existence check. All 153 tests pass; the rewritten queries were compared against the old SQL on a real SQLite across eight random seeds with matching results.

Round trips per request (before → after)

  • GetMailList: 2 + 3 per conversation → 1 query using window functions.
  • GetBBSMentionList: 1 + 2 per mention → 1 query, still dropping mentions whose post is gone.
  • GetPosts: count and page in one batch() (one transaction, one snapshot); the last reply comes from idx_bbs_reply_post_time instead of a window over every reply.
  • NewPost / NewReply: captcha check overlaps the read-only lookups and its error is still reported first when a lookup beside it fails; @-mention checks and writes run in parallel at most four at a time, with recipients de-duplicated first so two writes for the same person can't both insert.
  • DeletePost: one DELETE for all replies.
  • Legacy session: one upsert (Insert gains an OrReplace flag) instead of count then insert; a session xmoj already rejected is checked once, but a failed fetch still gets one retry.

Migration

  • migrations/0007_add_existing_indexes.sql records three production indexes created by hand, using the same names with IF NOT EXISTS so it's a no-op in production and brings local and fresh databases in line.

Written for commit 43cd19f. Summary will update on new commits.

View guided diff Turn on auto-fix

D1 runs these queries in well under a millisecond, but each one is a
network round trip from the edge to the database, which costs tens of
milliseconds. Several endpoints made them one after another in loops.

- GetMailList: 2 + 3 queries per conversation -> 1 query
- GetBBSMentionList: 1 + 2 per mention -> 1 query
- GetPosts: count and page in one batch, which also keeps both on one
  snapshot; last reply found through idx_bbs_reply_post_time instead of
  a window over every reply
- NewReply/NewPost: captcha check overlaps the read-only lookups
- NewReply/EditReply: @-mention existence checks and mention writes run
  in parallel; recipients are de-duplicated first so two writes for the
  same person can't both insert
- DeletePost: one DELETE for the replies instead of one per reply
- CheckSessionID: an upsert instead of count + insert, and a session xmoj
  has already rejected is no longer checked a second time; only a fetch
  that fails or errors is retried

Also records the hand-made production indexes as a migration, so local
and fresh databases match production.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A
@sourcery-ai

sourcery-ai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Sorry @boomzero, you've used your own review budget of 250,000 diff characters for the last 7 days.

You can request another review in 2 days and 12 hours by commenting @sourcery-ai review. Upgrade to get a review now.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T11:51:03.124480Z 50b3cc2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sourcery-ai

sourcery-ai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR reduces latency on the hottest backend paths by replacing sequential D1 and upstream calls with consolidated SQL, D1 batches, and parallel promises, while preserving snapshot consistency and error ordering. It also narrows legacy session retries to transient xmoj failures, records production indexes in migrations, and adds targeted regression coverage.

Sequence diagram for batched post listing

sequenceDiagram
    participant Client
    participant Process
    participant D1
    Client->>Process: GetPosts
    Process->>D1: batch([COUNT query, page query])
    D1-->>Process: CountResult and PageResult
    Process-->>Client: Posts with pagination metadata
Loading

Sequence diagram for parallel reply validation

sequenceDiagram
    actor User
    participant Process
    participant XMOJ
    participant D1
    User->>Process: NewReply
    par Validate captcha
        Process->>XMOJ: VerifyCaptcha
        XMOJ-->>Process: CaptchaResult
    and Load post
        Process->>D1: Select
        D1-->>Process: PostResult
    and Check lock
        Process->>D1: GetTableSize
        D1-->>Process: LockResult
    end
    Process->>Process: FindMentionedUsers
    par Check distinct mentioned users
        Process->>XMOJ: IfUserExistChecker
        XMOJ-->>Process: Exist
    and Check another mentioned user
        Process->>XMOJ: IfUserExistChecker
        XMOJ-->>Process: Exist
    end
    Process->>D1: Insert reply
    par Write de-duplicated mentions
        Process->>D1: AddBBSMention
        Process->>D1: AddBBSMention
    end
    Process-->>User: Result
Loading

Sequence diagram for legacy session retry handling

sequenceDiagram
    participant Process
    participant XMOJ
    participant D1
    Process->>XMOJ: CheckSessionID
    alt Session verified
        XMOJ-->>Process: HTTP 2xx with username
        Process->>D1: Insert with OrReplace
        D1-->>Process: Session recorded
    else Fetch failure or non-2xx response
        XMOJ-->>Process: Failed fetch or HTTP error
        Process->>XMOJ: CheckSessionID
        XMOJ-->>Process: Result
    else Session rejected in valid response
        XMOJ-->>Process: HTTP 2xx without matching username
        Process-->>Process: No retry
    end
Loading

Flow diagram for consolidated read endpoints

flowchart LR
    A[Read endpoint] --> B{Data access pattern}
    B -->|GetMailList| C[One SQL query with window functions]
    B -->|GetBBSMentionList| D[One SQL query with joins and subqueries]
    B -->|GetPosts| E[D1 batch]
    E --> F[Count query]
    E --> G[Page query using reply index]
    C --> H[Lower network round trips]
    D --> H
    F --> H
    G --> H
Loading

File-Level Changes

Change Details Files
Consolidate database reads and writes to reduce edge-to-D1 round trips on high-traffic endpoints.
  • Replace per-conversation and per-mention lookups with windowed or joined SQL queries.
  • Batch GetPosts count/page reads while preserving a shared snapshot, and use indexed correlated lookups for last replies.
  • Delete all post replies with one filtered DELETE and add an INSERT OR REPLACE option for session recording.
Source/Process.ts
Source/Database.ts
Run independent validation and notification work concurrently while preserving response semantics.
  • Run captcha and independent board/post/lock reads through Promise.all, reporting captcha failures first.
  • Deduplicate mentioned users before parallel existence checks and mention inserts.
  • Apply the same parallel mention handling to reply edits.
Source/Process.ts
Refine legacy session retry behavior to retry only transient upstream failures.
  • Track whether the last xmoj session check failed due to fetch or non-2xx response.
  • Stop retrying sessions explicitly rejected by xmoj while retaining one retry for unavailable upstream responses.
Source/Process.ts
test/process.test.js
Align schema metadata and engineering guidance with the new query patterns.
  • Record four existing production indexes via an idempotent migration.
  • Document the requirement to avoid sequential D1 round trips and N+1 queries.
  • Add regression tests for retry behavior, session upsert, mention deduplication, and GetPosts batching.
migrations/0007_add_existing_indexes.sql
CLAUDE.md
test/process.test.js

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 50b3cc20e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Source/Process.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment •

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.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread Source/Process.ts Outdated
Comment thread Source/Process.ts Outdated
Comment thread test/process.test.js
Comment thread CLAUDE.md Outdated
Comment thread migrations/0007_add_existing_indexes.sql Outdated
…licate index

- @-mention existence checks and mention writes run at most four at a
  time. Content can name any number of users, so firing every check at
  once let one request start a burst as large as its input.
- NewReply/NewPost settle the captcha check and the lookups beside it
  with allSettled, so a failed database read can no longer hide a
  failed captcha.
- 0007 no longer adds idx_phpsessid: token is the primary key, which
  SQLite already indexes.
- CLAUDE.md: the round-trip rule targets per-item queries, not loops
  where each query needs the previous one's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017dqWu3YgeVRGwBt5pXDq4A

@cubic-dev-ai cubic-dev-ai Bot left a comment

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.

0 issues found across 4 files (changes from recent commits).

Requires human review: Performance refactor to cut sequential D1 round trips via batching, window functions and bounded concurrency; also changes session retry/upsert and adds migration 0007. Needs human review due to database migration and auth/session behavior changes.

View guided diff | Turn on auto-fix | Re-trigger cubic

@boomzero
boomzero merged commit 9e501ef into master Oct 9, 2026
7 checks passed
@boomzero
boomzero deleted the perf/fewer-round-trips branch October 9, 2026 11:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant