Skip to content

fix(hardcover): stop the language evidence query timing out (#3333) - #3334

Merged
vavallee merged 1 commit into
vavallee:mainfrom
BerzinsU:fix/3333-hardcover-evidence-or-timeout
Oct 18, 2026
Merged

vavallee merged 1 commit into
vavallee:mainfrom
BerzinsU:fix/3333-hardcover-evidence-or-timeout

Conversation

@BerzinsU

@BerzinsU BerzinsU commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Summary

GetAuthorWorkLanguageEvidence and its follow-up for blank-language works (resolveBlankWorkLanguages) both filtered editions with {_or: [{book: {slug: {_in: $slugs}}}, {book_id: {_in: $bookIds}}]}. Hardcover can't serve that _or from an index, so the query ran until Hardcover cancelled it with HTTP 408 at about 15 s. Bindery's own 15 s client timeout fired first and query() retried twice, so every author with Hardcover works lost 45–70 s and its works fell back to the default-edition language.

Both lookups now go through one helper, fetchWorkEditions. It sends the slug predicate on its own and adds a separate book_id query only when some works are named by numeric id. bookIDs is filled only for numeric slugs, so in practice each lookup is one query. Rows from the two queries are merged by book id. Each query declares only the variables it uses, because Hasura rejects both declared-but-unused and undeclared variables with validation-failed.

Closes #3333.

Implementation notes

  • The follow-up query had the same _or. The issue names only the allowed-language query, but GetAuthorWorkKnownLanguages (added in fix(metadata): ISBN cache scope, supplement translations, OpenLibrary cover_edition, rebind editions #3102; the allowed-language query itself dates from fix(hardcover): don't treat translated default editions as work language #2754) uses the same predicate and also gets a 408. I measured it live at 15.4 s for Oliver Burkeman. On main it never runs, because the first query always fails before it. Fixing only the first query would have moved the timeout into the follow-up, where the failure is logged only at DEBUG.
  • Same guarantees as before. If either query fails, the whole lookup fails and returns no evidence, so the caller falls back as it does today. "A successful bounded result proves there is no allowed edition" still holds, because it is only used when every query that ran succeeded. Each query's limit is the number of works it names. For the slug query that is the old len(evidence), and for the book_id query it is the number of ids.
  • Merging. Both queries use the same distinct_on: [book_id] and order_by, so a work matched by both comes back as the same row, and the duplicate is dropped. Callers still key rows by hcBookRef.foreignID(), as before.
  • Empty lists. An empty slug or id list sends no query. When both are empty, nothing is sent, and the callers already return before reaching that point.
  • TestGetAuthorWorkLanguageEvidence_BatchesAllowedEditionLookup expected exactly one request carrying bookIds: [42], which encoded the _or. It now expects one slug query and one book_id query and checks the variables and limit of each.

How it was verified

The new tests fail on main for the reason in the issue

language_evidence_predicates_test.go adds a fake Hardcover that behaves like the real one where it matters here:

Request The fake answers
An editions query that combines the book.slug and book_id predicates HTTP 408 {"error":"Request timeout"}, the body Hardcover returns live, where it takes about 15 s
A variable that is declared but unused, or sent without being declared 200 with Hasura's live validation-failed error
Anything else Rows chosen by the slug or book_id predicate and the language filter, with distinct_on(book_id) and $limit applied

The new test file run against unmodified main (b0b99f7e):

=== RUN   TestGetAuthorWorkLanguageEvidence_SlugNamedWorksQueryBySlugAlone
    language_evidence_predicates_test.go:206: language evidence lookup failed: hardcover get author work language evidence: HTTP 408: Request timeout
--- FAIL: TestGetAuthorWorkLanguageEvidence_SlugNamedWorksQueryBySlugAlone (0.00s)
=== RUN   TestGetAuthorWorkLanguageEvidence_NumericIDsQueriedSeparatelyAndMerged
    language_evidence_predicates_test.go:255: language evidence lookup failed: hardcover get author work language evidence: HTTP 408: Request timeout
--- FAIL: TestGetAuthorWorkLanguageEvidence_NumericIDsQueriedSeparatelyAndMerged (0.00s)
=== RUN   TestGetAuthorWorkLanguageEvidence_EitherPredicateFailingReturnsNoEvidence
--- PASS: TestGetAuthorWorkLanguageEvidence_EitherPredicateFailingReturnsNoEvidence (0.00s)
    --- PASS: TestGetAuthorWorkLanguageEvidence_EitherPredicateFailingReturnsNoEvidence/slugs (0.00s)
    --- PASS: TestGetAuthorWorkLanguageEvidence_EitherPredicateFailingReturnsNoEvidence/bookIds (0.00s)
FAIL
FAIL	github.com/vavallee/bindery/internal/metadata/hardcover	0.003s

EitherPredicateFailingReturnsNoEvidence guards the semantics rather than reproducing the bug, so it passes on main too (any failure there already returned no evidence).

With this PR:

=== RUN   TestGetAuthorWorkLanguageEvidence_BatchesAllowedEditionLookup
--- PASS: TestGetAuthorWorkLanguageEvidence_BatchesAllowedEditionLookup (0.00s)
=== RUN   TestGetAuthorWorkLanguageEvidence_SlugNamedWorksQueryBySlugAlone
--- PASS: TestGetAuthorWorkLanguageEvidence_SlugNamedWorksQueryBySlugAlone (0.00s)
=== RUN   TestGetAuthorWorkLanguageEvidence_NumericIDsQueriedSeparatelyAndMerged
--- PASS: TestGetAuthorWorkLanguageEvidence_NumericIDsQueriedSeparatelyAndMerged (0.00s)
=== RUN   TestGetAuthorWorkLanguageEvidence_EitherPredicateFailingReturnsNoEvidence
--- PASS: TestGetAuthorWorkLanguageEvidence_EitherPredicateFailingReturnsNoEvidence (0.00s)
    --- PASS: TestGetAuthorWorkLanguageEvidence_EitherPredicateFailingReturnsNoEvidence/slugs (0.00s)
    --- PASS: TestGetAuthorWorkLanguageEvidence_EitherPredicateFailingReturnsNoEvidence/bookIds (0.00s)
PASS
ok  	github.com/vavallee/bindery/internal/metadata/hardcover	0.004s

I also checked two mutations locally (not committed):

Mutation Tests that fail
The follow-up query left on the _or SlugNamedWorksQueryBySlugAlone, NumericIDsQueriedSeparatelyAndMerged
A failed query skipped instead of returned EitherPredicateFailingReturnsNoEvidence (both subtests), FailureReturnsNoEvidence

Live against Hardcover

I captured the exact query text this branch's client sends and posted it with a real token, using languageCodes: ["en", "eng"] (what LanguageCodeVariants(["eng"]) produces), at least 3 s apart. Works were resolved by author name, as in the issue. The known-languages input was every work the allowed-language query did not match. That is an upper bound on what Bindery sends, since it only asks about works with no default-edition language.

Query Oliver Burkeman (24 works) Joseph Campbell (99 works)
Allowed languages, by slug 200, 0.71 s, 9 rows 200, 0.73 s, 29 rows
Allowed languages, by book_id 200, 0.44 s, 9 rows 200, 0.70 s, 29 rows
Both merged by book id 9 rows, the same books both ways 29 rows, the same books both ways
Known languages, by slug 200, 0.48 s, 5 rows (15 works asked) 200, 1.03 s, 22 rows (70 works asked)
Known languages, by book_id 200, 0.53 s, 5 rows (15 works asked) 200, 0.56 s, 22 rows (70 works asked)
main: known languages with the _or 408 after 15.4 s not re-run
main: allowed languages with the _or (from the issue) 408 after 15.5 s 408 after 15.4 s

In the normal case, with no numeric slugs, only the "by slug" queries are sent.

Local checks

  • go test -p 2 ./cmd/... ./internal/... (no race): ok
  • go test -race ./internal/metadata/hardcover/: ok
  • go vet ./internal/metadata/...: clean. gofmt -l .: empty.
  • golangci-lint v2.11.4 run --concurrency 2 ./internal/metadata/...: 0 issues. I built and ran it with Go 1.26.9, the toolchain CI uses, because a v2.11.4 binary built with Go 1.27 can't read 1.27 export data.
  • The internal/metadata/... tests pass under both Go 1.26.9 and 1.27.1.

Checklist

  • Every commit carries a Signed-off-by that matches its author (git commit -s)
  • Changelog fragment added as changelog.d/3333-hardcover-language-evidence-timeout.md
  • Tests added or updated
  • docs/DEPLOYMENT.md updated if env vars, config, or upgrade path changed (not applicable: none changed)
  • Wiki pages under docs/ updated if user-facing behaviour changed (not applicable: the User Guide's "checks Hardcover's editions for each author work in one batched request" still holds for slug-named works)
  • No new dependency

Test plan

  • go test ./cmd/... ./internal/...
  • cd web && npm run build: not run, because web/ is untouched

🤖 Generated with Claude Code

…#3333)

The editions lookup behind GetAuthorWorkLanguageEvidence, and its
follow-up for blank-language works, joined the book.slug and book_id
predicates in one _or. Hardcover cannot serve that from an index: the
query ran until Hardcover cancelled it with HTTP 408 after about 15 s,
so every author with Hardcover works lost 45-70 s to retries and its
works fell back to the default-edition language. Either predicate on
its own answers in under a second with the same rows.

Send the slug predicate alone, declaring only the variables it uses,
and run the book_id predicate as a second query only when some works
are named by numeric id, merging the rows by book id. A failure of
either query still fails the lookup, so a partial answer never counts
as proof that a work has no allowed edition.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Uģis <berzinsu@gmail.com>
@github-actions github-actions Bot added the bindery-notified Discord notification already sent for this PR label Oct 18, 2026
@codecov

codecov Bot commented Oct 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@vavallee vavallee left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked on 1f34c29: both language evidence queries now avoid the slug/book_id OR, the merged results match the old ones, a failing query returns no partial evidence, and the tests reject the OR by behaviour. Thank you for the measurements in the issue, they made this quick to verify.

@vavallee
vavallee merged commit 27a808c into vavallee:main Oct 18, 2026
42 checks passed
@vavallee vavallee mentioned this pull request Oct 18, 2026
3 of 4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bindery-notified Discord notification already sent for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hardcover language-evidence query times out (HTTP 408) on every author with Hardcover works: the slug/book_id _or defeats the index

2 participants