Repository navigation
Order pending work by commit and snapshot timestamps - #220
ianmacartney wants to merge 17 commits into
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
commit: |
|
One thing to note here is that reverting to an older version would require a migration to convert segments into their old format, - which could be hard to get right, but could make some assumptions about the maximum segment a user would have previously used, or migrate based on creation time of the documents or something..? |
reeceyang
left a comment
There was a problem hiding this comment.
When users migrate to this version, would work that was originally scheduled to run in the future get executed immediately?
4e3cbd1 to
99c41ba
Compare
d55a95d to
b619fc5
Compare
|
@cubic review |
@ianmacartney I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
All reported issues were addressed across 26 files
Not reviewed (too large): example/benchmarks/2026-09-08.json (~5,865 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 17 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
90bf8f7 to
0b0b60e
Compare
|
I split out the pendingStart batching into a stacked PR |
There was a problem hiding this comment.
All reported issues were addressed across 20 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
2ccceca to
ba7eb18
Compare
| segment: max(toSegment(runAt), getCurrentSegment()), | ||
| segment, | ||
| // Only near-term starts can commit behind the cursor. | ||
| ...(scheduled && (segment as bigint) <= toTimestamp(now + 5 * MINUTE) |
There was a problem hiding this comment.
is there a way to avoid the cast here?
There was a problem hiding this comment.
we could do different inserts based on scheduled, since segment is only ever the placeholder if scheduled is false?
| const batchFields = { | ||
| // The segment at query time — what "now" was when the batch was built. | ||
| segment: v.int64(), | ||
| upgrade: v.optional(v.object({ starts: v.array(vStart), done: v.boolean() })), |
There was a problem hiding this comment.
iiuc, these are the legacy entries that we now want to convert to use nanoseconds?
| // (1) Out-of-order entries left at the boundary stamp. Nothing at or above | ||
| // the cursor is read, so a bulk enqueue sharing this stamp costs nothing. |
There was a problem hiding this comment.
which cursor is meant in
Nothing at or above the cursor is read, so a bulk enqueue sharing this stamp costs nothing.
It looks like we do read at least one doc at or above the sweep cursor?
There was a problem hiding this comment.
We start at the sweepCursor for the scanTs field, but we only read up to incomingCursor for the segment, since anything greater than that will get picked up by the normal incomingCursor query.
So if sweep is at 3, incomingCursor is at 10, the queries are:
1. scanTs == sweepCursor && segment < incomingCursor
2. scanTs ≥ sweepCursor
3. segment ≥ incomingCursor
(1) is an optimization for the case where a huge transaction lands in a single scanTs. Instead of scanning over the whole transaction, once we land in the middle of it, the next iteration will only consider the section of it that is actually actionable, before moving on to the next scanTs.
| // Raised to at least the snapshot, which the cursor may reach this run, so | ||
| // the retry can't land behind it; a backoff shorter than the clocks' skew | ||
| // just starts next iteration. | ||
| const segment = maxBigint( |
There was a problem hiding this comment.
is this still needed now that snapshotTs <= Date.now <= commitTs?
There was a problem hiding this comment.
It's not needed for contexts where that's gone out. I'll wait for convex-test to support it before removing it though. I'll leave a TODO
| // ──────────────────────────────────────────────────────────────────── | ||
|
|
||
| describe("getBatch", () => { | ||
| it("waits for wall time when the snapshot clock is ahead", async () => { |
There was a problem hiding this comment.
this scenario is not possible in practice, right?
There was a problem hiding this comment.
it was possible before, but I believe the Date.now() change should be out soon/already. And it can happen in convex-test pretty often, since Date.now() is fixed for tests, but each transaction bumps the db clock
| expect(o.running.map((r) => r.workId)).toEqual([scheduledId]); | ||
| }); | ||
|
|
||
| it("starts scheduled work once it's due, without ever rewriting it", async () => { |
There was a problem hiding this comment.
we don't ever rewrite scheduled work now, right? (unless it's being upgraded from a legacy entry?)
ba7eb18 to
98666b0
Compare
65c31bd to
65184eb
Compare
65184eb to
155405d
Compare
155405d to
0287944
Compare

Order ready work, completions, and cancellations by commit timestamp. A sweep finds scheduled enqueues that commit behind the incoming cursor, which stays bounded by the transaction snapshot.
Upgrade legacy keys and pointers before scanning, preserving their original time ordering. Requires Convex 1.43.
Validation: 162 tests pass; build, typecheck, lint, and changed-file formatting pass.