Back to Feed
article 3m read

Slow report sync started over after every interruption, so I made it read its own receipts

A long windowed import of store discount data now skips windows that already have an exact success record, and the route turns away a second sync while one is running, with a few gaps I still have not checked.

Article DepthPractice

Flow

Pieces in the order they lean on each other

  1. 1Saves that repeat safely

    Check: one window run twice leaves the same rows.

  2. 2Success record after rows

    Check: a failed save leaves no success record.

  3. 3Exact-edge lookup

    Check: a receipt with shifted edges is ignored.

  4. 4Reuse adds up

    Check: only the missing window hits the upstream and totals add.

  5. 5One slot in memory

    Check: a second click gets a conflict showing the active range.

Nothing was remembered by my discounts-and-returns sync. It pulls credit data for the stores from a slow upstream, a month or a day at a time. If a long run got cut off near the end and I started it again, it went back to the start. And nothing stopped two syncs from running at once.

Both problems waste calls to an upstream I can't speed up. The shared client I'd moved the importer onto that day queues requests, but a queue doesn't know two requests belong to the same job.

Every finished window was already writing a receipt

Each finished window already saved a record: where it pulled from, its exact start and end, whether it worked, and how much it fetched and stored. Now each window checks for a successful record with the same source and the same edges before it calls out. If it finds one, it skips the fetch, adds the old counts to the totals, and posts a progress line saying it reused that window. Resume is on unless you turn it off.

If you build this, three things matter:

  • Match exact edges, not overlap. A different slicing then refetches instead of skipping data.
  • Make saves safe to repeat. Mine overwrite by source and the upstream's own ID, so a half-done window can just run again.
  • Write the success record after the rows, never before.

The test I added seeds a receipt for one of two windows and checks that only the other one hits the upstream, with the totals added up.

Old counts and old data

A reused window is as fresh as its first pull. If the upstream fixes an old month, you need a run with resume off to see it.

The second click gets a conflict

The sync route keeps one slot in memory: a random token, a start time and the range. If the slot is taken, the request gets a conflict answer that shows what's already running. When the run ends, its cleanup frees the slot, but only if the token still matches. The core of the guard is tiny.

If a run is already going, the request gets a conflict answer. Otherwise it takes the slot with its own ticket, start time and range.

One process, one slot

This only works inside one app process. Two instances would each let one sync through. I run one, so it's fine for now, and a crash clears the slot on its own. I haven't checked whether anything between the claim and the stream can throw and leave the slot stuck until a restart. The guard has no test yet, and a test that fires two claims at once is the next thing I'd add.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward
Continue the thread

The draft button checked for duplicates, then left a gap wide enough for a second one

The button that saves a delivery as a draft in the stores' inventory system checked first and wrote its record only after a slow outside call, so a second click could slip through. Now it claims the delivery in the database first, lets a partial unique index refuse the second claim, fixes a payload hash that couldn't see nested fields, and adds a narrow admin cancel for claims that get stuck.

via #concurrency · #idempotency · #reliability