Skip to content

Clarify expired-value semantics for non-conditional cache operations #571

Open

Description

@niemyjski

Review the pre-existing behavior of non-conditional operations on expired entries, especially numeric updates and unconditional replacement. Define the intended contract before changing those semantics, and add shared provider tests where applicable.

PR #570 handles conditional-operation races, rejected-update accounting, lost-lock renewal, and the large-list update investigation. The reproduced cloner crash is handled separately in lofcz/FastCloner#58; #570 keeps its vendored cloner unchanged. Its performance results and remaining storage/custom-sizing tradeoffs belong in that PR; this issue is not a reason to defer or approve those changes.

File splitting and decomposition are out of scope. Keep InMemoryCacheClient in its current file.

Hybrid local-cache concurrency remains #572. Automatic job cancellation remains outside #570; #26 tracks the broader concern.

Activity

  1. self-assigned this
    on Sep 22, 2026
  2. niemyjski commented on Sep 22, 2026

    @niemyjski
    MemberAuthor

    Suggested first step from the #570 review: now that entries are immutable, replace the per-site wasNewEntry/oldSize/newSize captured locals with one Upsert(key, addEntry, Func<CacheEntry, CacheEntry> update) helper (TryGetValue -> TryUpdate/TryAdd loop) that computes the memory delta from the (previous, current) entries after the swap succeeds, with a matching TryReplace for ReplaceIfEqual/SetExpiration/ListRemove. That deletes the retry-sensitive locals at ~10 sites (the class of bug fixed in SetInternalAsync in #570) and makes the double/long dedupe trivial.

  3. changed the title [-]Refactor InMemoryCacheClient: dedupe numeric/list operations and split the file[/-] [+]Refactor InMemoryCacheClient: split the file[/+] on Sep 22, 2026
  4. niemyjski commented on Sep 22, 2026

    @niemyjski
    MemberAuthor

    The review of #570 also found a few pre-existing issues (they're on main too) that fit with this cleanup:

    • SetExpirationAsync, IncrementAsync and SetIfHigherAsync/SetIfLowerAsync on an entry that has expired but hasn't been swept yet act on the stale value instead of treating the key as missing. AddAsync already treats an expired entry as absent; the other UpdateEntry callbacks should do the same.
    • ListAddAsync stores Size = -1 when a list grows past MaxEntrySize and throwing is disabled (CalculateEntrySize returns -1 in the update path).

    (IncrementAsync(long) keeping a stale size when overwriting a non-numeric value was also reported. #570 already fixes it, because the shared generic implementation recalculates the size.)

  5. niemyjski commented on Sep 24, 2026

    @niemyjski
    MemberAuthor

    PR #570 follow-up at f06ec7c fixes the small size-rejection/accounting defects while retaining this issue for broader decomposition and expired-value semantics. Correct file counts: 1,875 lines at the PR merge base, 1,484 after the original refactor, 1,486 after the targeted corrections.

    Please also track allocation optimization here. A BenchmarkDotNet default-job comparison (MemoryDiagnoser, M4 Max, .NET 10) confirms a 1,000-item list add/remove cycle with cloning/sizing disabled changes from 2.111 to 6.332 μs (3.00×), and 2.02 to 80.08 KB (39.73×). Copying occurs even with CloneValues=false, and CAS retries may repeat it. With cloning enabled, the exploratory comparison instead reduces allocation from 158.08 to 80.34 KB by avoiding repeated deep cloning. Scalar increments allocate 432 → 560 B; expiration updates 56 → 176 B.

    Optimize these costs without reintroducing mutable-entry publication, lost concurrent updates, or mutable list snapshots. The PR description contains the full validation scope and benchmark caveats. Hybrid concurrency stays in #572; automatic job cancellation stays outside this PR (#26).

  6. changed the title [-]Refactor InMemoryCacheClient: split the file[/-] [+]Review in-memory cache list allocation costs and expired-value semantics[/+] on Sep 24, 2026
  7. changed the title [-]Review in-memory cache list allocation costs and expired-value semantics[/-] [+]Clarify expired-value semantics for non-conditional cache operations[/+] on Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions