Skip to content

Alternate-key cache support for ConcurrentLru#739

Draft
Copilot wants to merge 18 commits intomainfrom
copilot/add-alternate-cache-from-concurrentlru
Draft

Alternate-key cache support for ConcurrentLru#739
Copilot wants to merge 18 commits intomainfrom
copilot/add-alternate-cache-from-concurrentlru

Conversation

Copy link

Copilot AI commented Mar 24, 2026

This pulls the alternate-key cache work for ConcurrentLru out of PR #638 into its own reviewable change. It adds a net9-only API for alternate lookups, removals, updates, and add-or-get/update flows on ConcurrentLru when the configured comparer implements IAlternateEqualityComparer<TAlternateKey, TKey>.

  • Alternate lookup API

    • Add GetAlternateLookup<TAlternateKey>() and TryGetAlternateLookup<TAlternateKey>(...) to ConcurrentLruCore.
    • Expose a top-level IAlternateLookup<TAlternateKey, TKey, TValue> in the BitFaster.Caching namespace with:
      • TryGet
      • TryRemove
      • TryUpdate
      • AddOrUpdate
      • GetOrAdd
      • GetOrAdd<TArg>
  • ConcurrentDictionary alternate lookup integration

    • Use ConcurrentDictionary<TKey, TValue>.GetAlternateLookup<TAlternateKey>() on net9 to perform alternate-key operations without first materializing the primary key on cache hits.
    • Cache the underlying ConcurrentDictionary alternate lookup adapter for repeated alternate-key operations.
    • Centralize the shared value-update logic so both primary-key and alternate-key TryUpdate paths preserve the same update semantics.
    • Cache the converted primary key across AddOrUpdate retries to avoid repeated alternate-key materialization under contention.
    • On misses, convert the alternate key to the primary key via IAlternateEqualityComparer<TAlternateKey, TKey>.Create(...) before falling back to the existing add/update paths.
    • Preserve current cache semantics for telemetry, eviction, update notifications, disposal, and removal callbacks.
  • Compatibility guardrails

    • Fail fast for unsupported alternate key types with Throw.IncompatibleComparer().
    • Keep the feature behind NET9_0_OR_GREATER so the public surface remains unchanged on earlier targets.
  • Targeted coverage

    • Add focused tests for:
      • compatible comparer alternate lookup
      • incompatible comparer rejection
      • alternate-key removal returning the actual stored key
      • TryUpdate behavior with alternate keys
      • AddOrUpdate behavior with alternate keys
      • GetOrAdd behavior with alternate keys
    • Use ReadOnlySpan<char> as the positive alternate key in tests with StringComparer.Ordinal.

Example:

var cache = new ConcurrentLru<string, string>(1, 3, StringComparer.Ordinal);

var alt = cache.GetAlternateLookup<ReadOnlySpan<char>>();

string value = alt.GetOrAdd("42".AsSpan(), key => $"value-{key.ToString()}");
alt.AddOrUpdate("42".AsSpan(), "updated");
bool updated = alt.TryUpdate("42".AsSpan(), "updated-again");
bool found = alt.TryGet("42".AsSpan(), out var existing);
bool removed = alt.TryRemove("42".AsSpan(), out var actualKey, out var removedValue);

📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.

Copilot AI and others added 3 commits March 24, 2026 05:18
Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/766875ac-21b6-44fa-a3cb-3b38b9beed94
Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/766875ac-21b6-44fa-a3cb-3b38b9beed94
Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/766875ac-21b6-44fa-a3cb-3b38b9beed94
@bitfaster
Copy link
Owner

@copilot similar to GetAlternateCache, add a method for TryGetAlternateCache

Copy link
Author

Copilot AI commented Mar 24, 2026

@copilot similar to GetAlternateCache, add a method for TryGetAlternateCache

Added in 554fe2fConcurrentLruCore now includes TryGetAlternateCache<TAlternateKey>(...) alongside GetAlternateCache<TAlternateKey>().

Copilot AI requested a review from bitfaster March 24, 2026 05:33
@bitfaster
Copy link
Owner

@copilot extract the interface IAlternateCache into a separate file in the top level namespace.

Copy link
Author

Copilot AI commented Mar 24, 2026

@copilot extract the interface IAlternateCache into a separate file in the top level namespace.

Done in fa5b42cIAlternateCache<TAlternateKey, TKey, TValue> is now in its own file under the top-level BitFaster.Caching namespace.

@coveralls
Copy link

coveralls commented Mar 24, 2026

Coverage Status

coverage: 98.927% (-0.06%) from 98.99%
when pulling 07526ab on copilot/add-alternate-cache-from-concurrentlru
into fc6739c on main.

@bitfaster
Copy link
Owner

@copilot in the positive unit tests. use ReadOnlySpan<char> as the alternate key, instead of integer and the AlternateIntStringComparer. The AlternateIntStringComparer can then be removed.

Copy link
Author

Copilot AI commented Mar 24, 2026

@copilot in the positive unit tests. use ReadOnlySpan<char> as the alternate key, instead of integer and the AlternateIntStringComparer. The AlternateIntStringComparer can then be remove...

Updated in 51324ab — the positive tests now use ReadOnlySpan<char> with StringComparer.Ordinal, and the AlternateIntStringComparer helper has been removed.

@bitfaster
Copy link
Owner

@copilot implement this feedback: go with "AlternateLookup" naming as opposed to "AlternateCache" as it is an adapter providing an alternative lookup over an existing cache, not an alternative cache as such. Also, "AlernativeLookup" is the established name in the .NET ecosystem.

Also, the alternate lookup on the underlying ConcurrentDictionary<K, V> should be retrieved and cached in a private field of the AlternateLookup struct, as opposed to calling this.Lru.dictionary.GetAlternateLookup() on each TryGet/TryRemove/GetOrAdd invocation.

Copy link
Author

Copilot AI commented Mar 24, 2026

@copilot implement this feedback: go with "AlternateLookup" naming as opposed to "AlternateCache" as it is an adapter providing an alternative lookup over an existing cache, not an alternative...

Updated in b303547 — the API and tests now use AlternateLookup naming throughout, including IAlternateLookup, GetAlternateLookup, and TryGetAlternateLookup, aligned with the underlying ConcurrentDictionary terminology.


public bool TryRemove(TAlternateKey key, [MaybeNullWhen(false)] out K actualKey, [MaybeNullWhen(false)] out V value)
{
var alternate = this.Lru.dictionary.GetAlternateLookup<TAlternateKey>();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot can you make a new iteration where the AlternateLookup returned on this line is cached as a member variable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 75d5105 — the AlternateLookup<TAlternateKey> adapter now caches the underlying ConcurrentDictionary<K, I>.AlternateLookup<TAlternateKey> as a member and reuses it for TryGet/TryRemove.

/// <param name="factoryArgument">The factory argument.</param>
/// <returns>The cached value.</returns>
TValue GetOrAdd<TArg>(TAlternateKey key, Func<TAlternateKey, TArg, TValue> valueFactory, TArg factoryArgument);
}
Copy link
Owner

@bitfaster bitfaster Mar 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • TryAdd
  • AddOrUpdate

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot add TryAdd and AddOrUpdate and implement them for ConcurrentLruCore. Add tests to verify they function correctly.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 3e40ba9.

Copilot AI requested a review from bitfaster March 24, 2026 18:55
/// <param name="key">The alternate key.</param>
/// <param name="value">The value to add.</param>
/// <returns><see langword="true" /> when the key was added; otherwise, <see langword="false" />.</returns>
bool TryAdd(TAlternateKey key, TValue value);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot change this to TryUpdate

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 64275ae.

Copilot AI requested a review from bitfaster March 24, 2026 19:35
Copilot AI requested a review from bitfaster March 24, 2026 20:38
return false;
}

public bool TryAdd(TAlternateKey key, V value)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot delete the TryAdd method because it is not part of ICache. Remove it from IAlternateLookup and also remove the associated tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 5420437.

Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/8911400c-2c01-4911-8e85-8bc70df76ef5
Copilot AI requested a review from bitfaster March 25, 2026 00:06
return false;
}

internal readonly struct AlternateLookup<TAlternateKey> : IAlternateLookup<TAlternateKey, K, V>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot implement AddOrUpdate as part of AlternateLookup. Add this new method to the IAlternateLookup interface and provide unit tests.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented in 023c085.

Copilot AI and others added 2 commits March 25, 2026 01:42
Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/43665a27-197b-4c1d-9b27-87101805f538
Co-authored-by: bitfaster <12851828+bitfaster@users.noreply.github.com>
Agent-Logs-Url: https://github.com/bitfaster/BitFaster.Caching/sessions/43665a27-197b-4c1d-9b27-87101805f538
Copilot AI requested a review from bitfaster March 25, 2026 01:48
@bitfaster bitfaster changed the title Extract net9 alternate-key cache support for ConcurrentLru Alternate-key cache support for ConcurrentLru Mar 26, 2026
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.

[Feature request] Add alternate lookup support

3 participants