Back to Feed
article 4m read

Per-move shift checks gave way to checking the finished week

The scheduling app now plans a whole change set on a copy of the week, checks only the result, and replays a lost save before it asks whether the board is stale. The swap that makes it worth it still only runs in a test.

Article DepthPractice

Flow

Order of checks on one save

  1. 11. Seen this key?

    Same key and same request gets the stored answer. Same key with a different request is an error.

  2. 22. Lock, then ask again

    Two copies of one save cannot both get past this point.

  3. 33. Board version

    An edit built on an old board is refused with the current version.

  4. 44. Load the week

    Too many shifts to load means a refusal, not a partial check.

  5. 55. Plan, then write

    Changes run on a copy, the finished week is checked, then everything is saved together.

Two people work the same morning, and the manager wants them to trade. Done as two separate saves, the first one gives shift A to the second person, and for a moment they're booked into the same morning twice. The end result is fine. The middle step is junk, and a check that runs between the two saves is judging a schedule nobody wanted.

My scheduling app used to save one shift per request. So every check could only look at the schedule one step at a time, and swaps, rotations and moving a whole crew all go through bad steps on the way to a good result.

This week I replaced that with one save per schedule. The browser sends a change set, which is a short list of adds, edits and deletes for one schedule. The server works out the finished week and checks that.

The planner builds the week first

The server copies the saved shifts and applies every change to the copy. Only then does it check the result. Does each shift end after it starts and stay inside the schedule's dates? Is anyone booked twice in this schedule? Is anyone booked against a shift they already have in another schedule? If anything fails, nothing is saved, and the error usually says which change caused it.

Because this step never touches the database, it's easy to test. My tests check that the swap passes, that a real overlap fails, that breaks move with their shift, and that someone who isn't an active, schedulable person can't be assigned.

A few rules are about the request itself. Each shift can appear only once. A save that changes nothing is refused. Moving a shift moves its breaks too, and if a break would land outside the shift, the save fails.

The database still double-checks

All of it happens in one transaction with the schedule locked. If the server would have to load too many shifts, it refuses instead of checking only part of the week. The database has its own no-overlap rule, set to check only at the end, so it doesn't trip on the middle steps. Old overlapping shifts had to be cleaned up first, and that cleanup only unassigned people. It never deleted anything.

Replay first, then check freshness

Each save carries two things: the version of the board the manager loaded, and a one-time key. The version answers "is this edit based on the latest board?" The key answers "have I already done this exact save?"

Order matters when the response gets lost. The save worked, so the version moved on. The browser retries with the same key and the old version. If the server checked the version first, it would tell the manager to reload, and they'd find their own change already there. So the key goes first.

Look up the key first, and if it has been seen, return the answer that was stored.

If the key was used before with a different request, that's an error. The key is checked again after the server takes its lock, so two copies of the same save can't both slip through. My tests replay a saved answer even when the version sent is far newer.

The role labels I mangled

My first version turned custom role names into all caps, and the browser sent every field on every edit. So changing just a shift's end time could change someone's typed role. Same day, I made the server only trim spaces and the browser send only what changed.

Two saves at the same instant

Nothing tests two identical saves landing at once yet. And the board still saves one shift at a time, so the swap works in a test, not in the app.

Keep reading

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

5 paths forward
Related idea

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