Skip to content

AttachmentQueue: foreground saveFile blocks for the full duration of an in-flight upload (shared mutex held across network I/O) #971

@kamadadze

Description

@kamadadze

Summary

In @powersync/common, AttachmentService exposes a single shared Mutex (via withContext) that is acquired by both the foreground AttachmentQueue.saveFile path and the background SyncingService.processAttachments loop. processAttachments keeps that mutex held across the per-attachment remoteStorage.uploadFile / downloadFile calls. As a result, a slow or stalled network request from the background sync loop blocks any foreground saveFile for the full duration of the upload.

Where

  • packages/common/src/client/attachments/AttachmentService.tsmutex = new Mutex(), withContext() wraps the entire callback in runExclusive.
  • packages/common/src/client/attachments/SyncingService.tsprocessAttachments() calls service.withContext(async (context) => { for (...) await this.uploadAttachment(att, context); ... }). The uploadAttachment body awaits this.remoteStorage.uploadFile(blob, attachment), which is user-provided fetch-based I/O.
  • packages/common/src/client/attachments/AttachmentQueue.tssaveFile() ultimately also goes through service.withContext(...) to insert the new attachment row, so it queues behind the in-flight upload.

Observed behaviour

A reproducible hang in a Tauri 2 / WebKitGTK 2.52 desktop app on Linux:

  1. User registers a new person with a photo. UI calls queue.saveFile(...).
  2. At the same time, processAttachments is mid-iteration on a previously queued attachment whose upload has stalled (flaky network, sleeping mobile NAT, etc.).
  3. WebKitGTK's fetch has an implicit ~96 s timeout (Linux TCP keepalive defaults are 2 h, far longer); the stalled upload sits inside the mutex until WebKit finally aborts it.
  4. During that window the foreground saveFile is pending — the user sees a silent infinite spinner; no error is thrown.
  5. Once the upload eventually errors out and the mutex releases, the foreground saveFile proceeds and the file lands on disk normally.

Reproduction

On Linux, applying degraded network with tc netem:

tc qdisc add dev <iface> root netem loss random 30% 25% \
    delay 800ms 400ms distribution normal reorder 25% 50% rate 100kbit

then triggering a foreground saveFile while a previous attachment is mid-upload reliably produces ~96 s delays between [attachments] arrayBuffer ready and [attachments] Photo saved. The delay matches the WebKit implicit fetch timeout almost exactly. Removing the netem rule (or running on a stable network) makes the symptom disappear.

Hypothesis

The mutex correctly protects the in-memory AttachmentContext cache and the atomic state transitions in the attachments table, but its scope in processAttachments is wider than it needs to be — the per-attachment network call has no shared state to protect. Holding the lock across the network I/O means any foreground writer is gated on the slowest in-flight upload.

Environment

  • @powersync/common 1.51.0 (also observed on 1.53.1)
  • @powersync/tauri-plugin 0.0.4
  • Tauri 2.x, WebKitGTK 2.52 on Linux
  • Supabase Storage as RemoteStorageAdapter

Happy to provide additional logs / a minimal repro if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions