Repository navigation
perf: batch Loops profile checks and index cap delete, recovery and caps list queries - #3393
Merged
Merged
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Member
Author
|
hey @greptileai, please re-review the PR |
Member
Author
|
hey @greptileai, please re-review the PR |
richiemcilroy
marked this pull request as ready for review
October 26, 2026 14:17
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four database hot spots found by checking query plans against PlanetScale Insights. Behaviour is unchanged in every case: same schedules, same results, same retry and lease semantics. The database simply does far less work.
Loops contact sync: batch the unchanged checks
Most scheduled profile checks find nothing changed, but each one paid for its own claim transaction, four source reads, a license read and a completion commit. The worker now claims up to 50 due jobs at once, reads their profiles with one query per source table, and completes every unchanged job in a single update. Jobs whose profile did change, or that need an identity hold or a Stripe customer, still go through the existing per-job path, one at a time, with their lease renewed first.
flowchart LR A[claimJobs: 50 due jobs, one transaction] --> B[readLoopsProfiles: 1 query per source table] B --> C{profile hash unchanged?} C -- yes --> D[completeUnchangedJobs: 1 update per next-check time] C -- no --> E[renewLease, then existing per-job sync] A -. deadline or 401/403/429 .-> F[releaseJobs: unprocessed leases freed]nextProfileCheck), hashing, retry delays and Loops API calls are untouched, so contacts stay exactly as fresh as before.readLoopsProfiles(packages/database/loops/sources.ts:165) returns the sameLoopsProfileSourceasreadLoopsProfilefor every user. Ids are matched case-insensitively, like the column collation the single reads use. License rows are attributed withFIELD(u.email, ...), so email matching keeps the column collation too.completeUnchangedJobs(packages/database/loops/jobs.ts:99) keeps the revision guard per row,(userId, revision) IN (...), so a sync queued mid-batch stays immediately due.packages/database/loops/jobs.tsso the MySQL contract tests can import it without the worker's app dependencies.Deleting a cap: index
notifications.videoIdDELETE FROM notifications WHERE videoId = ?runs inside the cap delete transaction (API, agent and organization deletes). No index starts withvideoId, so it scanned the whole table, and under REPEATABLE READ it locked every scanned row until commit. That blocked new notifications while the delete ran.Recovery crons: index
video_uploads.started_atThe stale upload recovery queries filter on a recent
started_atwindow but scanned the whole table to find it.Caps list: fetch the page before aggregating
The
/dashboard/capsquery joined comments, organizations, users and uploads for the owner's entire root library, grouped it, and only then appliedLIMIT. Its cost grew with library size on every page view. It now selects the page ids first (apps/web/app/(org)/dashboard/caps/page.tsx:154), then aggregates those rows only. A new(orgId, ownerId, folderId, effectiveCreatedAt)index lets the page subquery read exactlylimitindex entries in reverse.id DESC), so paging can't repeat or skip them.Benchmarks
MySQL 8.4, synthetic dataset (200k videos, 250k notifications, 30k uploads, one owner with 4.5k root caps), median server time from
EXPLAIN ANALYZE,mainvs this PR:notificationsbyvideoIdSchema
packages/database/migrations/0051_fancy_talkback.sqlis additive only: threeCREATE INDEXstatements, no drops or column changes.

Confidence Score: 5/5
The PR appears safe to merge; the latest change fixes the batch-stopping profile error.Summary
This PR batches Loops profile checks, adds three database indexes, and selects each caps page before counting related rows.
videoIdindex.started_atindex.Reviews (3) · Last reviewed commit: "fix(loops): defer only the job whose bat..." · Reviewed by Greptile