Skip to content

test(schema-registry): pin CachedSchemaResolver failure-caching contract (§19)#193

Merged
eschizoid merged 2 commits into
mainfrom
test/cached-schema-resolver-failure-caching
Jun 18, 2026
Merged

test(schema-registry): pin CachedSchemaResolver failure-caching contract (§19)#193
eschizoid merged 2 commits into
mainfrom
test/cached-schema-resolver-failure-caching

Conversation

@eschizoid

Copy link
Copy Markdown
Owner

Summary

Closes the criticality-8 coverage gap in CachedSchemaResolver flagged
by the prior coverage audit: the failure-propagation path under
contention. The §19 "cache forever in-process" invariant in
CLAUDE.md only holds for successful lookups — a failed delegate
call must NOT poison the cache with a sentinel, or a transient SR
outage would pin a topic into a permanent decode-error state.

No production change. The current implementation relies on the
ConcurrentHashMap.computeIfAbsent contract (mapping-function throws →
no entry stored, exception propagates) and these tests now lock that in.

Tests

  • failedLookupIsNotCachedAndIsRetriedOnNextCall — delegate throws
    on the first call, succeeds on the second. Asserts the exception
    propagates verbatim, cache.size() stays 0 after the failure, and the
    retry actually re-invokes the delegate (proving no sentinel). Catches:
    a regression where computeIfAbsent was wrapped in a try/catch that
    silently stored an empty-string sentinel — the second call would
    either also throw or return "" without hitting the delegate.

  • concurrentMissOnSameIdCollapsesToOneDelegateCall — two virtual
    threads race a miss on the same id; the first is parked inside the
    delegate via a CountDownLatch so the second is guaranteed to enter
    computeIfAbsent while the bucket is held. Asserts exactly one
    delegate call and both threads see the same schema string. Catches: a
    regression to a check-then-act get / put shape that would let both
    threads issue the HTTP call.

  • concurrentFailureOnSameIdDoesNotPoisonCache — 16 virtual threads
    race a failing delegate, then one recovery call with a successful
    delegate. Asserts every racing thread observes the failure, size()
    is 0 through the storm, the recovery call succeeds and now caches.
    Catches: a regression that races a cache-write before the delegate
    throw clears state (e.g. a misplaced cache.put(id, ...) outside the
    computeIfAbsent mapping function).

§19 wire-up

Per the §19 invariants in .claude/CLAUDE.md:

computeIfAbsent atomicizes load+store so concurrent misses on the same ID collapse to one HTTP call.

Test #2 is the direct executable proof of that line. Tests #1 and #3
are the implicit corollary: the "cache forever" claim is conditional on
"...after a successful resolve" — anything else (failure storms, partial
state) must leave the cache empty.

Test plan

  • ./gradlew :lib:kpipe-schema-registry-confluent:test — 11 tests pass (8 prior + 3 new), 0 failures.
  • All three new tests use virtual threads via Thread.ofVirtual() + CountDownLatch per project test doctrine.
  • No production change; the existing CachedSchemaResolver already satisfies the contract.

Adds three tests that nail down the failure-propagation path under
contention — the criticality-8 coverage gap in the prior audit.

The §19 "cache forever in-process" invariant is correct ONLY for
successful lookups. If a future regression ever wrote a sentinel into
the ConcurrentHashMap before the delegate exception unwound (e.g. a
"negative cache" optimization), a transient SR outage would pin a topic
into a permanent decode-error state. These tests prove that doesn't
happen today and will catch it if it ever does.

- failedLookupIsNotCachedAndIsRetriedOnNextCall: delegate throws once,
  then succeeds. Asserts the exception propagates, cache stays empty,
  and the retry actually re-invokes the delegate.
- concurrentMissOnSameIdCollapsesToOneDelegateCall: uses a CountDownLatch
  to guarantee the second virtual thread enters computeIfAbsent while
  the first holds the bucket. Asserts exactly one delegate call. Pins
  the §19 "concurrent misses collapse to one HTTP call" guarantee.
- concurrentFailureOnSameIdDoesNotPoisonCache: 16 virtual threads race a
  failing delegate, then one recovery call with a successful delegate.
  Asserts every racing thread observes the failure, cache stays empty
  through the storm, and the recovery call succeeds and caches normally.
@eschizoid

Copy link
Copy Markdown
Owner Author

@copilot please review

Per user feedback, code comments must state the rule directly rather
than cite section numbers from architecture docs (the section numbers
drift; the load-bearing context survives).

Replaces the two §19 references in CachedSchemaResolverTest with
direct wording: "Confluent SR schema IDs are immutable" and
"computeIfAbsent must collapse concurrent misses".
@eschizoid

Copy link
Copy Markdown
Owner Author

@copilot please review

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.60%. Comparing base (eb21492) to head (55db330).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #193      +/-   ##
============================================
- Coverage     77.64%   77.60%   -0.04%     
+ Complexity      732      731       -1     
============================================
  Files            68       68              
  Lines          2800     2800              
  Branches        346      346              
============================================
- Hits           2174     2173       -1     
  Misses          465      465              
- Partials        161      162       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@eschizoid eschizoid merged commit 816123d into main Jun 18, 2026
3 of 4 checks passed
@eschizoid eschizoid deleted the test/cached-schema-resolver-failure-caching branch June 22, 2026 04:02
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