Back to Ideas 3 min read

Safe to Run Twice

E
EkoHacks Team
·
Safe to Run Twice

Here is a question worth sitting with. What happens if you connect the same repository twice? Maybe the user clicks "Connect" twice because the first click felt slow. Maybe the network hiccuped and the request was retried. Maybe a webhook redelivered an event GitHub already sent. In a naive system, the second run duplicates every pull request and doubles everyone's XP. In ours, nothing bad happens: run it again and the result is the same. That property has a name, idempotency, and the importer earns it with two different techniques for two different kinds of work.

The write: upsert

When the importer processes a pull request, it does not blindly create a row. It upserts. Upsert is a portmanteau of update and insert: "if a row like this already exists, update it in place; otherwise create it." The trick is choosing the right key to decide "a row like this". Ours is the pair (repoId, githubId), the repo plus GitHub's own PR number, which is unique and stable. So on a reimport, PR #42 does not become a second PR #42; it finds the existing row and refreshes it.

await prisma.pullRequest.upsert({
  where: { repoId_githubId: { repoId, githubId: pull.number } },
  update: { title, state, additions, deletions /* ... */ },
  create: { /* the full row */ },
});

That one word, upsert, is why reimporting is safe for the data. The rows converge to the same state no matter how many times you run it. This is the first lesson: give your records a natural key and write through it, so a repeated write changes nothing rather than duplicating.

The side effect: a guard

But data is not the only thing an import does. When a pull request is merged, we award XP. That is a side effect, and side effects do not get idempotency for free. The upsert protects the PR row, but if the merge handler ran twice it would happily award the merge bonus twice, and someone's score would drift upward every time an event redelivered.

So awarding XP needs its own guard. Before paying the merge bonus, the importer asks the ledger a question: has this pull request already earned its merge XP?

const alreadyAwarded = await prisma.xPTransaction.findFirst({
  where: { pullRequestId: prRow.id, reason: 'PR_MERGED' },
});
if (!alreadyAwarded) {
  await awardXP({ /* PR_MERGED, and the right sized bonus */ });
}

The ledger row is not just a record of the payment; it is the guard. Its existence means "this already happened, do not do it again." This is exactly once from at least once machinery: the event may arrive many times, but the effect lands once.

The lesson

The two halves are the whole point. A robust operation separates the work into:

  • Idempotent writes, made safe by a natural key and an upsert. Run them a hundred times, same result.
  • Side effects, made safe by an explicit guard that records "done" and is checked before acting.

The mindset to carry into your own code: assume it will run twice. Networks retry, users double click, queues redeliver, cron overlaps. If your operation would misbehave on a second run, it has a bug that has not happened yet. Make the writes idempotent, guard the side effects, and you can hand someone a "Connect" button without fear of what a nervous second click will do.

E

Written by

EkoHacks Team

More from Ideas

·6 min read

What the Nullable Gave Back

One file, seven behaviours held fixed, the database swapped for a Nullable: about 180 times less time inside the tests, and coverage flat to two decimals.

E
EkoHacks Team
·6 min read

Twenty Six More Tests, Four Fewer Behaviours

Removing the mocks grew the suite from 44 tests to 70 and quietly deleted four behaviours, every one of them a failure path. Test count is not coverage.

E
EkoHacks Team
·6 min read

The Best Coverage Number in the Room

Same commit, same spec, same test count. The mocked suite ran 5.6 times faster, covered 3.5 fewer points of real code, and posted the best branch coverage.

E
EkoHacks Team