Skip to content

fix(import): skip on content-hash equality alone, not version ordering - #2325

Merged
rubenvdlinde merged 1 commit into
developmentfrom
fix/import-version-gate-digest-equality
Aug 4, 2026
Merged

fix(import): skip on content-hash equality alone, not version ordering#2325
rubenvdlinde merged 1 commit into
developmentfrom
fix/import-version-gate-digest-equality

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

computeDefinitionHash() already covers the fully-merged configuration — monolith plus every register.d fragment — and is computed unconditionally before the gate is consulted (ImportHandler ~1972). So an identical hash means importing would write exactly what is already stored.

The gate additionally required version_compare($version, $stored, '<='). Three apps (opencatalogi, procest, softwarecatalog) append a digest to that version per ADR-037 — 1.2.3+frag.a1b2c3d4 — so that a fragment change forces a re-import. But version_compare does not treat +… as semver build metadata; it compares it as further version parts, lexically:

incoming 1.0.0+frag.abc12345  stored 1.0.0+frag.def67890  ->  re-import
the same pair, reversed                                   ->  skip

So the decision hinged on how two md5 hashes happened to sort. Unchanged content re-imported about half the time; a genuine change was skipped the other half, caught only by the content-differs fallback. A digest has no order.

Correctness is unchanged. A changed fragment changes the merged data → changes the hash → fails this equality → proceeds to the per-entity gates exactly as before (#426). A never-stored hash also fails it, so older installs heal on their next run.

Measured: 4 apps now skip on unchanged content that previously re-imported.

This is not sufficient on its own

When an import IS needed it remains pathologically slow. OpenCatalogi's payload is 1 register, 10 schemas, 12 objects, 59 KB — and it runs 12+ minutes at 100% CPU, ~950 MB RSS, zero database queries. Sampled from the log mid-import:

87  [MagicTableHandler] Creating/updating table for register+schema
70  DocuDesk: Processing event
53  [ObjectChangeListener] Background mode - queueing extraction job
52  SoftwareCatalog: Processing event
52  ModuleComplianceSubscriber: SUBSCRIBER CALLED

Seeding config objects dispatches the object lifecycle, so every installed app's listeners run — document extraction and compliance checks for objects that are merely being seeded.

importFromApp() already wraps its work in SystemOperationContext::run(), and SystemOperationContext::isActive() is public. No listener in any app checks it. That is the lever for the follow-up.

Companion PRs dropping the now-pointless digest suffix: opencatalogi and procest fix/drop-frag-digest-from-version.

computeDefinitionHash() already covers the fully-merged configuration —
monolith plus every register.d fragment — and is computed UNCONDITIONALLY before
the gate is consulted. So an identical hash means importing would write exactly
what is stored, and there is nothing to do whatever the version says.

The gate additionally required version_compare($version, $stored, '<='). Three
apps (opencatalogi, procest, softwarecatalog) append a digest to that version
per ADR-037 — `1.2.3+frag.a1b2c3d4` — so a fragment change would force a
re-import. But version_compare does not treat `+…` as semver build metadata; it
compares it as further version parts, LEXICALLY. The gate therefore fired based
on how two md5 hashes happened to sort:

  incoming 1.0.0+frag.abc12345 vs stored 1.0.0+frag.def67890 -> re-import
  the same pair reversed                                      -> skip

Unchanged content re-imported roughly half the time. A digest has no order, so
version_compare was the wrong instrument for it.

Correctness is unchanged: a changed fragment changes the merged data, changes
the hash, fails this equality, and proceeds to the per-entity gates exactly as
before (#426). A never-stored hash also fails it, so older installs heal on
their next run.

Measured on the dev instance: 4 apps now skip on unchanged content that
previously re-imported.

NOT sufficient on its own. When an import IS needed it remains pathologically
slow, because seeding config objects dispatches the object lifecycle and every
installed app's listeners run — 100% CPU, ~950 MB RSS, zero queries. That is a
separate fix and `SystemOperationContext::isActive()` is the unused lever for it.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 6520452

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 173/173
npm ✅ 713/713
PHPUnit
Newman
Playwright

Quality workflow — 2026-08-04 07:38 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit d0a5647 into development Aug 4, 2026
30 checks passed
@rubenvdlinde
rubenvdlinde deleted the fix/import-version-gate-digest-equality branch August 4, 2026 08:11
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.

1 participant