Skip to content

[Fix] Keep uploading when a write races the write checkpoint - #1055

Open
bean1352 wants to merge 6 commits into
mainfrom
fix/upload-loop-write-checkpoint-race
Open

[Fix] Keep uploading when a write races the write checkpoint#1055
bean1352 wants to merge 6 commits into
mainfrom
fix/upload-loop-write-checkpoint-race

Conversation

@bean1352

@bean1352 bean1352 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What's wrong

When the CRUD queue looks empty, the upload loop requests a write checkpoint. updateLocalTarget re-reads the queue inside a write transaction and returns false if it finds a row, but it also returns false when there is nothing left to upload. The loop treated both the same way and stopped with the row still in ps_crud, even though updateLocalTarget had already seen it and logged "New data uploaded since write checkpoint N - need new write checkpoint".

The sync core then refuses every checkpoint with "Could not apply checkpoint due to local data".

The fix

Ask the adapter whether CRUD is still pending before stopping, and go round again if it is. This covers both branches where updateLocalTarget reports a race, including sequence updated.

The retry is delayed by crudUploadThrottleMs.

Tests

One test makes the queue read miss the row once with CRUD notifications left working, which reproduces the sequence in the logs from the original report. The other keeps that read missing and checks the retry stays near the throttle rate. Each one fails without the part of the fix it covers.

Still open

Why nextCrudItem() misses a committed row on React Native. It reads through this.db.getOptional while updateLocalTarget checks inside writeTransaction, so a stale read snapshot in the connection pool is the obvious candidate.

AI disclosure

I used Opus 5 to help research the original bug report, narrow down where the fault could be, and apply the fix. I reviewed and supervised the work.

@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 824eb52

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 7 packages
Name Type
@powersync/shared-internals Patch
@powersync/adapter-sql-js Patch
@powersync/capacitor Patch
@powersync/node Patch
@powersync/react-native Patch
@powersync/web Patch
@powersync/diagnostics-app Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@simolus3

Copy link
Copy Markdown
Contributor

The loop can't tell those apart, so it breaks out while the row is still in ps_crud.

Shouldn't the new write also have triggered a crud trigger? If we change crudUploadLoop to acquire the wait notification promise before the rest of the upload, does that work?

  private async crudUploadLoop(signal: AbortSignal, options: ResolvedSyncOptions): Promise<void> {
    while (!signal.aborted) {
      const crudChangeNotification = this.crudUploadNotifier.waitForNotification(signal);
      await Promise.all([
        // Start the initial CRUD upload on connect. Then, keep polling until we're done.
        this._uploadAllCrud(signal, options),
        this.delayRetry(signal, options.crudUploadThrottleMs)
      ]);

      await crudChangeNotification;
    }
  }

@bean1352

Copy link
Copy Markdown
Contributor Author

The loop can't tell those apart, so it breaks out while the row is still in ps_crud.

Shouldn't the new write also have triggered a crud trigger? If we change crudUploadLoop to acquire the wait notification promise before the rest of the upload, does that work?

  private async crudUploadLoop(signal: AbortSignal, options: ResolvedSyncOptions): Promise<void> {
    while (!signal.aborted) {
      const crudChangeNotification = this.crudUploadNotifier.waitForNotification(signal);
      await Promise.all([
        // Start the initial CRUD upload on connect. Then, keep polling until we're done.
        this._uploadAllCrud(signal, options),
        this.delayRetry(signal, options.crudUploadThrottleMs)
      ]);

      await crudChangeNotification;
    }
  }

As far as I can tell the write does trigger the notification (my mistake), so registering the wait earlier should not change anything. I tried your version against the new test and the upload still stops.

@bean1352
bean1352 marked this pull request as ready for review August 12, 2026 07:39
@bean1352
bean1352 requested a review from simolus3 August 12, 2026 07:45
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