Skip to content

Commit 268b5cc

Browse files
committed
Improves GetAllExpirationAsync performance
Optimizes GetAllExpirationAsync in HybridCacheClient and InMemoryCacheClient by first checking the local cache and then retrieving any missed keys from the distributed cache to reduce distributed cache calls. Adds null checks and optimizes the handling of empty key lists to improve efficiency and prevent errors.
1 parent 8cd843b commit 268b5cc

File tree

2 files changed

+34
-33
lines changed

2 files changed

+34
-33
lines changed

src/Foundatio/Caching/HybridCacheClient.cs

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
4+
using System.Collections.ObjectModel;
45
using System.Linq;
56
using System.Threading;
67
using System.Threading.Tasks;
@@ -372,33 +373,35 @@ public async Task<bool> ExistsAsync(string key)
372373

373374
public async Task<IDictionary<string, TimeSpan?>> GetAllExpirationAsync(IEnumerable<string> keys)
374375
{
375-
if (keys == null)
376+
if (keys is null)
376377
throw new ArgumentNullException(nameof(keys));
377378

378-
var keyList = keys.ToList();
379-
var result = new Dictionary<string, TimeSpan?>(keyList.Count);
380-
var misses = new List<string>();
379+
string[] keysArray = keys.ToArray();
380+
if (keysArray.Length is 0)
381+
return ReadOnlyDictionary<string, TimeSpan?>.Empty;
381382

382-
foreach (var key in keyList)
383+
var localExpirations = await _localCache.GetAllExpirationAsync(keysArray).AnyContext();
384+
foreach (string key in localExpirations.Keys)
383385
{
384-
if (await _localCache.ExistsAsync(key).AnyContext())
385-
{
386-
var expiration = await _localCache.GetExpirationAsync(key).AnyContext();
387-
result[key] = expiration;
388-
}
389-
else
390-
{
391-
misses.Add(key);
392-
}
386+
_logger.LogTrace("Local cache hit: {Key}", key);
387+
Interlocked.Increment(ref _localCacheHits);
393388
}
394389

395-
if (misses.Count > 0)
390+
if (keysArray.Length == localExpirations.Count)
391+
return localExpirations;
392+
393+
// Get the missed keys from the distributed cache.
394+
string[] missedKeys = keysArray.Except(localExpirations.Keys).ToArray();
395+
foreach (string key in missedKeys)
396396
{
397-
var distributedExpirations = await _distributedCache.GetAllExpirationAsync(misses).AnyContext();
398-
foreach (var kvp in distributedExpirations)
399-
result[kvp.Key] = kvp.Value;
397+
_logger.LogTrace("Local cache miss: {Key}", key);
400398
}
401399

400+
var result = new Dictionary<string, TimeSpan?>(localExpirations);
401+
var distributedExpirations = await _distributedCache.GetAllExpirationAsync(missedKeys).AnyContext();
402+
foreach (var kvp in distributedExpirations)
403+
result[kvp.Key] = kvp.Value;
404+
402405
return result.AsReadOnly();
403406
}
404407

@@ -414,7 +417,7 @@ public async Task SetExpirationAsync(string key, TimeSpan expiresIn)
414417

415418
public async Task SetAllExpirationAsync(IDictionary<string, TimeSpan?> expirations)
416419
{
417-
if (expirations == null)
420+
if (expirations is null)
418421
throw new ArgumentNullException(nameof(expirations));
419422

420423
await _localCache.SetAllExpirationAsync(expirations).AnyContext();

src/Foundatio/Caching/InMemoryCacheClient.cs

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Concurrent;
33
using System.Collections.Generic;
4-
using System.Collections.ObjectModel;
54
using System.Linq;
65
using System.Reflection;
76
using System.Text.RegularExpressions;
@@ -955,12 +954,10 @@ public Task<bool> ExistsAsync(string key)
955954
throw new ArgumentNullException(nameof(keys));
956955

957956
var result = new Dictionary<string, TimeSpan?>();
958-
var utcNow = _timeProvider.GetUtcNow().UtcDateTime;
959-
960957
foreach (string key in keys)
961958
{
962959
if (String.IsNullOrEmpty(key))
963-
continue;
960+
throw new ArgumentNullException(nameof(key), "Key cannot be null or empty");
964961

965962
if (!_memory.TryGetValue(key, out var existingEntry))
966963
{
@@ -983,10 +980,9 @@ public Task<bool> ExistsAsync(string key)
983980
result[key] = existingEntry.ExpiresAt.Value.Subtract(_timeProvider.GetUtcNow().UtcDateTime);
984981
}
985982

986-
return Task.FromResult<IDictionary<string, TimeSpan?>>(new ReadOnlyDictionary<string, TimeSpan?>(result));
983+
return Task.FromResult<IDictionary<string, TimeSpan?>>(result.AsReadOnly());
987984
}
988985

989-
990986
public async Task SetExpirationAsync(string key, TimeSpan expiresIn)
991987
{
992988
if (String.IsNullOrEmpty(key))
@@ -1000,9 +996,9 @@ public async Task SetExpirationAsync(string key, TimeSpan expiresIn)
1000996
return;
1001997
}
1002998

1003-
Interlocked.Increment(ref _writes);
1004-
if (_memory.TryGetValue(key, out var existingEntry))
999+
if (_memory.TryGetValue(key, out var existingEntry) && existingEntry.ExpiresAt != expiresAt)
10051000
{
1001+
Interlocked.Increment(ref _writes);
10061002
existingEntry.ExpiresAt = expiresAt;
10071003
await StartMaintenanceAsync().AnyContext();
10081004
}
@@ -1013,7 +1009,7 @@ public async Task SetAllExpirationAsync(IDictionary<string, TimeSpan?> expiratio
10131009
if (expirations is null)
10141010
throw new ArgumentNullException(nameof(expirations));
10151011

1016-
if (expirations.Count == 0)
1012+
if (expirations.Count is 0)
10171013
return;
10181014

10191015
var utcNow = _timeProvider.GetUtcNow().UtcDateTime;
@@ -1022,16 +1018,17 @@ public async Task SetAllExpirationAsync(IDictionary<string, TimeSpan?> expiratio
10221018
foreach (var kvp in expirations)
10231019
{
10241020
if (String.IsNullOrEmpty(kvp.Key))
1025-
continue;
1021+
throw new ArgumentNullException(nameof(kvp.Key), "Key cannot be null or empty");
10261022

10271023
if (!_memory.TryGetValue(kvp.Key, out var existingEntry))
10281024
continue;
10291025

1030-
Interlocked.Increment(ref _writes);
1031-
10321026
if (kvp.Value is null)
10331027
{
1034-
// Null TimeSpan clears the expiration, making the key persistent
1028+
if (existingEntry.ExpiresAt is null)
1029+
continue;
1030+
1031+
Interlocked.Increment(ref _writes);
10351032
existingEntry.ExpiresAt = null;
10361033
updated++;
10371034
}
@@ -1042,8 +1039,9 @@ public async Task SetAllExpirationAsync(IDictionary<string, TimeSpan?> expiratio
10421039
{
10431040
RemoveExpiredKey(kvp.Key);
10441041
}
1045-
else
1042+
else if (existingEntry.ExpiresAt != expiresAt)
10461043
{
1044+
Interlocked.Increment(ref _writes);
10471045
existingEntry.ExpiresAt = expiresAt;
10481046
updated++;
10491047
}

0 commit comments

Comments
 (0)