Skip to content

v2.2.0 — target architecture + cooldown + publish-date registry + serve-before-commit#34

Merged
aydasraf merged 435 commits into
masterfrom
2.2.0
Jul 3, 2026
Merged

v2.2.0 — target architecture + cooldown + publish-date registry + serve-before-commit#34
aydasraf merged 435 commits into
masterfrom
2.2.0

Conversation

@aydasraf

@aydasraf aydasraf commented Apr 16, 2026

Copy link
Copy Markdown
Collaborator

v2.2.0 — target-architecture train + cooldown metadata filtering + publish-date registry + serve-before-commit

Summary

This PR lands the v2.2 target architecture (9 work items), cooldown metadata filtering across all 7 adapters, publish-date registry replacing per-adapter cooldown inspectors, serve-before-commit performance pattern across all proxy adapters, search & browse overhaul, group RaceSlice priority fix, ECS log field compliance, and preemptive Basic auth for Maven/Gradle.

Highlights

  • Serve-before-commit: writeAndVerify() returns a VerifiedArtifact after streaming upstream to a verified temp file — HTTP response streams immediately while commitAsync() persists to cache in the background. Wired into Maven, Go, Composer, PyPI.
  • Parallel sidecar saves: CompletableFuture.allOf() replaces sequential thenCompose chain for checksum sidecar writes.
  • Preemptive Basic auth: Eliminates 401 challenge-response round-trip on every Maven/Gradle artifact fetch.
  • Publish-date registry: DbPublishDateRegistry (Caffeine L1 + Postgres L2 + 6 upstream source implementations) replaces all per-adapter CooldownInspector classes. RegistryBackedInspector is the single inspector.
  • Search & Browse overhaul: Natural name sort, artifact classification (excludes metadata/checksum noise), DB-hydrated tree browser with sort-by-size/date/name, upload-date display.
  • Group RaceSlice priority: 2xx > 403 > 5xx > 404 outcome selection; upstream 4xx propagated as 404 for correct fallback; 502 not 503 for upstream failures.
  • ECS log field compliance: 488 non-compliant fields migrated across all modules.
  • ComposerGroupSlice fix: providers field type guard prevents ClassCastException when Packagist returns [] instead of {}.

Target architecture work items (9)

  • WI-00 — queue overflow + access-log level policy
  • WI-01 — Fault + Result sum types
  • WI-02 — RequestContext + Deadline + ContextualExecutor
  • WI-03 — StructuredLogger 5-tier + LevelPolicy + AuditAction
  • WI-04 — GroupResolver replaces GroupSlice at every production site
  • WI-05 — SingleFlight coalescer
  • WI-07 — ProxyCacheWriter + Maven checksum integrity
  • WI-post-05 — retire RequestDeduplicator
  • WI-post-07 — wire ProxyCacheWriter into pypi/go/composer

Cooldown metadata filtering (8 phases)

Two-layer enforcement (soft metadata filter + hard 403) for Maven, npm, PyPI, Docker, Go, Composer, Gradle with 5 performance hardenings and SOLID package restructure.

Additional features

  • Circuit breaker: rate-over-sliding-window replaces consecutive-count
  • Auth cache L1/L2 + cluster-wide invalidation
  • GroupMetadataCache stale 2-tier (aid-not-breaker)
  • Cooldown history/retention/fallback/admin UI
  • Auth-settings server-side enforcement
  • UI: searchable group member picker, inline repo creation
  • HTTP/3 optional PROXY protocol v2

Test plan

  • mvn -T8 test — all modules green (4900+ tests, 0 failures)
  • Docker smoke test: docker compose up -d && curl -u ayd:ayd http://localhost:8081/maven_group/...
  • Maven/Gradle preemptive auth: verify no 401s in access log
  • PHP group: curl -u ayd:ayd http://localhost:8081/php_group/packages.json returns 200
  • Search: verify artifact classification excludes metadata noise
  • Cooldown: verify publish-date registry resolves dates from upstream registries
  • git log --format='%B' | grep -c '^Co-Authored-By:' returns 0

aydasraf added a commit that referenced this pull request Apr 16, 2026
…I-02, WI-03)

Refreshes the three release artefacts produced by the final
end-to-end reviewer after the Wave 3 commits landed on 2.2.0:

  CHANGELOG-v2.2.0.md (144 L)
    Adds Wave 3 entries to Highlights / Added / Changed /
    Deprecated / Under-the-hood.  Version-bump, BaseCachedProxySlice
    SingleFlight migration, pypi/go/composer ProxyCacheWriter
    wiring, RequestContext expansion + Deadline + ContextualExecutor,
    StructuredLogger 5-tier + LevelPolicy + AuditAction, and the
    @deprecated MdcPropagation status — all documented with forensic
    and architecture-review section refs.

  docs/analysis/v2.2.0-pr-description.md (174 L)
    PR #34 body; WI checklist now shows 8 shipped / 6 deferred;
    test-run evidence 3,432 tests green; five PR-reviewer focus
    points (remaining MdcPropagation callers, lost user_agent sub-
    field parsing, audit-logger suppressibility gap in log4j2.xml,
    DbIndexExecutorService submit()-path bypass, four-adapter
    "any exception → 404" swallow inherited from Maven).

  docs/analysis/v2.2-next-session.md (399 L)
    Refreshed agent-executable task list.  Removes the four
    shipped items (WI-post-05, WI-post-07, WI-02, WI-03).  Keeps
    WI-04 / WI-06 / WI-06b / WI-08 / WI-09 / WI-10 in the same
    Goal / Files / Tests / DoD / Depends-on shape.  Adds four
    WI-post-03 follow-ups surfaced during Wave 3:
      a. Hoist DbIndexExecutorService to pantera-core/http/
         context/ContextualExecutorService.
      b. Re-lift user_agent.name / .version / .os.name parsing
         into StructuredLogger.access.
      c. Unify the ~110 remaining MdcPropagation call-sites
         after WI-06 + WI-08 + the Vert.x-handler migration,
         then delete MdcPropagation.java.
      d. Migrate 11 Vert.x API handlers (AdminAuth, Artifact,
         Auth, Cooldown, Dashboard, Pypi, Repository, Role,
         Settings, StorageAlias, User) to a ContextualExecutor-
         wrapped worker pool — the single biggest MdcPropagation
         debt.
    Adds one new concern:
      C6. Audit logger inherits log-level config from
          com.auto1.pantera parent — §10.4 declares audit as
          "non-suppressible" but log4j2.xml has no dedicated
          block.  Five-line fix tracked separately.

Review verdict: PASS.  Every §12 DoD met.  Every commit conforms
to type(scope): msg, zero Co-Authored-By trailers across all 11
new commits (verified via git interpret-trailers --only-trailers).
3,432 tests green across pantera-core / pantera-main / every
touched adapter module.
@aydasraf
aydasraf force-pushed the 2.2.0 branch 2 times, most recently from 215ffbc to 5b86aa9 Compare April 22, 2026 17:05
@aydasraf

Copy link
Copy Markdown
Collaborator Author

Search + Browse overhaul (bugs A–F) — 6 commits added to this branch

Users filed six discrete complaints against search & browse — investigation collapsed them into four root causes, plus a separate delete-artifacts bug surfaced along the way.

Commits in this stack

SHA Scope
c98359fb Bug A: SearchHandler enum round-trip dropped sort_dir; every created_at query degraded to RELEVANCE. 2-line fix + regression test + contract pin.
69be3cd6 Bug C + D: V123 name_sort generated column for natural name order (pkg-2 < pkg-10); search UI renders created_at with relative label + tooltip.
95808845 Bug B: V124 artifact_kind classification column; search SQL unconditionally excludes .md5 / .sha* / .asc / .sig / maven-metadata.xml / .meta.* / .pantera-meta.* / Debian index files / Helm / Composer. The wkda.common.api.vehicle-api query that returned 16 510 metadata rows now returns real artifacts.
e7fd0039 Bug E + F: treeHandler rewritten to hydrate file entries via a single batched DB query (size / modified / kind); accepts `sort=name
a07b01f3 Cleanup + delete-artifact fix: kill three dead SQL constants + three legacy search helpers (the 3-arg search() overload now delegates to the filtered path); RepoData.deleteArtifact now uses DB-fallback storage lookup so DB-only repos don't 500 with ValueNotFoundException: No value for key: {repo}.yml.
a4d7d023 CHANGELOG + docs/analysis/v2.2.0-search-browse-overhaul.md covering root causes, schema changes, migration cost, scale characteristics, test matrix.

Schema changes

  • V123 adds name_sort STORED generated column built via natural_sort_key(text) — zero-pads digit runs.
  • V124 adds artifact_kind STORED generated column built via classify_artifact(text) — labels rows as ARTIFACT | CHECKSUM | SIGNATURE | METADATA.

Both are table rewrites at migration time. On a 10M-row table plan 2–10 minutes; SHARE ROW EXCLUSIVE lock blocks writes, does not block reads.

Test coverage

262 passing tests in the broader suite. Added:

  • createdAtSortHonoursDirection — pins asc/desc produce distinct strictly-ordered items.
  • toSortFieldRejectsEnumNameFormForDate — contract pin: "date" is not a valid wire key (prevents regression).
  • nameSortIsNaturalpkg-2 / pkg-10 / pkg-11 / pkg-100 in both directions.
  • searchExcludesNonArtifactKinds — 6 sidecar rows filtered, 1 real artifact returned.
  • classificationFunctionLabelsKnownPatterns — 19-case contract for the classification function.
  • treeEchoesSortParams — default ordering + round-trip + unknown-value fallback.

Also added DbManager.migrate() to DbArtifactIndexTest.setUp() — the test suite previously ran only the core schema via ArtifactDbFactory.createStructure() and couldn't see Flyway-managed columns (version_sort, name_sort, artifact_kind).

Deploy notes

  • Two migrations (V123, V124) will rewrite the artifacts table. Reads unaffected; writes pause during each rewrite. Run outside peak windows on large tables.
  • No config/env-var changes.
  • Existing API callers unaffected — sort=created_at silently started working; sort=date still falls back to relevance (documented).
  • New tree response fields (modified, artifact_kind, sort, sort_dir) are additive.

Explicit non-goals / follow-ups

  • ?include=metadata,checksums,… query param on /api/v1/search — the classification + index are in place, only the handler plumbing is deferred.
  • Deleting FileSystemBrowseSlice — still wired as HTML fallback; revisit after the JSON tree beds in.
  • Gradle adapter is genuinely dead (no source, not in reactor) — explicitly documented as such.

🤖 Generated with Claude Code

@aydasraf

Copy link
Copy Markdown
Collaborator Author

Follow-up: 51abdaae fix(delete): cascade storage delete into artifacts DB index. Delete now cleans up the artifacts rows; search/locate no longer returns ghosts after DELETE /api/v1/repositories/:name/artifacts. +3 regression tests (38 total now).

aydasraf added 14 commits April 27, 2026 15:35
…ast-Modified

Replaces the Solr-based MavenCentralSource approach for latency-sensitive
lookups. HEAD to repo.maven.apache.org returns accurate Last-Modified in
~67ms with zero indexing lag (vs 11+ month lag from Solr API).
…k for JFrog mirrors

Queries JFrog Artifactory's /api/storage/{repoName}/{path} endpoint to retrieve
the original upstream lastModified date, bypassing the intermittently stale
Last-Modified header JFrog returns for remote/cached artifacts.
…omposition

Composes a primary PublishDateSource with a fallback: if primary returns
Optional.empty() or completes exceptionally, the fallback is queried.
Includes 6 unit tests covering all branches using stub sources (no WireMock).
…own timeout to 2s

Replace Solr-based MavenCentralSource with ChainedPublishDateSource
(MavenHeadSource → JFrogStorageApiSource) for both "maven" and "gradle"
repo types. Reduce cooldown orTimeout from 5s to 2s since HEAD completes
in ~67ms.
…ommit

Splits write-with-sidecars into verify-then-commit phases so callers
can serve from the temp file immediately, committing to storage async.
Replaces sequential thenCompose chain with allOf for ~4x speedup
on 4-sidecar artifacts (Maven). Saves ~5% of cold-start overhead.
Uses writeAndVerify → contentFromTempFile for immediate response,
commitAsync for background persistence. Eliminates re-read-from-storage
latency (~15-20% of cold-start overhead).
Same serve-before-commit pattern as Maven. Go .zip artifacts now stream
from the verified temp file immediately.
commitVerified() now reads the temp file eagerly into a byte[] for
storage save, avoiding the race where commit() deleted the temp file
before the response Flowable opened it. contentFromTempFile() disposer
handles cleanup after the response body is fully consumed.
Same serve-before-commit pattern as Maven and Go.
Same serve-before-commit pattern as Maven, Go, and Composer.
Eliminates the 401 challenge-response round-trip on every request.
Each Maven artifact fetch was doing 2 HTTP requests (unauthenticated →
401 → retry with auth). This cuts ~20% of cold-start proxy overhead.
fix(composer): guard providers type in ComposerGroupSlice merge
@aydasraf aydasraf changed the title v2.2.0 foundation: queue hotfix + Fault + SingleFlight + CacheWriter v2.2.0 — target architecture + cooldown + publish-date registry + serve-before-commit Apr 29, 2026
aydasraf and others added 9 commits May 3, 2026 22:37
Add release_date column to artifact_cooldowns + history tables (V126),
thread the value through JdbcCooldownService -> CooldownRepository ->
DbBlockRecord -> CooldownHandler API, and surface it in the admin
CooldownView.vue. Migrates Docker from DockerProxyCooldownInspector to
the registry-backed inspector so docker tags now persist publish dates
the same way other ecosystems do.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Remove the legacy maven-adapter NegativeCache and the four shim test
classes; collapse all callers onto the shared pantera-core implementation
keyed by composite (scope, repoType, artifactName, artifactVersion).
URL-encode embedded colons in the components, switch pattern matching
from contains() to exact-match-with-* wildcard, and update the admin
endpoint + UI to render the new key shape.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add SyncArtifactIndexer SPI in pantera-core and DbSyncArtifactIndexer
implementation in pantera-main; thread it through every adapter's
upload/push slice so the artifact_index row is written inline with the
storage commit before the 201 returns. Closes the 2 s read-after-write
window where the async DbConsumer left a freshly-uploaded artifact
invisible to group lookups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Populate go.mod/hello.go/hello_test.go with real module content so the
test publish script no longer ships zero-byte uploads. In GoUploadSlice,
invalidate the negative cache for the just-uploaded module so a publish
followed by an immediate fetch returns 200 instead of stale 404. Tighten
GoProxySource and GoSlice handling for the fanout path that the new
sync indexer now touches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Read max_connections_per_destination from the http_client YAML block
(was hard-coded to 5, the Vertx default), apply a 0.25 factor to the
publish-date concurrency budget, and forward each ecosystem's native
User-Agent (pip / npm / cargo / etc.) to upstream so we are not
throttled or fingerprinted as a generic bot. Renames the old "hcs"
setting to a human-readable name and keeps the per-source UA aligned
with what real clients send.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MavenGroupSlice and ComposerGroupSlice now run merged maven-metadata.xml
and packages.json through the cooldown filter before caching, with the
RegistryBackedInspector wiring (slashed -> dotted name conversion;
maven-group/gradle-group both routed through the maven-metadata path).
Ensures gradle/mvn clients hitting a virtual group do not see versions
that the cooldown service has blocked at member level.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two pre-existing bugs surfaced while debugging Gradle latest.release
returning 403 against a cooldown-blocked Guava 33.6.0-jre. (1)
MavenMetadataFilter.updateLatest only rewrote <latest>; clients still
read the stale <release> and tried to fetch the blocked version.
updateLatest now recomputes <release> from the filtered version list
(latest non-SNAPSHOT). (2) CachedProxySlice.applyMetadataCooldown
passed the package name in slashed form (com/google/guava/guava) but
MavenHeadSource splits on the last dot to derive groupId/artifactId,
so the inspector lookup silently missed and the filter fell open.
Convert to dotted form before invoking the metadata service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PypiSimpleHandler and PypiJsonHandler passed the raw URL package name
(Foo_Bar) to the cooldown service, but the publish path stores release
dates under the canonical form (foo-bar). The inspector lookup missed
the DB row and the filter silently fell open. Apply
NormalizedProjectName.Simple at the handler boundary so the request
key matches the publish key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous implementation used String.contains("-") and a handful of
substring checks, so any version with a hyphen was flagged prerelease.
Guava's classifier-style suffixes (-jre, -android) were therefore all
filtered out of the "newest stable" candidate list, and the cooldown
service collapsed <latest>/<release> to a 2011-vintage release (r09)
when 33.6.0-jre was blocked. Tokenize on -/./+, skip the version core
token, and match each remaining token against an explicit set of known
qualifier keywords plus the Maven milestone shorthand. Adds tests
covering Guava classifier suffixes, Spring .RELEASE, and substring
false-positives (-archived, -developer, -macos).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lution

AuditLogger's five methods (upload, download, delete, resolution,
publish) each read client.ip and other identity fields from MDC,
silently emitting nothing when the calling thread never had MDC
bound (Quartz workers, RxJava continuations, any adapter missing a
RequestContextHeaders.bindToMdc call). A full trace across every
format's upload/publish/delete/proxy-fetch path found several
distinct defects beyond the reported client.ip gap:

  - download() was dead code: SliceDownload was imported by nine
    format adapters, never instantiated in production. Deleted.
  - SliceUpload called AuditLogger.upload() directly AND queued an
    ArtifactEvent that separately produced AuditLogger.publish() via
    DbConsumer -- a double audit line for the same upload, for every
    format reusing this generic class (conan, generic files).
  - Cooldown-blocked requests had zero audit trail. AuditLogger's
    five methods all hardcoded event.outcome=success; there was no
    way to express a denial. A blocked fetch returned a correct 403
    visible only in the generic HTTP access log.
  - A cache-hit proxy serve was never audited at all -- only a
    cache-miss-then-fetch reached AuditLogger.publish() via the
    async DbConsumer queue.
  - Range-based dependency resolution (npm ^2.0.0, unpinned pip
    install) has a silent blind spot: when cooldown filters the
    newest matching version out of a metadata listing, the resolver
    picks the next-allowed version with no record a newer one
    existed. Six formats had their own metadata-filter class with
    this gap, none auditing it.

New taxonomy -- four events, one AuditContext record carrying
traceId/clientIp as compulsory constructor parameters instead of an
MDC read:

  artifact_publish    local/hosted repo, actual artifact upload
  artifact_access     proxy fetch + local reads, actual artifact,
                       any outcome (cache hit / cache-miss fetch /
                       cooldown block)
  artifact_delete     any repo mode
  artifact_resolution proxy/group metadata listing view, fires
                       unconditionally (event.type=allowed|change)

Deliberately out of scope for publish/access: pure metadata requests
carrying no binary artifact bytes (maven-metadata.xml, PyPI
simple-index, Composer's JSON-only AddSlice as opposed to the real
AddArchiveSlice upload path) -- only resolution covers metadata, and
only for the cooldown-visibility case above.

EcsLogger.field() already has MDC-wins deduplication built in (drops
a field value only when ThreadContext already has that key, keeps it
otherwise) -- so passing client.ip/trace.id explicitly via
AuditContext is safe with zero duplicate-key risk either way.

Per-format fixes landed as part of this taxonomy:
  - Docker: DockerProxyCooldownSlice's three near-identical
    cooldown-block branches consolidated into one audited helper;
    DockerManifestByTagHandler now audits access;
    DockerTagsListHandler now audits resolution.
  - PyPI + Debian: their bespoke DeleteSlice classes never queued a
    DB-index-removal event at all -- a functional bug beyond the
    audit gap. Both now route through
    RepositoryEvents.addDeleteEventByKey and audit delete.
  - Maven + npm: share one metadata-filter choke point
    (MetadataFilterService.evaluateAndFilter), fixed once for both.
    CooldownMetadataService.filterMetadata gained a new 9-arg default
    overload delegating from the untouched 7-arg original, so
    NoopCooldownMetadataService and every test double kept compiling
    unmodified.
  - PyPI, Go, Composer, Docker's remaining metadata-filter handlers
    all now audit resolution unconditionally on both branches.
  - RepositoryEvents gained repoType()/repoName()/artifactName(Key)
    accessors and a public VERSION constant so SliceUpload/
    SliceDelete build audit calls with the same identity the queued
    ArtifactEvent uses.

Verified with a genuinely clean mvn clean install -U -T8 -- mvn
compile / mvn test-compile against a stale target/ initially reported
success while four test files were actually broken (Maven's default
incremental compiler skips recompiling an unchanged test file even
when a method it calls changed shape elsewhere); only a clean build
surfaced them.
@aydasraf

aydasraf commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Audit logging: structural taxonomy rewrite (commit 7793cf7)

Follow-up to the earlier client.ip/trace.id audit-log fix (e36d86dec) — a full trace across every format's upload/publish/delete/proxy-fetch path surfaced several distinct defects beyond the reported gap, and this commit fixes them all under one consistent taxonomy.

Found:

  • AuditLogger.download() was dead code — imported by 9 adapters, never instantiated.
  • The generic SliceUpload double-audited every upload for conan + generic-files (artifact_upload direct call + artifact_publish via the async queue, same physical upload).
  • Cooldown-blocked requests had zero audit trail — AuditLogger's methods all hardcoded event.outcome=success, no way to express a denial.
  • Cache-hit proxy serves were never audited at all — only cache-miss-then-fetch reached the audit log.
  • 6 formats (maven, npm, pypi, go, composer, docker) each have their own cooldown metadata-filter class with an identical silent blind spot: range-based dependency resolution (npm ^2.0.0, unpinned pip install) picks the next-allowed version with zero record a newer one was hidden.
  • PyPI's and Debian's bespoke DeleteSlice classes never queued a DB-index-removal event — a functional bug (stale artifacts table rows), not just an audit gap.

New taxonomy — four events (artifact_publish, artifact_access, artifact_delete, artifact_resolution), each taking a compulsory AuditContext(traceId, clientIp) record instead of an MDC lookup, wired consistently across all 15 format adapters. Full rationale + per-field table in CHANGELOG.md.

Verification note: mvn compile/mvn test-compile against the existing target/ initially reported success while 4 test files were actually broken — Maven's default incremental compiler skips recompiling an unchanged test file even when a method it calls changed shape elsewhere. Only mvn clean install -U -T8 surfaced them. Final build: BUILD SUCCESS, full test suite, all ~30 modules.

@aydasraf

aydasraf commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Audit-semantics hardening pass (b73d92c)

Squash of four fixes on top of the taxonomy rewrite (7793cf7), each triggered by live log anomalies and closed with regression tests that were verified to fail against the pre-fix code:

1. artifact_publish fired on every cache-hit re-serve (composer/npm/go/pypi) — reported as a php_proxy package showing artifact_publish with no client.ip on a plain re-download of an already-cached artifact. All four bespoke proxy slices now enqueue the publish event only on genuine cache-miss-then-fetch, bind MDC context before constructing ProxyArtifactEvent (no more null client.ip / stale-thread trace.id), and emit artifact_access for hit/miss/cooldown-block. All four stage-2 *ProxyPackageProcessor jobs now forward context via withContext(...) like maven's.

2. npm cooldown was structurally bypassed for fresh tarballsNpmProxy.getAsset() fetches-and-saves internally on miss, so the cooldown gate ran only after the artifact was already cached. New hasAssetInStorageAsync existence probe; blocked version now → 403, zero upstream contact, empty storage (pre-fix replay returned 200).

3. artifact_resolution now fires on EVERY metadata listing view — reported as "multiple npm show, no audit logs". Three systemic suppression points in MetadataFilterService.filterMetadata (cooldown-disabled early return, filtered-metadata cache hits, npm ETag-304 exits) plus a branch-by-branch sweep of all 7 dedicated metadata handlers (composer skipped its handlers entirely when cooldown off; go's all-blocked 403s and .info/.mod; docker's /tags/list fallbacks incl. an unfiltered-tags leak now made visible; maven's three cache/fallback holes incl. the group slice). Audit moved to the filterMetadata boundary — exactly one record per request at every exit, with the blocked-version list preserved across cache hits.

4. npm audit records were emitted twice per request — same trace.id 2 ms apart: CachedNpmProxySlice's dedup ran the origin slice twice (signal probe + re-fetch). The leader now serves its own response; origin-traversal count asserted == 1 in tests (pre-fix: was <2>). Bonus: one wasted origin round-trip per npm request removed, probe body no longer dropped unconsumed.

Full mvn clean install -T8 green. CHANGELOG updated with all four entries.

…he-miss only, resolution on every metadata view, no doubles, full client.ip/trace.id propagation

Squash of four fixes hardening the v2.2.0 audit taxonomy
(publish/access/delete/resolution) across every wired proxy format.

1. Publish gated to genuine cache-miss; access wired everywhere.
   Composer, npm, go, and pypi's bespoke proxy slices fired an
   artifact_publish DB/audit event on every cache-hit re-serve of an
   already-cached artifact and never bound request context before
   constructing the ProxyArtifactEvent (client.ip null, trace.id
   stale-thread leak). All four now enqueue only on
   cache-miss-then-fetch, bind MDC context first, and emit
   artifact_access for cache-hit / miss-fetch / cooldown-block
   outcomes. All four *ProxyPackageProcessor stage-2 jobs now forward
   trace.id/client.ip onto the final ArtifactEvent via withContext(...)
   (previously only maven's did). Maven's bespoke primary path gained
   the missing access wiring.

2. npm cooldown actually enforces on fresh tarballs.
   NpmProxy.getAsset() conflates the storage check with the upstream
   fetch-and-save, so DownloadAssetSlice's cooldown gate ran only
   after the artifact had already been fetched and cached. New
   NpmProxy#hasAssetInStorageAsync pure existence probe; the slice
   gates on it. Regression test replayed against the pre-fix code
   returned HTTP 200 for a blocked version; now 403 with zero
   upstream contact and empty storage.

3. artifact_resolution fires on EVERY metadata listing view.
   Three systemic suppression points in the maven/npm choke point
   (MetadataFilterService.filterMetadata): cooldown-disabled early
   return, FilteredMetadataCache hits (request #2..N within the 24h
   TTL were silent — the reported "multiple npm show" symptom), and
   npm's ETag early-304 exits. Audit moved to the filterMetadata
   boundary, one record per request at every exit; CacheEntry carries
   the blocked-version set (L1) so cache hits keep the precise
   filtered list; new AuditLogger.resolutionDetailUnknown
   (event.type=["info"]) covers serves where filter state cannot be
   known (L2 promotion, 304, parse fallbacks); all-blocked denials
   audit the full hidden list before throwing; the
   CooldownMetadataService 9-arg interface default audits so Noop
   deployments audit too. Per-format gaps closed from a
   branch-by-branch sweep of all 7 dedicated metadata handlers:
   composer's proxy slice no longer skips its handlers entirely when
   cooldown is Noop; go's all-blocked 403s, parse fallbacks, and
   .info/.mod files (now resolution; previously silent or mis-audited
   as access); docker's /tags/list parse/empty/rewrite fallbacks (the
   last leaks unfiltered tags — now visible); maven's materialised
   filtered-output cache hit, null-service branch, group
   merged-metadata cache hit, and group Noop short-circuit; npm's
   no-service serves and the /latest raw shortcut. PyPI was already
   correct. Non-2xx upstream forwards deliberately do not emit
   resolution (no listing served; HTTP log carries it under the same
   trace.id).

4. npm audit records no longer doubled.
   CachedNpmProxySlice's WI-05 signal-based dedup ran the origin
   slice twice per request on the success path (signal probe with
   discarded unconsumed body + re-fetch), emitting every audit record
   twice with the same trace.id. The dedup leader now serves the
   response from its single origin traversal; followers keep the
   re-fetch (their single traversal). Synthetic 404/503 signal
   mappings preserved for RaceSlice's multi-remote fallback; probe
   body drained on non-success paths. Side benefit: one full origin
   round-trip per npm request eliminated. BaseCachedProxySlice
   verified unaffected (signalToResponse reads the cache directly).

Every fix carries regression tests verified to fail against the
pre-fix code (cooldown-block 200->403, resolution-on-cache-hit /
-disabled / -all-blocked, origin-traversal count "was <2>").
Full mvn clean install -T8 green (build + complete test suite).
aydasraf added 23 commits July 2, 2026 22:23
…reason phrases

Under concurrent load a request could observe an npm cache entry whose
stream-through save was still mid-commit (the request-dedup gate
releases followers when the leader's RESPONSE resolves, which is before
the storage tee finishes writing). The follower's read then failed with
ValueNotFoundException and surfaced as a 502 that fed the group's
per-member circuit breaker. RxNpmProxyStorage.getAsset now degrades a
present-but-unreadable entry to a cache miss so callers refetch from
upstream — a cache entry we cannot read is not a cache entry.

PanteraHttpException.MEANINGS lacked 502/503/504, so upstream Bad
Gateway / Service Unavailable / Gateway Timeout were all logged as
"PanteraHttpException: Unknown", which materially slowed diagnosis.

Both found by running the revived CI perf gate end-to-end locally.
Verified: pantera-core + npm-adapter suites green; perf gate 4/4
consecutive local passes at 60s/30rps.
…o-end

The performance/*/* gitignore silently kept the whole harness (WireMock
mappings + fixture generator, k6 scenarios, repo configs, nginx LB
conf, run scripts) out of the tree — both perf workflows had failed at
the checkout step since their introduction, and the perf gate never
executed anywhere but dev machines.

Now committed and bootable from a fresh checkout:
- make setup → scripts/setup-harness.sh: renders pantera-config.yml
  from the template (no envsubst dependency), generates a throwaway
  RS256 keypair when secrets are absent, seeds fixture files; fully
  idempotent.
- docker-compose-scaling.yml: k6 load-generator service (profile-
  gated), metrics port published (the gate scrapes /metrics/vertx —
  metrics were previously not even enabled in the bench config),
  overridable host ports so the harness can run beside a dev stack,
  bench auth via PANTERA_USER_NAME/PASS + seed.sql principal (2.2.0 is
  deny-by-default), WireMock sized for the 10MB body bucket and run
  with --no-request-journal (the unbounded journal OOMed the mock
  after ~2 runs and a wounded JVM dripped 500s that tripped Pantera's
  circuit breaker — the gate's original flakiness), nginx retry of
  transient backend hiccups, HEAD stub so breaker probes can close.
- k6/gate.js: bounded reads-only constant-arrival gate load (default
  60s/30rps) with a sequential warmup and a checks threshold so a
  silent all-401/all-503 run fails instead of handing the invariant
  check an idle instance. scenario.js (the ~20min 3000rps matrix ramp)
  stays nightly-only.
- perf-gate.yml: correct service names (pantera-sut / mock-upstream —
  the old ones didn't exist in the compose file), no more `|| true` on
  the ramp, concurrency cancellation.
- perf-baseline.yml deleted (redundant, never passed once);
  perf-cold-bench.yml is nightly + workflow_dispatch only — wall-clock
  thresholds are too noisy on shared runners to block PRs.

Local verification: mock stack clean at 4x gate rate (5400/5400 at
120rps), then four consecutive full gate runs: 100% checks, zero 429s,
amplification ≤0.75, zero breaker trips — 4/4 PASS.

The bring-up also surfaced a latent product bug chain (host-shared
upstream breaker trips on a single 5xx; synthetic fastfail 502s convict
all group members; all-blocked maps to blanket 404 + negative-cache
poisoning). RCA with code refs: logs/.analysis/perf-gate-cascade-rca.md
(untracked analysis dir) — follow-up fixes recommended.
… dependabot, SHA pins

ci.yml targeted the larger-runner label ubuntu-latest-4-cores, which
was never provisioned for this repo: every run queued for 24 hours and
was auto-cancelled — the repo has had no functioning CI (and no checks
on any PR, dependabot included). Public repos get 4-core ubuntu-latest
for free, so the label bought nothing but a dead pipeline.

- ci.yml: ubuntu-latest; cheap static gates (ECS drift, nginx CVE
  guard) run first in a ~1min job the build jobs depend on; dorny/
  paths-filter skips the backend build on UI-only PRs and vice versa;
  per-ref concurrency cancellation.
- release.yml: publishes a multi-arch (amd64+arm64) image to
  ghcr.io/auto1-oss/pantera with GITHUB_TOKEN on every tag (latest tag
  reserved for finals), reusing the built dist instead of rebuilding;
  attaches SHA256SUMS; attests build provenance for the binaries and
  the image (id-token/attestations permissions).
- nightly-integration.yml: mvn verify -Pitcase nightly — integration
  tests previously ran ONLY in the tag-time release workflow, the worst
  possible moment to discover breakage.
- dependabot.yml: weekly grouped updates for maven, pantera-ui npm,
  and github-actions; docker-compose test fixtures deliberately not
  covered (pinned dependency trees exist to exercise the proxy).
- Every third-party action across all workflows pinned by commit SHA.
Also documents the final root cause of the dead pipeline: beyond the
unprovisioned runner label, the CI workflow had been manually DISABLED
in the repo settings (now re-enabled via gh workflow enable).
The org Actions allowlist permits GitHub-owned actions only (repo-level
override returns 409), which startup-failed every run referencing a
third-party action. Rather than requesting an org-admin allowlist
change, the workflows now use zero third-party actions — the smaller
trust surface suits the product anyway:

- ci.yml: dorny/paths-filter replaced by plain git-diff change
  detection against the PR merge-base (fetch-depth: 0 on the cheap
  static job only).
- release.yml: softprops/action-gh-release replaced by the
  preinstalled gh CLI (gh release create with GITHUB_TOKEN);
  docker/setup-qemu / setup-buildx / login / build-push replaced by
  raw docker buildx + tonistiigi/binfmt (a container image, not
  policy-gated) + docker login --password-stdin. Image digest for the
  provenance attestation read from buildx --metadata-file.
… tests

The revived pipelines' first executions surfaced three Linux-runner
issues that macOS development never hit:

- seed-files.sh linked ~70k artifacts to one source inode; ext4 caps
  hardlinks at ~65k per inode (APFS is far higher), failing the gate's
  harness prep with "Too many links". Links now distribute across a
  4-copy pool per size bucket (~17.5k per inode).
- RaceSliceTest.returnsFirstSuccessResponseInParallel asserted a 50ms
  success beats a 150ms one — a 100ms margin that scheduler jitter on
  loaded shared runners inverted. The assertion is about ordering
  semantics, not timing precision; margins widened to 10ms vs 2s.
- ContextualExecutorIntegrationTest.runnerThreadContextRestored seeded
  and probed ThreadContext (per-thread state) across a 2-thread pool
  with no thread-identity guarantee. Now uses a dedicated
  single-thread executor.
Perf gate: the SUT container runs as uid 2021; Linux bind mounts preserve
host ownership, so openssl's 600-perm throwaway JWT key was unreadable
(RsaKeyLoader -> 'JWT private key not found') and the seeded data tree was
unwritable for proxy-cache stores. setup-harness.sh now chmods the bench
keys to 644 and the data tree a+rwX. macOS Docker Desktop remaps ownership,
which is why local runs never hit this.

CI: standalone 'mvn pmd:check' resolves the build-tools ruleset jar and
sibling-module jars from the local repository, which is empty on a clean
runner. Fold test + PMD + license into the canonical single-session
'mvn clean install -T8' where reactor resolution and module-by-module
install ordering make both resolvable by construction.
Remove perf-gate.yml, perf-cold-bench.yml and nightly-integration.yml —
public CI is now exactly two workflows: ci.yml (build + test backend/UI;
external contributors' runs start only after maintainer approval, which is
already enforced by the repo's all_external_contributors policy) and
release.yml.

release.yml is workflow_dispatch-only: the version is read from the root
POM of the dispatched ref (bump-version.sh stays the single source of
truth), a pre-flight job refuses to reuse an existing v<version> tag, and
'gh release create --target' creates the tag at the dispatched commit.
Artifacts, SHA256SUMS, provenance attestations and the multi-arch GHCR
image are unchanged. Release notes heredoc now actually interpolates the
version (was quoted, rendered literal ${VERSION}).

The perf harness itself stays in the repo for local runs (make setup /
smoke / cell); integration tests still gate every release via -Pitcase.
The docker-build profile auto-activates whenever /var/run/docker.sock
exists — true on GitHub runners — and binds a multi-arch
'docker buildx --load' to pantera-main's install phase. Runners have no
arm64 binfmt and --load cannot export a manifest list, so the build died
at the last module. Image publishing is release.yml's job; CI only needs
compile + tests + PMD + license. Verified locally: exec reports 'skipping
execute as per configuration', reactor BUILD SUCCESS.
…rmat

The 2.2.0 entry carried ~580 unsectioned lines of internal development
log (RCAs, bench tables, file lists) above the first section header,
plus an invented 'CI & tooling' section. Entry now uses only the house
sections with concise attributed bullets. User-facing items that lived
only in the dev log (audit taxonomy, unified save bar, group request
coalescing, abandoned-body timer, PyPI uv/PEP 691, PyPI cooldown
fail-open) are kept as single bullets; stale bullets corrected
(bulkhead defaults 40/+4 per V135, removed bullets describing the
deleted CI perf workflows and the flipped head-fallback default).
The async subscribe returned before the server registered the
subscriber, so an instance logged 'pub/sub started' while not yet
subscribed — invalidations broadcast during that window (e.g. while a
node joins during a rolling deploy) were silently lost, leaving stale
L1 caches. Surfaced as a lost-message race in CacheInvalidationPubSubTest
under CI load.

Blocking is confined to startup: the only production construction path
is VertxMain -> SettingsFromPath -> YamlSettings on the boot thread,
which already blocks on connectPubSub() two lines above; this adds one
round-trip on an established connection. publish() and message dispatch
stay fully async.
Maven: handlebars 4.4.0->4.5.2 (root pom; declared but unreferenced —
removal candidate), commons-lang3 3.14.0->3.20.0 (pantera-storage-core,
npm-adapter; aligns with pantera-core), protobuf-java 3.21.10->3.25.5
(hexpm-adapter; runtime is gencode-compatible), xmlunit-core/matchers
2.7.0->2.10.0 (rpm-adapter).

pantera-ui: axios 1.15.2->1.16.0, vite 8.0.8->8.0.16, vitest
3.2.4->3.2.6 (critical), plus lockfile-only transitive fixes via npm
audit fix (js-cookie, form-data, esbuild, ws, vite-node's vite 7.x).
npm audit now reports 0 vulnerabilities.

Verified: full reactor 'mvn clean install -T8' green (one unrelated
VertxSliceServerTest flake under parallel load passed 3x + full module
suite standalone); UI type-check/lint/94 tests/build all green.

Not addressed here: ini4j 0.5.4 (conan-adapter) has no patched release;
needs a replacement decision. Test-fixture alerts (nuspec parser data,
uv-compat sample lock, docker-compose demo pom) dismissed as not_used.
ini4j 0.5.4 has an open advisory with no patched release, and it parsed
conaninfo.txt from uploaded/proxied packages — attacker-influenceable
input. The adapter already ships its own IniFile parser; ConansEntity
now uses it. Behavioral parity is pinned by ConansEntityTest's exact
JSON expectations: bare lines (recipe_hash, list-style entries) stay
out of section objects (IniFile stores them as empty-value keys, the
old parser as null values), empty sections still render as {}.
IniFile gains a String constructor; ini4j is removed from the pom.
Root-caused in the 2.2.0 breaker RCA: the outbound HTTP breaker tripped
on a SINGLE 5xx for 30s keyed by bare hostname; its synthetic 502s lost
the X-Pantera-Circuit-Open marker in the npm/maven adapters, so
GroupResolver convicted every member's health window on fabricated
evidence; all-members-blocked walks returned 404 and poisoned the
shared negative cache beyond the outage.

- Marker propagation: UpstreamCircuitOpenException (pantera-core)
  carries circuit-open + Retry-After through npm and maven adapter
  error funnels; GroupResolver treats a marker-502 as 'member
  skipped' — walk continues, no recordFailure().
- All-skipped terminal is 503 + Retry-After + marker, never a
  negative-cached 404; genuine all-404 walks still cache.
- Volume/rate gate: the breaker now trips at >=50% failures across
  >=10 calls in a 30s sliding window (AutoBlockSettings shape), seed
  2s; window cleared on trip; failed probes still re-trip
  unconditionally.
- Keyed per scheme://host:port instead of bare host.
- Circuit-open members get one cache-only probe (internal header,
  honored by BaseCachedProxySlice) so warm cache still serves —
  Nexus-style serve-cached-while-blocked.
- ECS transition logging: trip/probe-failure at WARN, recovery at
  INFO with fast-fail count; group skip/all-open events logged.

16 new tests across breaker gate, group walk semantics, cache-only
probe, and marker survival.
The outbound breaker's gate and backoff (failure-rate threshold,
minimum calls, window seconds, seed/max backoff) become DB-backed
runtime settings, mirroring the group-member breaker's
AutoBlockSettings pattern: upstream_breaker_* keys in auth_settings
(seeded by V136), UpstreamBreakerSettingsLoader with DB -> env
(PANTERA_UPSTREAM_BREAKER_*) -> hardcoded fallback, invalidated on
admin PUT. Breakers read config through a live supplier, so changes
apply to existing breakers on the next recorded outcome — window
re-allocates and backoff reshapes on change.

GET/PUT /api/v1/admin/upstream-breaker-settings with key whitelist and
constructor round-trip validation. Admin UI gains an 'Upstream HTTP
Circuit Breaker' section directly below the existing one, which is
relabeled 'Group Member Circuit Breaker'; both cards name the other to
keep the two layers distinct. Full save-bar/dirty/discard integration
plus a test pinning independent dirty-tracking.
Audited all dashboards against the live /metrics/vertx exposition and
the registered meter names in code. Fixed three broken references:
pantera_artifact_{download,upload}_bytes_total never existed (now
pantera_repo_bytes_*), and pantera_cooldown_all_blocked is a gauge —
the _total-suffixed rate queries silently rendered nothing.

Added ~25 panels with source-verified tag names: upstream requests/
latency/429s/rate-limit gate/availability, bulkhead permits/overflow/
ramp, proxy cache-write failures (proxy); cache operation latency/
dedup/errors and SWR metadata outcomes (cache-storage); storage
executor pools, events queues, Jetty connection pool (infrastructure);
group resolution latency, member metadata size, drain drops (group);
in-flight requests, handler phases, body sizes (overview); cooldown
evaluate/cache-load latency and admin actions (cooldown). Families
without histogram buckets chart sum/count averages, not quantiles.

The upstream-circuit-breaker and proxy-phase-latency dashboards are now
provisioned into the local compose Grafana (previously only shipped in
src/main/resources and never loaded).
release.yml gains a publish-ui-image job: multi-arch (amd64+arm64)
ghcr.io/<repo>-ui image from pantera-ui/Dockerfile, :latest withheld on
pre-releases, provenance attested, gated on the build-ui test job. The
Dockerfile's node build stage is pinned to $BUILDPLATFORM — the vite
output is platform-independent, so it builds once natively instead of
again under QEMU. Release notes now list both images.

Docs updated for the two release run paths (published images; JAR dist
+ UI bundle): README quick start, admin installation.md (image/port
tables, tag policy, attestation verification, compose example) and
ui-deployment.md (image as first option, full 10-variable env table),
plus pointers in the user and developer guides. Fixed two stale claims
in ui-deployment.md: APP_TITLE defaults to Pantera (not Artipie) and
the /api/ proxy block is generated only when API_UPSTREAM is set.
Verified locally: UI image builds and serves, entrypoint renders
config.json from env defaults.
Panel-by-panel audit of all provisioned dashboards against live
Prometheus (instant-query every target expression):

- pantera-vertx-metrics: every query filtered job="pantera-vertx" but
  all series carry job="pantera" (registry commonTags) — the entire
  dashboard rendered no data since inception. Label fixed; 26 series
  now match the request-rate panel alone.
- pantera-cooldown: cache hits/misses filtered {tier="l2"} but the
  counters carry no tier tag — filter removed.
- pantera-infrastructure: HikariCP acquire/usage panels queried
  client-side quantile series (quantile="0.95"/"0.99") that HikariCP's
  Micrometer binding does not publish — rewritten to sum/count averages,
  which the exposition does carry.
- pantera-cache-storage: removed the SWR metadata-cache panel; audit
  found SwrMetadataCache is never constructed anywhere, so its
  counters can never emit (dead code — flagged separately).

Remaining empty panels are either histogram-quantile queries gated on
PANTERA_METRICS_PERCENTILES_HISTOGRAM=true (documented, default false)
or lazily-registered event counters that are legitimately absent until
their first event (breaker trips, 429s, overflow, errors).
MetadataCacheSwrTest bounded cache serves at 5 ms of wall clock — pure
scheduler noise on loaded CI runners (failed at 9 ms), and zero
detection power: the fake loaders complete instantly, so even a fully
blocking implementation passed the bound.

Fresh-window test: the calls==1 assertion is the actual no-upstream
proof; the timing bound is dropped. Stale-window test: the refresh
loader now parks on a latch — the foreground load() completing while
the loader is parked IS the serve-stale-refresh-in-background
invariant; a regression to synchronous refresh deadlocks into the new
@timeout(10) and fails deterministically. Verified 3x green + full
module suite.
pantera_group_member_requests/_latency lost their recording call sites
when the parallel fanout became the sequential walk (2.2.0), and
pantera_artifact_{downloads,uploads}/artifact_size_bytes never had any
callers — the group and repository dashboards' panels charting them
could not render.

GroupResolver now records each member's outcome and wall latency at
every walk step (success / not_found / error, plus a latency-less
circuit_open count for marker skips and a success record when a
circuit-open member serves its warm cache). RepoMetricsSlice records
artifact counts and the size distribution beside the existing per-repo
byte counters — downloads count only GET responses (HEAD carries
Content-Length without a body transfer).

Verified live against the dev stack: member series appear with
group/member/result tags; artifact counters track authenticated GETs.
PANTERA_METRICS_PERCENTILES_HISTOGRAM previously enabled Micrometer's
~70 auto buckets per timer series. It now publishes two curated
service-level-objective ladders:

- transfer timers (http/proxy/upstream request duration, storage ops,
  Vert.x server response time): 18 boundaries, 10 ms to 20 min — these
  timers span full body streaming, and multi-GB artifacts to slow
  clients legitimately run minutes; without the tail p99 clamps at the
  top bucket and lies.
- control-plane timers (everything else): 16 boundaries, 1 ms to 30 s,
  denser in the 1-500 ms band where those paths live.

Recording cost is bucket-count-insensitive (binary search + lock-free
increment); the cost axis is series count at scrape/TSDB time — the
ladders emit ~6x fewer series than the auto mode at equal dashboard
value (~buckets+1 per tag combination, roughly 6-10k series per
instance under heavy traffic). Bucket shape is fixed at meter creation,
so ladder changes need a restart.

The dev compose sets the flag so every Grafana p95/p99 panel works out
of the box; docs updated accordingly. Verified live: transfer ladder on
pantera_http_request_duration (19 series incl +Inf), control ladder on
pantera_group_member_latency (17 series).
…utorTest

The shared HandlerExecutor pool has a bounded queue + AbortPolicy by
design (production backpressure must be visible). In the shared
surefire JVM, async work leaked by earlier test classes can keep the
queue momentarily full on a loaded CI runner, rejecting this test's
submits (RejectedExecutionException in poolThreadsAreDaemon on run
28672873815). All five submits now retry briefly until the pool
accepts — the tests assert context propagation and thread attributes,
not queue capacity. Verified 3x green.
CooldownMetadataServicePerformanceTest.cacheHitLatencyUnder5ms failed
CI at 5 ms of scheduler noise (run 28673733622) — the seventh distinct
wall-clock flake this branch surfaced serially. Instead of another
one-test fix, this sweeps every remaining tight absolute latency
assertion in the unit suite (core + npm cooldown perf tests, parallel
version evaluation) to ~10x its idle-laptop value.

The assertions guard order-of-magnitude regressions (accidental I/O,
lost caching, per-version network calls); at 10x they still catch every
one of those failure modes while surviving shared-runner load. The
in-memory chaos-test budgets (500 ms / 5 s) were already at sane
margins and are untouched.
@aydasraf
aydasraf merged commit c78e638 into master Jul 3, 2026
3 checks passed
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