Skip to content

Retire the CoverageProvider machinery (PP-4468) - #3520

Merged
dbernstein merged 8 commits into
mainfrom
chore/retire-coverage-provider
Sep 11, 2026
Merged

dbernstein merged 8 commits into
mainfrom
chore/retire-coverage-provider

Conversation

@dbernstein

@dbernstein dbernstein commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Description

Retires the legacy CoverageProvider machinery. Overdrive bibliographic coverage now runs as a direct metadata-apply step inside OverdriveAPI.update_licensepool instead of routing through OverdriveBibliographicCoverageProvider.ensure_coverage. The base CoverageProvider classes, the Overdrive/Bibliotheca providers, the coverage-provider runner scripts and their bin/repair entry points, and the now-dead readers (Identifier.missing_coverage_from, the Explain script's coverage block) are deleted.

BibliographicData.apply no longer writes CoverageRecord rows (the create_coverage_record parameter is removed; the Celery apply task and the Bibliotheca updater already passed create_coverage_record=False).

The new _ensure_bibliographic_coverage helper wraps bibliographic.apply(...) in a BasePalaceException guard so bad or incomplete data for a single title is logged and skipped rather than aborting the batch the caller is working through. A failed metadata fetch (Overdrive unreachable, bad credentials) and non-Palace exceptions from apply (a KeyError, a SQLAlchemy error) are deliberately left to propagate: those affect the whole run and should stop it loudly instead of quietly leaving thousands of pools without Works. The docstring spells that split out.

The dead admin refresh_metadata endpoint is removed outright, along with the route, its tests, and the two now-unused METADATA_REFRESH_* problem details. It was unreachable by any client: circulation-admin only renders its "Refresh Metadata" button when the admin OPDS entry carries a http://librarysimplified.org/terms/rel/refresh link, and nothing has emitted that rel since the metadata wrangler was removed (#792); ios-core never touched the admin API. With no provider wired up, any request that did arrive got an unconditional 400.

The CoverageRecord model and coveragerecords table are intentionally left in place (marked dormant) for one release, per the online-migration / N-1 rule. The stacked follow-up #3521 drops the coveragerecords and dormant equivalentscoveragerecords tables.

Note: this PR supersedes #3503; it is opened from a branch on the upstream repository and rebased on the latest main.

Motivation and Context

JIRA (PP-4468)

The CoverageProvider batch-processing pattern is being retired in favor of Celery. Investigation showed nothing live reads coverage rows:

  • The only live caller of ensure_coverage (Overdrive's update_licensepool) passes force=True and ignores the result.
  • The admin refresh_metadata endpoint is wired with no provider and no client can reach it, so it always returned METADATA_REFRESH_FAILURE.
  • Identifier.missing_coverage_from / should_update / the Explain coverage block are only reachable through the already-dead runner scripts.

The Celery bibliographic_apply task and the Bibliotheca updater already skip coverage-record writes, so Overdrive was the last writer.

How Has This Been Tested?

  • mypy and the project's pre-commit hooks pass on all changed files.
  • Full-suite collection is clean (6136 tests, no dangling imports), and every bin/ script's imports were checked to resolve — bin/ is not imported by CI, which is how the two stale bin/repair coverage entry points survived the first pass.
  • Targeted suites pass under tox -e py312-docker (619 tests): Overdrive API including the rewritten update_licensepool bibliographic-coverage path and the tests covering the bibliographic.apply guard (Palace errors swallowed, non-Palace errors propagating), the admin work-editor and route tests, the Bibliotheca package, data-layer apply, admin feeds, scripts, and the affected model tests.

Checklist

  • I have updated the documentation accordingly.
  • All new and existing tests passed.

🤖 Generated with Claude Code

@claude

claude Bot commented Jun 29, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @dbernstein's task in 6m 3s —— View job


Summary

The deletion side of this is careful and well-verified — no dangling imports, bin/ entry points cleaned up, mocks and tests removed in step, and the new _ensure_bibliographic_coverage faithfully reproduces what OverdriveBibliographicCoverageProvider.process_item + handle_success did (same error codes, same _edition/apply/calculate_work/set_presentation_ready sequence), with good tests for both the swallow and the re-raise path. The narrowed except BasePalaceException and the propagating metadata_lookup are both defensible and now documented. My one substantive concern is the dormant-CoverageRecord plan: the model is still wired into Collection with a delete cascade, so this release does not actually stop using the table, which is what the stacked drop in #3521 depends on.

Details

src/palace/manager/sqlalchemy/model/coverage.py:365-371

The docstring's claim that "nothing in the current code reads or writes this table" isn't true while Collection.coverage_records is still mapped with cascade="all". Collection.delete() finishes with _db.delete(self), so SQLAlchemy loads that relationship (SELECT ... FROM coveragerecords WHERE collection_id = ...) and emits DELETEs for the children — which is exactly what TestCollection.test_delete asserts today. Once #3521 drops the table, the collection_delete Celery task running on N-1 (i.e. this release's) workers will fail with UndefinedTable, which is the failure mode CLAUDE.md's "stop using means stopping both reads and writes" rule exists to prevent. Dropping the relationship alone isn't sufficient either: rows still exist and coveragerecords.collection_id has no ON DELETE action, so a release-1 migration needs to hand cleanup to the database (ON DELETE CASCADE on the coveragerecords FKs) before the Collection/Identifier/DataSource relationships come off. Worth confirming with ./docker/ci/test_backwards_compatibility.sh <this commit> checked out against #3521 before that one merges.

Dormant model retained only so the ``coveragerecords`` table stays in the
schema for one more release. The CoverageProvider machinery that read and
wrote these records has been retired; nothing in the current code reads or
writes this table. Per our online-migration convention the table cannot be
dropped in the same release that stops using it (N-1 app servers still write
here during a rolling deploy), so this model and the table will be removed in
a follow-up PR that ships after this release.

Minor: src/palace/manager/integration/license/overdrive/api.py:1340-1343

When calculate_work() returns None the method returns silently, so a pool that ends up with no Work leaves no trace at warning level — the old path logged this as a transient failure ("Work could not be calculated"). This is reachable for the untitled-edition case, where calculate_work only logs at info. Since every other skip in this method logs a warning explaining why the pool has no Work, an else here would keep the diagnostics consistent:

work, _ = license_pool.calculate_work()
if work:
    work.set_presentation_ready()
else:
    self.log.warning(
        "Could not calculate a Work for %s after applying Overdrive bibliographic data.",
        identifier.identifier,
    )

if not license_pool.work or not license_pool.work.presentation_ready:
work, _ = license_pool.calculate_work()
if work:
work.set_presentation_ready()

--- · chore/retire-coverage-provider

@dbernstein
dbernstein force-pushed the chore/retire-coverage-provider branch from 8f4574b to 27bf685 Compare June 30, 2026 00:01
@dbernstein
dbernstein requested a review from a team June 30, 2026 00:02
@greptile-apps

greptile-apps Bot commented Jun 30, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR retires the legacy CoverageProvider batch-processing machinery (~4,500 lines deleted) in favour of Celery, replacing Overdrive's bibliographic coverage path with a new inline _ensure_bibliographic_coverage helper on OverdriveAPI. The dead refresh_metadata admin endpoint and its route are also removed outright; the CoverageRecord model and table are intentionally kept dormant for one release to satisfy the N-1 online-migration rule.

  • New _ensure_bibliographic_coverage: fetches Overdrive metadata, extracts BibliographicData, applies it to the edition, and ensures the pool has a presentation-ready Work. BasePalaceException from apply is caught and logged so one bad title cannot abort a batch; network failures and non-Palace exceptions from apply propagate deliberately, as documented in the method's docstring.
  • BibliographicData.apply simplified: the create_coverage_record parameter is removed; the method no longer writes CoverageRecord rows. All callers already passed False or are updated.
  • Admin cleanup: refresh_metadata endpoint, its route, the two METADATA_REFRESH_* problem details, and all related tests are consistently removed.

Confidence Score: 5/5

  • Safe to merge. The change is a well-scoped deletion of verified-dead code plus a small, well-tested replacement for Overdrive bibliographic coverage.
  • The deleted code was confirmed dead before removal (no live callers, coverage rows not read by anything), the new _ensure_bibliographic_coverage helper has targeted tests for both the catch and re-raise paths, and the exception-propagation design is explicitly documented. No new logic was added elsewhere in the codebase.
  • No files require special attention.

Important Files Changed

Filename Overview
src/palace/manager/integration/license/overdrive/api.py Core change: removes OverdriveBibliographicCoverageProvider dependency and introduces _ensure_bibliographic_coverage. New method is well-guarded — handles Overdrive errorCode responses, missing bibliographic data, and BasePalaceException from apply, with the remaining propagation behavior clearly documented. Tests cover both the catch and re-raise paths.
src/palace/manager/data_layer/bibliographic.py Removes the create_coverage_record parameter and the CoverageRecord.add_for call inside apply. Clean removal — callers already passed False or are updated; the corresponding test is also updated.
src/palace/manager/core/coverage.py Entire file deleted (1,364 lines). Verified that no remaining production code imports from this module.
src/palace/manager/sqlalchemy/model/coverage.py CoverageRecord model retained as dormant with a clear docstring explaining the N-1 migration convention. Timestamp model unchanged and still actively used by Monitors.
src/palace/manager/api/admin/controller/work_editor.py refresh_metadata method fully removed. The route, its tests, and the two METADATA_REFRESH_* problem details are all consistently cleaned up across routes.py and problem_details.py.
tests/manager/integration/license/overdrive/test_api.py Adds two targeted tests for _ensure_bibliographic_coverage: one confirms BasePalaceException is caught and logged without propagating; the other confirms non-Palace exceptions (e.g. KeyError) re-raise. The existing test_update_licensepool is updated to remove the now-defunct coverage-record assertion.
tests/manager/data_layer/test_bibliographic.py Removes test_apply_creates_coverage_records. Updates test_apply_does_not_create_coverage_records to call apply without the now-removed create_coverage_record=False kwarg — test body is still correct, but the name is now misleading since there's no longer an alternative behavior to contrast it against.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant OverdriveAPI
    participant Overdrive as Overdrive API
    participant BibliographicData
    participant LicensePool

    Caller->>OverdriveAPI: update_licensepool(book_id)
    OverdriveAPI->>Overdrive: get availability
    Overdrive-->>OverdriveAPI: availability JSON
    OverdriveAPI->>LicensePool: for_foreign_id()
    LicensePool-->>OverdriveAPI: license_pool, is_new

    alt is_new OR no work
        OverdriveAPI->>OverdriveAPI: _ensure_bibliographic_coverage(pool)
        OverdriveAPI->>Overdrive: metadata_lookup(identifier)
        Overdrive-->>OverdriveAPI: metadata JSON

        alt "errorCode = NotFound / InvalidGuid"
            OverdriveAPI-->>OverdriveAPI: log warning, return
        else metadata valid
            OverdriveAPI->>BibliographicData: book_info_to_bibliographic(info)
            BibliographicData-->>OverdriveAPI: bibliographic (or None)

            alt bibliographic is None
                OverdriveAPI-->>OverdriveAPI: log warning, return
            else
                OverdriveAPI->>BibliographicData: apply(db, edition, collection)
                alt BasePalaceException
                    BibliographicData-->>OverdriveAPI: raises
                    OverdriveAPI-->>OverdriveAPI: log warning, return
                else success
                    BibliographicData-->>OverdriveAPI: (edition, changed)
                    OverdriveAPI->>LicensePool: calculate_work()
                    LicensePool-->>OverdriveAPI: work
                    OverdriveAPI->>LicensePool: work.set_presentation_ready()
                end
            end
        end
    end

    OverdriveAPI->>OverdriveAPI: update_licensepool_with_book_info(...)
    OverdriveAPI-->>Caller: (pool, is_new, changed)
Loading

Reviews (5): Last reviewed commit: "Say exactly what the bibliographic apply..." | Re-trigger Greptile

Comment on lines +1832 to +1851
try:
bibliographic.apply(
self._db,
edition,
self.collection,
replace=ReplacementPolicy.from_license_source(),
)
except Exception as e:
# Mirror the resilience of the former
# ``BibliographicCoverageProvider.set_bibliographic``: a database
# error or unexpected data shape during apply should not crash
# ``update_licensepool``. Log and leave the pool without updated
# coverage, matching the prior "silently continue" behavior.
self.log.warning(
"Error applying Overdrive bibliographic data to edition %s: %s",
edition.id,
e,
exc_info=e,
)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 metadata_lookup exceptions propagate outside the guard

The except Exception block covers only bibliographic.apply(). A transient HTTP error or malformed JSON response from metadata_lookup() (lines 1812–1813) will propagate to update_licensepool and abort the entire availability update, preventing update_licensepool_with_book_info from running. This matches the old behaviour — the retired process_batch also did not wrap process_item in a try/except — so this is not a regression, but it is worth calling out explicitly because the guard communicates an intent ("bibliographic failures must not crash availability updates") that isn't fully satisfied if the fetch itself throws. Consider wrapping metadata_lookup in the same guard, or adding a dedicated try/except around just that call with a return on failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Leaving metadata_lookup outside the guard on purpose, and the docstring now says so instead of implying blanket resilience.

The asymmetry is the point. A failed apply is usually about one title's data. A failed fetch usually isn't: Overdrive is unreachable, or the credentials are wrong, or the collection token is stale — conditions that will hit every identifier in the batch. Callers run this in a loop over batches of identifiers, so swallowing a fetch failure would walk the whole batch logging warnings and finish looking like a successful run, having left every pool without a Work. Letting it propagate fails the task, which is the honest outcome and the one that gets retried.

So the guard's intent isn't "bibliographic failures must not crash availability updates" — it's narrower than that, and the docstring is now explicit about which failures are skipped and which are allowed to stop the run.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's a well-reasoned split — the asymmetry makes sense. Per-title apply failures are ignorable noise; a fetch failure is a signal about the whole run that should surface loudly rather than be buried in warnings. If the updated docstring now explicitly documents which failures are skipped vs. allowed to propagate, the intent is clear to future readers. No further change needed.

Comment thread src/palace/manager/api/admin/controller/work_editor.py Outdated
@jonathangreen

Copy link
Copy Markdown
Contributor

@dbernstein it looks like tests are failing on this one, and greptile has a valid review comment about unconditionally calling self.load_work.

I'll let you resolve those issues before reviewing this one. When its ready you can request a review from @ThePalaceProject/backend again.

@jonathangreen jonathangreen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some comments on this for consideration while you are resolving the test issues

Comment on lines +453 to +456
Metadata refresh used to run through the per-source CoverageProvider
machinery, which has been retired. No provider is wired up, so this
endpoint is retained for API compatibility but no longer performs a
refresh; it always reports failure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this does nothing can we deprecate it, or just remove it entirely? What calls this endpoint? I think we need a plan to deal with this endpoint since its no longer functional.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

There a 👍🏻 on this, but do we have a ticket or plan for it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No ticket needed — I checked what calls it, and the answer is nothing, so it's removed outright in this PR rather than deprecated.

circulation-admin only renders its "Refresh Metadata" button when the admin OPDS entry carries a http://librarysimplified.org/terms/rel/refresh link (editorAdapter.ts looks for that rel, and BookDetailsEditor gates the button on bookData.refreshLink). Nothing in the manager has emitted that rel since the metadata wrangler came out in #792 — the current admin annotators emit only suppress / unsuppress / edit / search. ios-core has no reference to the rel or the endpoint, and doesn't use the admin API at all.

So the button never appears and the route was unreachable; anything that did reach it got an unconditional 400. Removed the route, the controller method, its tests, and the two orphaned METADATA_REFRESH_* problem details. Also dropped test_feed_includes_refresh_link, which despite the name only asserted the absence of that rel — a leftover from the wrangler removal that no longer has an endpoint behind it.

Tagged the PR incompatible changes since it drops a registered admin route, though in practice no client could reach it.

Comment thread src/palace/manager/integration/license/overdrive/api.py Outdated
Comment thread src/palace/manager/integration/license/overdrive/api.py Outdated
Comment thread src/palace/manager/integration/license/overdrive/api.py Outdated
Comment thread tests/manager/integration/license/overdrive/test_api.py Outdated
@dbernstein
dbernstein force-pushed the chore/retire-coverage-provider branch from 27bf685 to ac18fb5 Compare July 1, 2026 18:08
@codecov

codecov Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 82.60870% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.69%. Comparing base (a55b97e) to head (601d261).
⚠️ Report is 6 commits behind head on main.

Files with missing lines Patch % Lines
...alace/manager/integration/license/overdrive/api.py 81.81% 3 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3520      +/-   ##
==========================================
+ Coverage   93.61%   93.69%   +0.07%     
==========================================
  Files         514      510       -4     
  Lines       47065    46398     -667     
  Branches     6408     6301     -107     
==========================================
- Hits        44060    43471     -589     
+ Misses       1944     1891      -53     
+ Partials     1061     1036      -25     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@dbernstein
dbernstein requested a review from jonathangreen July 13, 2026 19:53
@dbernstein
dbernstein force-pushed the chore/retire-coverage-provider branch from e5b1dc0 to c7d813d Compare July 14, 2026 17:06
Comment on lines +1832 to +1849
try:
bibliographic.apply(
self._db,
edition,
self.collection,
replace=ReplacementPolicy.from_license_source(),
)
except BasePalaceException as e:
# A failure while applying the bibliographic data must not abort the
# surrounding availability update, so log it and leave the pool's
# presentation edition unchanged.
self.log.warning(
"Error applying Overdrive bibliographic data to edition %s: %s",
edition.id,
e,
exc_info=e,
)
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Exception guard scope excludes SQLAlchemy errors from apply

The except BasePalaceException block only catches Palace-domain exceptions. BibliographicData.apply performs multiple SQLAlchemy writes (contributors, links, identifiers, equivalencies) that can raise IntegrityError, OperationalError, or DataError — none of which inherit from BasePalaceException. When those slip through, the exception propagates out of _ensure_bibliographic_coverage and into update_licensepool, where there is no further guard, so the outer availability update is aborted for that title.

The old BibliographicCoverageProvider.set_bibliographic wrapped bibliographic.apply in a bare except Exception, which is what the PR description ("broad except Exception") implies was the intent. The two new tests only cover BasePalaceException / KeyError side-effects from a mock, so SQLAlchemy exceptions are untested here.

If the intentional design is to let non-Palace exceptions surface as bugs, the docstring's claim that "a database error … does not propagate out of update_licensepool" should be tightened to match. If the intent truly is to swallow all apply-time errors, the guard should be widened to except Exception.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Took the first option: kept the narrow guard and tightened the docs to match.

Widening back to except Exception isn't available — that's what @jonathangreen pushed back on above ("they catch and mask so much, like this would catch and hide a key name typo in the code"), and he's right. So the split is now deliberate and spelled out in the docstring:

  • BasePalaceException from apply means bad or incomplete data for this one title. Logged, skipped, batch continues.
  • A SQLAlchemy error means a broken session or a genuine data-integrity problem, and a KeyError means a bug. Both propagate, because neither is specific to one title and both should stop the run loudly rather than quietly leaving thousands of pools without Works.

Good catch on the PR description — it still claimed a "broad except Exception" from an earlier revision. Corrected.

@dbernstein
dbernstein marked this pull request as draft July 14, 2026 21:31

@jonathangreen jonathangreen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks like there are some valid AI code review comments that still should be resolved on this one, so I'm going to leave it until you get back from vacation @dbernstein

@dbernstein dbernstein added incompatible changes Changes that require a new major version cleanup migration PR that will need a cleanup migration once its been fully deployed labels Aug 21, 2026
@dbernstein
dbernstein force-pushed the chore/retire-coverage-provider branch from c7d813d to 593e380 Compare August 24, 2026 14:15
@dbernstein
dbernstein force-pushed the chore/retire-coverage-provider branch from 593e380 to 27b613c Compare September 9, 2026 20:05
@dbernstein
dbernstein marked this pull request as ready for review September 9, 2026 20:13

@jonathangreen jonathangreen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks good

dbernstein and others added 7 commits September 11, 2026 20:11
Apply Overdrive bibliographic metadata directly instead of routing it through
the CoverageProvider machinery, then delete the now-dead machinery.

Nothing read the coverage rows: the only live caller of ensure_coverage was
OverdriveAPI.update_licensepool (force=True, return value ignored), and the
admin refresh_metadata endpoint was wired with no provider. The Celery apply
task and the Bibliotheca updater already skipped coverage-record writes, so
Overdrive was the last writer.

- Rewrite OverdriveAPI.update_licensepool to fetch + apply metadata and make
  the work presentation-ready via a new _ensure_bibliographic_coverage helper,
  replacing OverdriveBibliographicCoverageProvider.ensure_coverage.
- Stop writing coverage records: drop create_coverage_record from
  BibliographicData.apply and its call sites.
- Delete core/coverage.py, the Overdrive and Bibliotheca coverage providers,
  the coverage-provider runner scripts, Identifier.missing_coverage_from, and
  the Explain script's coverage block.
- Neutralize the now-dead admin refresh_metadata endpoint.
- Leave the CoverageRecord model and coveragerecords table in place (dormant)
  for one release; the drop migration follows in a stacked PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restore the resilience of the retired BibliographicCoverageProvider:
catch and log exceptions from bibliographic.apply in
_ensure_bibliographic_coverage so a database error or unexpected data
shape during apply does not propagate out of update_licensepool.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback: the _ensure_bibliographic_coverage docstring, its
exception-handling comment, and the matching test comment described the retired
CoverageProvider classes instead of what the code does now. Rewrite them to
explain the current behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address review feedback: a bare `except Exception` around bibliographic.apply
masks programming errors (a mistyped attribute raising AttributeError/KeyError
would be silently swallowed). Catch BasePalaceException instead -- apply raises
PalaceValueError for invalid data -- so genuine bugs propagate while a single
bad title still does not abort the availability update. Update the guard test
to raise PalaceValueError and add a test that an unexpected error propagates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_ensure_bibliographic_coverage_apply_error asserted on caplog.text without
setting a capture level, so it relied on the ambient root log level. Under
pytest-randomly, a prior test that raises that level leaves the warning
uncaptured, so caplog.text is empty and the assertion fails. Set the level
explicitly with caplog.set_level(LogLevel.warning), matching the convention used
by other caplog tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The endpoint could not be reached by any client. circulation-admin only
renders its "Refresh Metadata" button when the admin OPDS entry carries a
`http://librarysimplified.org/terms/rel/refresh` link, and nothing has
emitted that rel since the metadata wrangler was removed; ios-core never
used the admin API at all. With no provider wired up, any request that did
arrive got an unconditional 400.

Drop the route, the controller method, its tests, and the two now-unused
METADATA_REFRESH_* problem details. Also drop the misnamed
test_feed_includes_refresh_link, which only asserted the absence of a rel
that no longer has an endpoint behind it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The guard catches BasePalaceException, but the comment promised that no
failure would abort the surrounding availability update, which is not what
the code does. Describe the actual split instead: bad data for one title is
logged and skipped so the batch continues, while a failed metadata fetch
(Overdrive down, bad credentials) and non-Palace exceptions from apply (a
KeyError, a SQLAlchemy error) propagate, because those affect the whole run
and should stop it loudly.

Also drop the last CoverageProviders mention from Script.update_timestamp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rebase fixups for changes that landed on main after this branch was cut:

- metadata_lookup now returns a MetadataResponse model (#3705) rather
  than a raw dict, so the two _ensure_bibliographic_coverage tests mock
  it with a model instead of a dict.
- test_coverage.py, deleted here, had gained a test for the case where
  Overdrive returns a 200 with a document the extractor cannot use.
  That case and the unrecognized-ID case are now covered against
  _ensure_bibliographic_coverage in test_api.py, so retiring the
  provider does not take their coverage with it.
- Drop the now-dead palace.manager.core.coverage mypy override.
@dbernstein
dbernstein force-pushed the chore/retire-coverage-provider branch from 27b613c to 601d261 Compare September 11, 2026 18:11
@dbernstein
dbernstein enabled auto-merge (squash) September 11, 2026 18:11
@dbernstein
dbernstein merged commit 7a3274e into main Sep 11, 2026
23 of 24 checks passed
@dbernstein
dbernstein deleted the chore/retire-coverage-provider branch September 11, 2026 18:19
dbernstein added a commit that referenced this pull request Sep 28, 2026
## Description

Release-1 half of retiring the `coveragerecords` table, split out of
#3521.

#3520 retired the CoverageProvider machinery and left the
`CoverageRecord` model "dormant" for a release. It wasn't dormant:
`Identifier`, `DataSource` and `Collection` still mapped a
`coverage_records` relationship, and SQLAlchemy loads a relationship
whenever its parent is deleted — to cascade the delete
(`mapper.cascade_iterator`) or to null the child's foreign key
(`dependency.presort_deletes`). So every `session.delete()` on one of
those parents still SELECTed from `coveragerecords`.

This PR actually stops using the table:

- Removes the `coverage_records` relationships from `Identifier`,
`DataSource` and `Collection`, and the `identifier` / `data_source` /
`collection` relationships that pointed back at them from
`CoverageRecord`.
- Reduces `CoverageRecord` to a bare table definition — columns,
`__table_args__` and the trailing module-level `Index`. `lookup`,
`add_for`, `bulk_add`, `assert_coverage_operation`, `human_readable` and
`__repr__` are gone, along with the five `*_OPERATION` constants that
only fed them, and `BaseCoverageRecord.not_covered` with the
`ALL_STATUSES` / `PREVIOUSLY_ATTEMPTED` / `DEFAULT_COUNT_AS_COVERED`
lists that only fed *it*. Nothing called any of them, and they had
become a hazard rather than merely dead: with the parent relationships
gone, a row written by `add_for`/`bulk_add` could no longer be cleaned
up when its parent is deleted, so it would make that delete fail on a
foreign key. `status_enum` and the four status constants stay — they are
the column type for both dormant models.
- Empties `coveragerecords` in a migration. Its foreign keys carry no
`ON DELETE` clause, so with the relationships gone SQLAlchemy no longer
cleans up children by hand and a surviving row would make deleting its
parent fail with a foreign-key violation. The rows are dead data, so
`TRUNCATE` is cheaper than teaching a table we drop next release to
cascade. It runs under `SET LOCAL lock_timeout = '5s'`: N-1 servers
still read the table, so `TRUNCATE`'s `ACCESS EXCLUSIVE` lock can queue
behind an in-flight parent delete and block every later reader — failing
fast and retrying beats stalling instance startup.
- Removes the `CoverageRecord` tests and the `db.coverage_record`
fixture, so the next release's backwards-compatibility gate doesn't run
them against a schema where the table is gone.

The `CoverageRecord` and `EquivalencyCoverageRecord` **models stay** for
one more release. A fresh database's schema is built with `create_all`
from the models rather than by replaying migrations
(`InstanceInitializationScript.initialize_database_schema`), so deleting
the classes now would drop the table out from under N-1 servers on new
installs. The models and the tables go together in the stacked
follow-up, #3521.

`equivalentscoveragerecords` needs no change here — it has no ORM
relationships pointing at it and its one foreign key already declares
`ON DELETE CASCADE`.

## Motivation and Context

JIRA (PP-4653)

This is the missing middle step in the CoverageProvider retirement. The
original plan was #3520 (stop using) → #3521 (drop), but #3520 did not
stop the ORM-level reads, so the drop in #3521 would have broken N-1
webservers on any collection or identifier delete during a rolling
deploy.

Splitting at the relationship rather than at the model is what makes the
sequence work: release 1 (this PR) removes the reads but keeps the table
in fresh schemas; release 2 (#3521) removes the model and the table
together.

## How Has This Been Tested?

- `mypy` clean; `pre-commit` clean.
- `tests/migration/`, `tests/manager/sqlalchemy/` and
`tests/manager/data_layer/` pass under `tox -e py312-docker` (664
tests), including a new migration test covering the truncate and
confirming the table itself survives this release.
- **Backwards compatibility verified locally**, which is the point of
the PR:
  ```
./docker/ci/test_backwards_compatibility.sh
7a3274e # the #3520 release
→ 3346 passed — "The previous release works against the current schema
🎉"
  ```
For contrast, the same gate run against the old single-PR #3521 failed
with **30 tests** erroring on `relation "coveragerecords" does not
exist`, 24 of them purely from the relationships being walked on parent
deletes.
- The stacked #3521 passes the same gate against this branch's content
(3391 passed). It needs a rebase onto the current head of this branch
before that result applies to the pushed commits.

## Checklist

- [x] I have updated the documentation accordingly.
- [x] All new and existing tests passed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cleanup migration PR that will need a cleanup migration once its been fully deployed incompatible changes Changes that require a new major version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants