Skip to content

fix(audience): Json guards + DiskStore.Count cache + test-surface cleanup (SDK-239)#706

Merged
ImmutableJeffrey merged 4 commits into
mainfrom
feat/audience-hardening-cleanup
Apr 24, 2026
Merged

fix(audience): Json guards + DiskStore.Count cache + test-surface cleanup (SDK-239)#706
ImmutableJeffrey merged 4 commits into
mainfrom
feat/audience-hardening-cleanup

Conversation

@ImmutableJeffrey
Copy link
Copy Markdown
Collaborator

@ImmutableJeffrey ImmutableJeffrey commented Apr 24, 2026

Summary

  • Json.Serialize guards against cyclic / pathologically deep dicts. Both defences against StackOverflowException (uncatchable in .NET): MaxDepth = 64 cap and a ReferenceEquality visited-set carried through the recursive write. Sibling diamonds (same dict reachable via two independent keys) still serialise.
  • DiskStore.Count cache: seeded eagerly at construction, maintained by Write / Delete deltas via Interlocked.Add. Count() reads via Volatile.Read. Avoids a Directory.GetFiles scan per poll.
  • Rename CurrentConsentForTestingCurrentConsent — the accessor is the canonical internal read of the consent level, not a test-only affordance. Internal-only API; no SDK consumers break.

Closes SDK-239. Stacked on PR #705 — retarget this PR to main once #705 merges.

Test plan

  • dotnet test src/Packages/Audience/Tests/Audience.Tests.csproj passes.
  • New tests: Serialize_NestingExceedsMaxDepth_ThrowsFormatException, Serialize_SelfReferentialDict_ThrowsFormatException, Serialize_SharedChildInSiblingKeys_IsNotTreatedAsCycle.
  • Existing DiskStoreTests pass; Count() matches on-disk state after a Write / Delete sequence.

🤖 Generated with Claude Code

@ImmutableJeffrey ImmutableJeffrey force-pushed the feat/audience-hardening-cleanup branch from 954cb4f to 75b0081 Compare April 24, 2026 01:47
@ImmutableJeffrey ImmutableJeffrey changed the base branch from main to feat/audience-diagnostic-surface April 24, 2026 01:47
@ImmutableJeffrey ImmutableJeffrey force-pushed the feat/audience-diagnostic-surface branch from 85c7b38 to 73d3e46 Compare April 24, 2026 01:51
@ImmutableJeffrey ImmutableJeffrey force-pushed the feat/audience-hardening-cleanup branch from 75b0081 to 3184bd5 Compare April 24, 2026 01:51
Comment thread src/Packages/Audience/Runtime/Transport/DiskStore.cs Outdated
Base automatically changed from feat/audience-diagnostic-surface to main April 24, 2026 02:30
@ImmutableJeffrey ImmutableJeffrey force-pushed the feat/audience-hardening-cleanup branch from 3184bd5 to 46c433e Compare April 24, 2026 02:32
ImmutableJeffrey and others added 4 commits April 24, 2026 12:37
…eep dicts

Two defences against StackOverflowException (uncatchable in .NET).

- MaxDepth = 64 — WriteObject/WriteArray throw FormatException past
  the cap rather than recursing.
- ReferenceEquality visited-set — self-referential or shared-
  instance containers throw on re-entry. Sibling diamonds (same
  dict under two independent keys) are NOT cycles; enforced by
  visited.Remove after the recursive write returns.

HashSet<object> is allocated lazily, so scalar-only payloads pay
nothing. No behaviour change for any acyclic, shallow caller.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both guards exist because their absence yields StackOverflowException,
which the .NET runtime does not catch — the process terminates. Neither
guard had a test. Three new tests:

- nesting one level past MaxDepth throws FormatException with "nesting
  exceeds" message. Sabotage: disabling GuardDepth fails the test.
- self-referential dict throws FormatException with "cycle" message.
  Sabotage: disabling the visited.Add(container) check makes the test
  recurse into the depth guard and the assertion on "cycle" text fails.
- shared-child diamond (same dict in two sibling keys) is not treated as
  a cycle. Sabotage: removing the post-recursion visited.Remove makes
  the second sibling throw "cycle".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sample diagnostics panels poll QueueSize on a UI tick — at 10 Hz with
a non-trivial spool that's a continuous Directory.GetFiles scan for a
value that barely changes. Cache the count instead.

- Seed at construction from the existing spool so first poll is
  correct without a pre-mutation branch. Lazy-seeding on first bump
  had a double-count bug: the seed scan already saw the just-written
  file, then the +1 delta pushed the cache past the real on-disk
  count.
- Write / Delete / DeleteAll / ApplyAnonymousDowngrade maintain the
  delta via BumpCount (Interlocked.Add) and TryDelete now returns a
  bool so "actually removed" vs "already gone" can drive the
  decrement.
- TryDelete folds DirectoryNotFoundException into the true
  (idempotent) branch — per the MS docs it fires on invalid paths
  (example: "unmapped drive"), and an unreachable path can't point
  to a real file — and keeps IOException / UnauthorizedAccessException
  in the false branch so the cache stays honest when a file survived
  the delete attempt. FileNotFoundException is deliberately absent:
  File.Delete silently succeeds when the file is gone, so there is
  nothing to catch.
- Count() reads through Volatile.Read so a caller on a different
  thread sees the latest published value.

Cache invariant: on-disk-count-at-ctor + Σ tracked deltas. Events
written outside the DiskStore API (direct File.* calls from tests
that plant fixtures) are not tracked and will drift the cache; such
tests assert on the post-op filesystem state, not Count().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "ForTesting" suffix signalled a test-only affordance, but the
accessor is the canonical internal read of the consent level and is
used the same way inside the SDK. Drop the suffix.

Internal-only API — no SDK consumers break.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ImmutableJeffrey ImmutableJeffrey force-pushed the feat/audience-hardening-cleanup branch from 46c433e to ba5f256 Compare April 24, 2026 02:37
@ImmutableJeffrey ImmutableJeffrey marked this pull request as ready for review April 24, 2026 02:47
@ImmutableJeffrey ImmutableJeffrey requested review from a team as code owners April 24, 2026 02:47
@ImmutableJeffrey ImmutableJeffrey merged commit e9ad00a into main Apr 24, 2026
19 checks passed
@ImmutableJeffrey ImmutableJeffrey deleted the feat/audience-hardening-cleanup branch April 24, 2026 02:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

2 participants