{{ message }}
Repository navigation
feat: add a Data Consistency admin page (#1622) - #2086
Merged
Merged
Conversation
Extension.setActive() was routinely called as part of a full-entity merge/update. Hibernate generates a SQL UPDATE covering every mapped column, so a version-publish thread that read the entity before another thread flipped active could overwrite that flag back to its stale value once it saved - a classic lost update, not guarded by any transaction isolation level Postgres offers by default. Add @DynamicUpdate so Hibernate only includes changed columns in the UPDATE, closing the window for this specific race without introducing optimistic-locking retries/contention on a hot entity. Also add a native query to find extensions whose active flag disagrees with whether any of their versions is active, as a way to detect and repair rows that were already corrupted before this fix - used by the data-consistency check landing in the next commit.
Introduce a small, pluggable framework for detecting and repairing
known data inconsistencies, plus an admin UI for it:
- ConsistencyCheck: the extension point a check implements (id, name,
description, check(), fix(entityId)); autoFixOnSchedule() defaults
to true but lets a check opt out when a fix needs human judgment.
- ConsistencyCheckService: lists checks with their live finding count,
fetches findings for one check on demand, and fixes one or all
findings for a check. No persistence of past runs - findings are
always computed live against the current database state.
- ConsistencyCheckJobRequestHandler + ScheduleConsistencyCheckJobs: a
daily JobRunr job that runs every check and auto-fixes the ones that
allow it, so routine drift self-heals without an admin having to
notice and click a button.
- Every fix, whether run interactively by an admin or by the scheduled
job, is recorded via LogService under a system user
("ConsistencyCheckUser"), the same convention already used for
ExtensionControlService actions, so it shows up on the existing
Admin Logs page instead of a bespoke history view.
- ExtensionActiveFlagCheck: the first check, built on the
findExtensionsWithInconsistentActiveFlag() query added in the
previous commit, for extensions whose active flag disagrees with
whether any of their versions is active.
- ConsistencyAPI: GET /admin/consistency (summaries), GET
/admin/consistency/{checkId}/findings, POST
/admin/consistency/{checkId}/fix and .../fix/{entityId}.
- webui: a "Data Consistency" admin dashboard page showing one card per
check with its live finding count; expanding a card lazily loads its
findings with a "Fix" action per finding, and a "Fix all" action is
available directly on the card without expanding it. A "Refresh"
button re-fetches the check list on demand.
This was referenced Sep 18, 2026
netomi
added a commit
that referenced
this pull request
Sep 21, 2026
#2231) * fix: repair and stop re-corrupting extensions' stale last-updated date Extension.lastUpdatedDate - what sitemap.xml reports as `lastmod` - is exposed to the same cross-transaction lost-update race #2086 already fixed for the `active` flag: before @DynamicUpdate, any save of the entity emitted a full-row UPDATE using that transaction's in-memory snapshot, so a transaction that never touched lastUpdatedDate (a download-count bump, a review, mirror sync) could silently revert it to a stale value if it committed after a concurrent publish. A production query found 788 extensions affected, with lags from seconds to several years. Fixes #2229. Add LastUpdatedDateCheck, mirroring ExtensionActiveFlagCheck, so the existing daily Data Consistency job finds and repairs affected rows going forward - recomputing lastUpdatedDate from the extension's actual latest active version rather than stamping "now". Also fix a related gap found while investigating: ExtensionService#updateExtension, the method that stamps lastUpdatedDate, never evicted the sitemap cache, so even a correct value could take up to the cache TTL to surface. Move the eviction there from the narrower @CacheEvict that only covered the publish path in PublishExtensionVersionService#activateExtension. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test: exercise findExtensionsWithStaleLastUpdatedDate in the repository smoke test RepositoryServiceSmokeTest asserts every public RepositoryService method gets invoked; the new query needs to be in that list too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * test: fix LocalDateTime cast in LastUpdatedDateCheckTest The native query for last_updated_date returns java.time.LocalDateTime directly in this Hibernate version, not java.sql.Timestamp. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix: address PR review feedback on LastUpdatedDateCheck Copilot review found two real issues: - LastUpdatedDateCheck#fix's read/recompute/write sequence could race a concurrent publish: without a lock held across all three steps, a publish landing in between could have its newer, correct timestamp overwritten by the fixer's stale, pre-publish computation - recreating the exact corruption this check exists to repair. Lock the extension for the whole fix, the same way a real publish already does. Added a regression test that reproduces the interleaving and asserts the fixer waits instead of racing it. - ExtensionService#updateExtension's new evictSitemap() call had no regression test, so removing the old @CacheEvict could silently reintroduce the original bug unnoticed. Added a direct test for it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes #1622 and #1729 and #1733 .
What
Extension.activelost-update fix:@DynamicUpdateonExtensionsoHibernate only writes changed columns, closing a race where a full-entity
UPDATE from one thread could resurrect a stale
activeflag after anotherthread had legitimately flipped it. Also adds
findExtensionsWithInconsistentActiveFlag()to detect extensions alreadyleft in that state.
pluggable framework (
ConsistencyCheck) for this and future checks:ConsistencyCheckServicelists checks with a live finding count and canfix one finding or all findings for a check. Findings are always computed
live against the current DB state - nothing is persisted between runs.
need human judgment (
autoFixOnSchedule(), opt-out per check).LogServiceunder asystem user (
ConsistencyCheckUser), the same convention used byExtensionControlService, so it shows up on the existing Admin Logs page.ExtensionActiveFlagCheck./admin/consistency; a new "Data Consistency" pagein the admin dashboard with one card per check, a "Fix all" action
directly on the card, per-finding "Fix" once expanded, and a manual
"Refresh".
Testing
./gradlew test: 987 tests passing.yarn test(webui): 74 tests passing.yarn lint(webui): clean.🤖 Generated with Claude Code