Skip to content

A contested byline must not kill the feed that carried it - #146

Merged
ralyodio merged 1 commit into
mainfrom
worktree-slug-race
Aug 19, 2026
Merged

A contested byline must not kill the feed that carried it#146
ralyodio merged 1 commit into
mainfrom
worktree-slug-race

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

The harm

Two Substack feeds have been failing every crawl for hours:

21:39:59  error feed  zinotrust.substack.com could not be crawled — UNIQUE constraint failed: authors.slug
21:40:00  error feed  The Modern Selling Podcast could not be crawled — UNIQUE constraint failed: authors.slug

The cost is wildly out of proportion to the cause. Because the author insert rides in the crawl's own write transaction, losing a slug race takes the whole crawl with it — the feed is recorded uncrawlable and walks up the backoff ladder. Ten consecutive failures marks a feed dead. Over a byline.

The race

claimAuthorSlug reads the slugs already taken, and this insert runs later. Two crawls naming the same author can both pick jane-doe before either commits.

The existing on conflict (identity_key) upsert does not catch it, because the colliding rows are two different people who happen to share a display name.

It predates the write queue — it was failing crawls hours before Redis was switched on.

The fix

SQLite has allowed multiple upsert clauses since 3.35 and libSQL honours them (verified against a real database before relying on it), so this is one line:

on conflict (identity_key) do update set ... where ...
on conflict (slug) do nothing

Doing nothing is the right answer, not merely the safe one. The loser of the race is deferred, not lost: by the next crawl the slug is taken, so claimAuthorSlug picks the next free one and the credit lands then. A byline missing for one cycle is a far smaller thing than a feed marked dead.

upsertAuthor takes the other path into this table from the enrichment pass and had the same hole; closed the same way.

Tests

The tests pin the whole shape, including that the guard does not swallow the ordinary path — a matching identity_key must still update, and only a different identity colliding on slug is a no-op. Verified non-vacuous: removing the clause fails them.

Full suite: 1,157 tests, 0 failures.

Note

Feeds already pushed up the backoff ladder by this will recover on their own once the fix deploys, but their error_count stays elevated until a successful crawl resets it. Say the word if you want those reset explicitly, as I did for the 429s earlier.

Two Substack feeds have been failing every crawl for hours on
`UNIQUE constraint failed: authors.slug`, and the cost is out of all proportion
to the cause: because the author insert rides in the crawl's own write
transaction, losing a slug race took the *whole crawl* with it. The feed was
recorded as uncrawlable and walked up the backoff ladder -- ten consecutive
failures marks a feed dead -- over a byline.

The race is real and older than the queue. `claimAuthorSlug` reads the slugs
already taken and this insert runs later, so two crawls naming the same author
can both pick `jane-doe` before either commits. The `on conflict (identity_key)`
upsert does not catch it, because the colliding rows are two *different* people
who happen to share a display name.

SQLite has allowed multiple upsert clauses since 3.35 and libSQL honours them,
so the fix is one line: `on conflict (slug) do nothing`.

Doing nothing is the right answer and not merely the safe one. The loser of the
race is deferred rather than lost -- by the next crawl the slug is taken, so
`claimAuthorSlug` picks the next free one and the credit lands then. A byline
missing for one cycle is a far smaller thing than a feed marked dead.

`upsertAuthor` takes the other path into this table from the enrichment pass and
had the same hole; it is closed the same way.

The tests pin the whole shape, including that the guard does not swallow the
ordinary path: a matching identity_key must still update, and only a different
identity colliding on slug is a no-op. Verified non-vacuous -- removing either
clause fails them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 0417771 into main Aug 19, 2026
3 checks passed
@ralyodio
ralyodio deleted the worktree-slug-race branch August 19, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant