Skip to content

caveman sync's local-spend watermark has no lock, so two overlapping syncs upload the same rows twice #1132

Description

@AmirF194

What happen

syncLocalSavings (packages/cli/src/index.ts) reads the sync watermark, queries the local spend DB for rows past it, POSTs them to /api/v1/imports, and only then writes the new watermark. There's no lock around that read-query-post-write sequence, so two processes that both fire it around the same time (two terminals finishing a cave wrap session close together, or two cave sync runs) can both read the same watermark before either writes it. Both then fetch the same/overlapping row set and both POST successfully, so the same spend/savings rows get uploaded twice.

It's the same shape as the telemetry watermark race fixed in #1116, just with no guard at all, not even the optimistic check telemetryTokenDelta had before that fix.

Ran it: built the CLI in a container, wrote a regression test using the same technique #1116's own test uses (a --require preload that delays fs.writeFileSync on the watermark file, to widen the exit race), pointed it at sync.json instead of config.json, seeded 2 rows in the local spend DB, and ran two caveman sync processes at once against a stub import endpoint. Both exited 0, both printed "synced 2 spans", the stub got 2 separate POSTs, 4 rows uploaded for 2 real spans.

No idea if the control-api backend dedupes imports server-side by span/trace id since that's not in this repo, so I can't say whether this actually double-counts on a real dashboard, only that the client sends the duplicate POST and the stub accepts both.

Expected

Each local spend row makes it to the dashboard exactly once, whichever process happens to sync it.

Before/after example

Input: two `caveman sync` processes racing, 2 unsynced rows
Got:   both POST the same 2 rows, watermark still ends up correct, but 4 rows landed server-side for 2 real spans
Want:  one process wins the race, the other sees the advanced watermark and sends nothing (or the same lock #1116 already uses)

Platform

Not tied to a particular wrapped agent, any two concurrent cave wrap/cave sync invocations trigger it.

Version / install method

HEAD 2fd153c6, built from source.

Happy to send a PR using the same lock as #1116 if that's useful, just say the word.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions