feat: add a Data Consistency admin page (#1622) by netomi · Pull Request #2086 · eclipse-openvsx/openvsx · GitHub
Skip to content

feat: add a Data Consistency admin page (#1622) - #2086

Merged
netomi merged 2 commits into
mainfrom
admin-data-consistency
Aug 21, 2026
Merged

netomi merged 2 commits into
mainfrom
admin-data-consistency

Conversation

@netomi

@netomi netomi commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Closes #1622 and #1729 and #1733 .

What

  • Extension.active lost-update fix: @DynamicUpdate on Extension so
    Hibernate only writes changed columns, closing a race where a full-entity
    UPDATE from one thread could resurrect a stale active flag after another
    thread had legitimately flipped it. Also adds
    findExtensionsWithInconsistentActiveFlag() to detect extensions already
    left in that state.
  • Data Consistency admin page, built on top of that fix as a general,
    pluggable framework (ConsistencyCheck) for this and future checks:
    • ConsistencyCheckService lists checks with a live finding count and can
      fix one finding or all findings for a check. Findings are always computed
      live against the current DB state - nothing is persisted between runs.
    • A daily JobRunr job runs every check and auto-fixes the ones that don't
      need human judgment (autoFixOnSchedule(), opt-out per check).
    • Every fix - scheduled or manual - is logged via LogService under a
      system user (ConsistencyCheckUser), the same convention used by
      ExtensionControlService, so it shows up on the existing Admin Logs page.
    • First check: ExtensionActiveFlagCheck.
    • New endpoints under /admin/consistency; a new "Data Consistency" page
      in 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

netomi added 2 commits August 21, 2026 23:57
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.
@netomi
netomi merged commit a77ab0c into main Aug 21, 2026
5 checks passed
@netomi
netomi deleted the admin-data-consistency branch August 21, 2026 22:12
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>
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.

Need a way to check the database for consistency

1 participant