Skip to content

Commit c54cedb

Browse files
authored
Reset MARC files on settings update (PP-4637) (#3554)
## Description - Adds a `marc_export_reset` Celery task that sets up the next MARC export as a first run by removing existing files (database records and S3 objects). The records are deleted in one transaction, and S3 deletion is best-effort, so a failure won't leave the reset half-applied. - Queues the reset when a library's MARC exporter settings change in the admin interface and when a library registry pushes a new web client URL (only if the change would result in a new web client URL). - First-run full exports now also record a full-content delta pointing at the same S3 object, so (1) consumers that only apply delta files receive the complete refreshed dataset without a second upload and (2) we avoid an extra upload and the doubled storage utilization. - Export finalization now detects a configuration change that raced an in-flight export (records reset, settings drift, or the library no longer MARC-enabled), discards the stale upload, and immediately re-queues a fresh full export. - Cleanup and reset only delete a shared S3 object when the last record referencing it is removed, and upload aborts are best-effort so cleanup failures cannot cascade. - The MARC download page labels the full-content delta as "Full content as of <date>" instead of a date range starting at the full delta sentinel epoch. ## Motivation and Context MARC record exports run periodically, but a configuration change would not trigger an update. Record updates based on those changes might not become available until long after the change was made. Further, some MARC consumers apply only delta files rather than reprocessing full exports. When a library's MARC export configuration changed (organization code, summary/genre inclusion, web client URLs), records exported before the change were never corrected for those consumers, leaving stale data in their catalogs indefinitely. Resetting the library's export state on configuration changes forces a full refresh that reaches both full-file and delta-only consumers. [Jira PP-4637] ## How Has This Been Tested? - New and updated tests for the new functionality. - All tests/checks pass locally. - [CI tests](https://github.com/ThePalaceProject/circulation/actions/runs/29181152088)/checks pass. ## Checklist - [x] I have updated the documentation accordingly. - [x] All new and existing tests passed.
1 parent 3a39802 commit c54cedb

13 files changed

Lines changed: 1170 additions & 40 deletions

File tree

src/palace/manager/api/admin/controller/catalog_services.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@
88
)
99
from palace.manager.api.admin.form_data import ProcessFormData
1010
from palace.manager.api.admin.problem_details import MULTIPLE_SERVICES_FOR_LIBRARY
11+
from palace.manager.celery.tasks.marc import (
12+
MARC_EXPORT_RESET_COOLDOWN_SECONDS,
13+
marc_export_reset,
14+
)
1115
from palace.manager.integration.catalog.marc.exporter import MarcExporter
16+
from palace.manager.integration.catalog.marc.settings import MarcExporterLibrarySettings
1217
from palace.manager.integration.goals import Goals
1318
from palace.manager.integration.settings import BaseSettings
1419
from palace.manager.sqlalchemy.listeners import site_configuration_has_changed
@@ -87,19 +92,50 @@ def process_post(self) -> Response | ProblemDetail:
8792
validated_settings = ProcessFormData.get_settings(settings_class, form_data)
8893
catalog_service.settings_dict = validated_settings.model_dump()
8994

95+
# Capture current library settings so we can detect changes after the
96+
# update.
97+
old_library_settings: dict[int, MarcExporterLibrarySettings] = {
98+
lc.library_id: impl_cls.library_settings_load(lc)
99+
for lc in catalog_service.library_configurations
100+
if lc.library_id is not None
101+
}
102+
90103
# Update library settings
91104
if libraries_data:
92105
self.process_libraries(
93106
catalog_service, libraries_data, impl_cls.library_settings_class()
94107
)
95108

109+
# Find libraries whose settings actually changed. A MARC export reset
110+
# is queued for each so that the next run produces a fresh full
111+
# export along with a full-content delta.
112+
reset_library_ids = [
113+
lc.library_id
114+
for lc in catalog_service.library_configurations
115+
if lc.library_id is not None
116+
and lc.library_id in old_library_settings
117+
and impl_cls.library_settings_load(lc)
118+
!= old_library_settings[lc.library_id]
119+
]
120+
96121
# Trigger a site configuration change
97122
site_configuration_has_changed(self._db)
98123

99124
except ProblemDetailException as e:
100125
self._db.rollback()
101126
return e.problem_detail
102127

128+
# Dispatch the resets before the request transaction commits: a failed
129+
# enqueue then fails the request and rolls the settings change back, so
130+
# a retried save re-queues the reset. The reverse order could commit the
131+
# settings and then lose a reset, leaving delta-only consumers with
132+
# stale records indefinitely. The task cooldown keeps a reset from
133+
# running before the transaction commits.
134+
for library_id in reset_library_ids:
135+
marc_export_reset.apply_async(
136+
(library_id,), countdown=MARC_EXPORT_RESET_COOLDOWN_SECONDS
137+
)
138+
103139
return Response(str(catalog_service.id), response_code)
104140

105141
def process_delete(self, service_id: int) -> Response:

src/palace/manager/api/controller/marc.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from sqlalchemy.orm import Session
1010

1111
from palace.manager.api.util.flask import get_request_library
12-
from palace.manager.integration.catalog.marc.exporter import MarcExporter
12+
from palace.manager.integration.catalog.marc.exporter import MARC_EPOCH, MarcExporter
1313
from palace.manager.service.integration_registry.catalog_services import (
1414
CatalogServicesRegistry,
1515
)
@@ -171,7 +171,12 @@ def download_page_body(self, session: Session, library: Library) -> str:
171171
body += "<ul>"
172172
for update in files.deltas:
173173
update_url = self.storage_service.generate_url(update.key)
174-
update_label = f"Updates from {update.since.strftime(time_format)} to {update.created.strftime(time_format)}"
174+
# Avoid misleading label for full-content delta.
175+
update_label = (
176+
f"Full content as of {update.created.strftime(time_format)}"
177+
if update.since == MARC_EPOCH
178+
else f"Updates from {update.since.strftime(time_format)} to {update.created.strftime(time_format)}"
179+
)
175180
body += f'<li><a href="{update_url}">{update_label}</a></li>'
176181
body += "</ul>"
177182

0 commit comments

Comments
 (0)