Skip to content

fix(#5357): make EventMeshFrame immutable (defensive copy + unmodifiable view + withAttribute) - #5368

Merged
qqeasonchen merged 1 commit into
developfrom
arch-guard/5357-mutable-frame
Sep 8, 2026
Merged

fix(#5357): make EventMeshFrame immutable (defensive copy + unmodifiable view + withAttribute)#5368
qqeasonchen merged 1 commit into
developfrom
arch-guard/5357-mutable-frame

Conversation

@qqeasonchen

Copy link
Copy Markdown
Contributor

What this PR does

Closes #5357 — Phase 0 enforcement of the production-HA plan (#5354): EventMeshFrame becomes deeply immutable. A frame that can be mutated after publish lets two concurrent dispatchers race on the same instance; the attribute map was also aliased from codec accumulators at construction.

1. EventMeshFrame changes

Aspect Before After
Constructor this.attrs = attrs — aliases the caller's map (codec accumulators could mutate a live frame) deep copy (new LinkedHashMap<>(attrs)); null map/data normalized
attributes() returns the internal mutable map Collections.unmodifiableMap(attrs)
Derivation mutate the returned map new withAttribute(name, value) — derives a new frame with one attribute set (null removes); source unchanged

2. Call-site migration

Verified by grep over all 72 attributes() usages — production code had zero mutation sites; exactly 3 test files mutated the returned map:

  • RestartCursorAlignmentTest / MqCursorRecordingTest: POP-cursor stamping now chains withAttribute
  • FrameProtocolConversionTest.framePopCkStampedForDeferredAck: converted from a mutability test to an immutability test (asserts withAttribute derives a stamped frame, source unchanged, attributes().put throws UnsupportedOperationException)

3. ArchUnit guardrail (14 → 15 rules)

  • ruleEventMeshFrameImmutable — only org.apache.eventmesh.common.wire.. may implement EventMeshFrame* types, so no other class can grow frame-mutating APIs.

Verification (full local CI parity, Temurin 21.0.11)

:eventmesh-architecture-guard:test + :eventmesh-common:test + :eventmesh-runtime:test  BUILD SUCCESSFUL
checkstyleMain/Test ×3 modules (maxWarnings=0)                                          BUILD SUCCESSFUL
./gradlew clean build dist jacocoTestReport (CI task set)                              BUILD SUCCESSFUL (11m12s)

Relations

Co-authored-by: qqeasonchen qqeasonchen@gmail.com

…ble view)

Phase 0 enforcement of the production-HA plan (#5354), sub-issue
#5357: a frame that can be mutated after publish lets two concurrent
dispatchers race on the same instance; the attribute map was also
aliased from codec accumulators at construction.

EventMeshFrame changes:
- constructor deep-copies the attrs map (a caller-held codec
  accumulator can no longer alias frame state); null map/data are
  normalized
- attributes() returns Collections.unmodifiableMap
- new withAttribute(name, value) derives a new frame with one
  attribute set (null value removes); the source frame is unchanged

Call-site migration (3 test files; production code had zero mutation
sites - verified by grep over all 72 attributes() usages):
- RestartCursorAlignmentTest / MqCursorRecordingTest: pop-cursor
  stamping now chains withAttribute
- FrameProtocolConversionTest.framePopCkStampedForDeferredAck:
  converted from a mutability test to an immutability test (asserts
  withAttribute derives a stamped frame, source unchanged, and
  attributes() throws UnsupportedOperationException on put)

ArchUnit guardrail (14 -> 15 rules):
- ruleEventMeshFrameImmutable: only org.apache.eventmesh.common.wire..
  may implement EventMeshFrame* types, so no other class can grow
  frame-mutating APIs

Full local CI parity (Temurin 21.0.11):
- :eventmesh-architecture-guard:test + :eventmesh-common:test +
  :eventmesh-runtime:test -> BUILD SUCCESSFUL
- checkstyleMain/Test x3 modules (maxWarnings=0)              -> BUILD SUCCESSFUL
- clean build dist jacocoTestReport (CI task set)            -> BUILD SUCCESSFUL (11m12s)

Refs #5357 (sub-issue of #5354).

Co-authored-by: qqeasonchen <qqeasonchen@gmail.com>
@qqeasonchen
qqeasonchen merged commit 7ac0ab8 into develop Sep 8, 2026
1 check passed
qqeasonchen added a commit that referenced this pull request Sep 8, 2026
…cess

Phase 0 enforcement of the production-HA plan (#5354), sub-issue
#5358: gauge-style quota resources (CONNECTIONS / SUBSCRIPTIONS /
BACKLOG) leak when acquire has no guaranteed release. Production
callers today (UniHttpServer x2, A2AGatewayHttpHandler,
ConnectorScheduler) all use one-shot THROUGHPUT check() and never
release - correct for windows, but nothing structural stops the next
gauge-style call site from leaking the same way.

New API (security.gate):
- QuotaHandle implements AutoCloseable: close() releases exactly once,
  double-close is a no-op, non-releasable (THROUGHPUT) handles do not
  release (window counters self-expire)
- SecurityGate.acquire(ctx, frame) -> QuotaHandle: runs the same
  auth/ACL-first check() then returns the paired handle; throws
  QuotaExceededException (new) when exhausted, consuming no slot
- callers wrap gauge work in try-with-resources so the release runs
  on success, failure and cancellation alike

ArchUnit guardrail (15 -> 16 rules):
- ruleQuotaManagerOnlyFromGate: QuotaManager.tryAcquire/release may
  only be called from the security.gate package - any direct manager
  call outside the gate bypasses the pairing contract and fails the
  build

Tests:
- QuotaHandleTest (new, 4 cases): close-releases-exactly-once,
  try-with-resources releases on exception, THROUGHPUT handle does
  not release, exhausted acquire throws without consuming
- checkstyleMain/Test + guard tests + full CI build all green

Rebased onto develop after #5368 (rule ordering in
ArchitectureRules.java); the 16-rule set now reads
#5322(9) + #5348(2) + #5356(2) + #5357(1) + #5358(1).

Full local CI parity (Temurin 21.0.11):
- checkstyle + tests x (runtime, architecture-guard) -> BUILD SUCCESSFUL
- clean build dist jacocoTestReport (CI task set)   -> BUILD SUCCESSFUL (7m59s)

Refs #5358 (sub-issue of #5354).

Co-authored-by: qqeasonchen <qqeasonchen@gmail.com>
qqeasonchen added a commit that referenced this pull request Sep 8, 2026
…cess (#5369)

Phase 0 enforcement of the production-HA plan (#5354), sub-issue
#5358: gauge-style quota resources (CONNECTIONS / SUBSCRIPTIONS /
BACKLOG) leak when acquire has no guaranteed release. Production
callers today (UniHttpServer x2, A2AGatewayHttpHandler,
ConnectorScheduler) all use one-shot THROUGHPUT check() and never
release - correct for windows, but nothing structural stops the next
gauge-style call site from leaking the same way.

New API (security.gate):
- QuotaHandle implements AutoCloseable: close() releases exactly once,
  double-close is a no-op, non-releasable (THROUGHPUT) handles do not
  release (window counters self-expire)
- SecurityGate.acquire(ctx, frame) -> QuotaHandle: runs the same
  auth/ACL-first check() then returns the paired handle; throws
  QuotaExceededException (new) when exhausted, consuming no slot
- callers wrap gauge work in try-with-resources so the release runs
  on success, failure and cancellation alike

ArchUnit guardrail (15 -> 16 rules):
- ruleQuotaManagerOnlyFromGate: QuotaManager.tryAcquire/release may
  only be called from the security.gate package - any direct manager
  call outside the gate bypasses the pairing contract and fails the
  build

Tests:
- QuotaHandleTest (new, 4 cases): close-releases-exactly-once,
  try-with-resources releases on exception, THROUGHPUT handle does
  not release, exhausted acquire throws without consuming
- checkstyleMain/Test + guard tests + full CI build all green

Rebased onto develop after #5368 (rule ordering in
ArchitectureRules.java); the 16-rule set now reads
#5322(9) + #5348(2) + #5356(2) + #5357(1) + #5358(1).

Full local CI parity (Temurin 21.0.11):
- checkstyle + tests x (runtime, architecture-guard) -> BUILD SUCCESSFUL
- clean build dist jacocoTestReport (CI task set)   -> BUILD SUCCESSFUL (7m59s)

Refs #5358 (sub-issue of #5354).
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.

[P0] arch-guard: ban mutable EventMeshFrame and unbounded attribute maps

1 participant