Skip to content

Commit

Permalink
Adapt time-related CacheEntry properties and backing storage.
Browse files Browse the repository at this point in the history
  • Loading branch information
edevoogd committed May 15, 2021
1 parent 9376f4f commit aafd8b6
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 44 deletions.
42 changes: 28 additions & 14 deletions src/libraries/Microsoft.Extensions.Caching.Memory/src/CacheEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ internal sealed partial class CacheEntry : ICacheEntry

private CacheEntryTokens _tokens; // might be null if user is not using the tokens or callbacks
private TimeSpan? _absoluteExpirationRelativeToNow;
private TimeSpan? _slidingExpiration;
private long? _size;
private CacheEntry _previous; // this field is not null only before the entry is added to the cache
private object _value;
Expand All @@ -35,7 +34,14 @@ internal CacheEntry(object key, MemoryCache memoryCache)
/// <summary>
/// Gets or sets an absolute expiration date for the cache entry.
/// </summary>
public DateTimeOffset? AbsoluteExpiration { get; set; }
public DateTimeOffset? AbsoluteExpiration
{
get => _absoluteExpirationClockOffset.HasValue ? _cache.ClockQuantizer.ClockOffsetToUtcDateTimeOffset(_absoluteExpirationClockOffset!.Value) : null;
set
{
_absoluteExpirationClockOffset = !value.HasValue ? null : _cache.ClockQuantizer.DateTimeOffsetToClockOffset(value!.Value);
}
}

/// <summary>
/// Gets or sets an absolute expiration time, relative to now.
Expand Down Expand Up @@ -66,18 +72,25 @@ public TimeSpan? AbsoluteExpirationRelativeToNow
/// </summary>
public TimeSpan? SlidingExpiration
{
get => _slidingExpiration;
get => !_slidingExpirationClockOffsetUnits.HasValue ? null : _cache.ClockQuantizer.ClockOffsetUnitsToTimeSpan(_slidingExpirationClockOffsetUnits!.Value);
set
{
if (value <= TimeSpan.Zero)
if (value.HasValue)
{
throw new ArgumentOutOfRangeException(
nameof(SlidingExpiration),
value,
"The sliding expiration value must be positive.");
}
if (value <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(
nameof(SlidingExpiration),
value,
"The sliding expiration value must be positive.");
}

_slidingExpiration = value;
_slidingExpirationClockOffsetUnits = _cache.ClockQuantizer.TimeSpanToClockOffsetUnits(value!.Value);
}
else
{
_slidingExpirationClockOffsetUnits = null;
}
}
}

Expand Down Expand Up @@ -126,7 +139,8 @@ public object Value
}
}

internal DateTimeOffset LastAccessed { get; set; }
// Note that this is a "rounded-down" value for entries without sliding and/or absolute expiration based on CurrentInterval.ClockOffset at the time of last access.
internal DateTimeOffset LastAccessed => _cache.ClockQuantizer.ClockOffsetToUtcDateTimeOffset(_lastAccessedClockOffsetSerialPosition.ClockOffset);

internal EvictionReason EvictionReason { get => _state.EvictionReason; private set => _state.EvictionReason = value; }

Expand Down Expand Up @@ -174,7 +188,7 @@ internal void SetExpired(EvictionReason reason)
[MethodImpl(MethodImplOptions.AggressiveInlining)] // added based on profiling
private bool CheckForExpiredTime(in DateTimeOffset now)
{
if (!AbsoluteExpiration.HasValue && !_slidingExpiration.HasValue)
if (!AbsoluteExpiration.HasValue && !SlidingExpiration.HasValue)
{
return false;
}
Expand All @@ -189,8 +203,8 @@ bool FullCheck(in DateTimeOffset offset)
return true;
}

if (_slidingExpiration.HasValue
&& (offset - LastAccessed) >= _slidingExpiration)
if (SlidingExpiration.HasValue
&& (offset - LastAccessed) >= SlidingExpiration)
{
SetExpired(EvictionReason.Expired);
return true;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

namespace Microsoft.Extensions.Caching.Memory
{
internal partial class CacheEntry : ICacheEntry
{
private long? _slidingExpirationClockOffsetUnits;


private Internal.ClockQuantization.LazyClockOffsetSerialPosition _lastAccessedClockOffsetSerialPosition;
internal Internal.ClockQuantization.LazyClockOffsetSerialPosition LastAccessedClockOffsetSerialPosition
{
get => _lastAccessedClockOffsetSerialPosition;
set
{
_lastAccessedClockOffsetSerialPosition = value;
}
}


private long? _absoluteExpirationClockOffset;
}
}

#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
Expand All @@ -29,6 +28,8 @@ public class MemoryCache : IMemoryCache
private bool _disposed;
private DateTimeOffset _lastExpirationScan;

internal readonly Internal.ClockQuantization.ClockQuantizer ClockQuantizer;

/// <summary>
/// Creates a new <see cref="MemoryCache"/> instance.
/// </summary>
Expand Down Expand Up @@ -58,12 +59,11 @@ public MemoryCache(IOptions<MemoryCacheOptions> optionsAccessor, ILoggerFactory

_entries = new ConcurrentDictionary<object, CacheEntry>();

if (_options.Clock == null)
{
_options.Clock = new SystemClock();
}
var clock = Internal.ClockQuantization.SystemClock.Create(_options.Clock);
ClockQuantizer = new Internal.ClockQuantization.ClockQuantizer(clock, _options.ExpirationScanFrequency);

_lastExpirationScan = _options.Clock.UtcNow;
var start = ClockQuantizer.Advance().ClockOffset;
_lastExpirationScan = ClockQuantizer.ClockOffsetToUtcDateTimeOffset(start);
}

/// <summary>
Expand Down Expand Up @@ -103,12 +103,17 @@ internal void SetEntry(CacheEntry entry)
throw new InvalidOperationException(SR.Format(SR.CacheEntryHasEmptySize, nameof(entry.Size), nameof(_options.SizeLimit)));
}

DateTimeOffset utcNow = _options.Clock.UtcNow;
// Determine exact time only when (potentially) needed
Internal.ClockQuantization.LazyClockOffsetSerialPosition position = default;
if (entry.SlidingExpiration.HasValue || entry.AbsoluteExpiration.HasValue || entry.AbsoluteExpirationRelativeToNow.HasValue)
{
ClockQuantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: true);
}

DateTimeOffset? absoluteExpiration = null;
if (entry.AbsoluteExpirationRelativeToNow.HasValue)
{
absoluteExpiration = utcNow + entry.AbsoluteExpirationRelativeToNow;
absoluteExpiration = ClockQuantizer.ClockOffsetToUtcDateTimeOffset(position.ClockOffset) + entry.AbsoluteExpirationRelativeToNow;
}
else if (entry.AbsoluteExpiration.HasValue)
{
Expand All @@ -127,7 +132,8 @@ internal void SetEntry(CacheEntry entry)
}

// Initialize the last access timestamp at the time the entry is added
entry.LastAccessed = utcNow;
ClockQuantizer.EnsureInitializedClockOffsetSerialPosition(ref position);
entry.LastAccessedClockOffsetSerialPosition = position;

if (_entries.TryGetValue(entry.Key, out CacheEntry priorEntry))
{
Expand All @@ -136,7 +142,7 @@ internal void SetEntry(CacheEntry entry)

bool exceedsCapacity = UpdateCacheSizeExceedsCapacity(entry);

if (!entry.CheckExpired(utcNow) && !exceedsCapacity)
if (!entry.CheckExpired(ClockQuantizer.ClockOffsetToUtcDateTimeOffset(position.ClockOffset)) && !exceedsCapacity)
{
bool entryAdded = false;

Expand Down Expand Up @@ -212,7 +218,7 @@ internal void SetEntry(CacheEntry entry)
}
}

StartScanForExpiredItemsIfNeeded(utcNow);
StartScanForExpiredItemsIfNeeded(ref position);
}

/// <inheritdoc />
Expand All @@ -221,15 +227,24 @@ public bool TryGetValue(object key, out object result)
ValidateCacheKey(key);
CheckDisposed();

DateTimeOffset utcNow = _options.Clock.UtcNow;
Internal.ClockQuantization.LazyClockOffsetSerialPosition position = default;

if (_entries.TryGetValue(key, out CacheEntry entry))
{
if (entry.SlidingExpiration.HasValue || entry.AbsoluteExpiration.HasValue)
{
ClockQuantizer.EnsureInitializedExactClockOffsetSerialPosition(ref position, advance: true);
}
else
{
ClockQuantizer.EnsureInitializedClockOffsetSerialPosition(ref position);
}

// Check if expired due to expiration tokens, timers, etc. and if so, remove it.
// Allow a stale Replaced value to be returned due to concurrent calls to SetExpired during SetEntry.
if (!entry.CheckExpired(utcNow) || entry.EvictionReason == EvictionReason.Replaced)
if (!entry.CheckExpired(ClockQuantizer.ClockOffsetToUtcDateTimeOffset(position.ClockOffset)) || entry.EvictionReason == EvictionReason.Replaced)
{
entry.LastAccessed = utcNow;
entry.LastAccessedClockOffsetSerialPosition = position;
result = entry.Value;

if (entry.CanPropagateOptions())
Expand All @@ -239,7 +254,7 @@ public bool TryGetValue(object key, out object result)
entry.PropagateOptions(CacheEntryHelper.Current);
}

StartScanForExpiredItemsIfNeeded(utcNow);
StartScanForExpiredItemsIfNeeded(ref position);

return true;
}
Expand All @@ -250,7 +265,7 @@ public bool TryGetValue(object key, out object result)
}
}

StartScanForExpiredItemsIfNeeded(utcNow);
StartScanForExpiredItemsIfNeeded(ref position);

result = null;
return false;
Expand All @@ -273,7 +288,8 @@ public void Remove(object key)
entry.InvokeEvictionCallbacks();
}

StartScanForExpiredItemsIfNeeded(_options.Clock.UtcNow);
Internal.ClockQuantization.LazyClockOffsetSerialPosition now = default;
StartScanForExpiredItemsIfNeeded(ref now);
}

private void RemoveEntry(CacheEntry entry)
Expand All @@ -292,30 +308,40 @@ internal void EntryExpired(CacheEntry entry)
{
// TODO: For efficiency consider processing these expirations in batches.
RemoveEntry(entry);
StartScanForExpiredItemsIfNeeded(_options.Clock.UtcNow);

Internal.ClockQuantization.LazyClockOffsetSerialPosition now = default;
StartScanForExpiredItemsIfNeeded(ref now);
}

// Called by multiple actions to see how long it's been since we last checked for expired items.
// If sufficient time has elapsed then a scan is initiated on a background task.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void StartScanForExpiredItemsIfNeeded(DateTimeOffset utcNow)
private void StartScanForExpiredItemsIfNeeded(ref Internal.ClockQuantization.LazyClockOffsetSerialPosition position)
{
if (_options.ExpirationScanFrequency < utcNow - _lastExpirationScan)
{
ScheduleTask(utcNow);
}
DateTimeOffset utcNow = ClockQuantizer.ClockOffsetToUtcDateTimeOffset(position.IsExact ? position.ClockOffset : ClockQuantizer.UtcNowClockOffset);
DateTimeOffsetBasedStartScanForExpiredItemsIfNeeded(utcNow);

void ScheduleTask(DateTimeOffset utcNow)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void DateTimeOffsetBasedStartScanForExpiredItemsIfNeeded(DateTimeOffset utcNow)
{
_lastExpirationScan = utcNow;
Task.Factory.StartNew(state => ScanForExpiredItems((MemoryCache)state), this,
CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
if (_options.ExpirationScanFrequency < utcNow - _lastExpirationScan)
{
ScheduleTask(utcNow);
}

void ScheduleTask(DateTimeOffset utcNow)
{
_lastExpirationScan = utcNow;
Task.Factory.StartNew(state => ScanForExpiredItems((MemoryCache)state), this,
CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
}
}
}

private static void ScanForExpiredItems(MemoryCache cache)
{
DateTimeOffset now = cache._lastExpirationScan = cache._options.Clock.UtcNow;
DateTimeOffset now = cache._lastExpirationScan = cache.ClockQuantizer.UtcNow;

foreach (CacheEntry entry in cache._entries.Values)
{
if (entry.CheckExpired(now))
Expand Down Expand Up @@ -398,7 +424,7 @@ private void Compact(long removalSizeTarget, Func<CacheEntry, long> computeEntry
long removedSize = 0;

// Sort items by expired & priority status
DateTimeOffset now = _options.Clock.UtcNow;
DateTimeOffset now = ClockQuantizer.UtcNow;
foreach (CacheEntry entry in _entries.Values)
{
if (entry.CheckExpired(now))
Expand Down Expand Up @@ -454,7 +480,7 @@ static void ExpirePriorityBucket(ref long removedSize, long removalSizeTarget, F
// TODO: Refine policy

// LRU
priorityEntries.Sort((e1, e2) => e1.LastAccessed.CompareTo(e2.LastAccessed));
priorityEntries.Sort((e1, e2) => Compare(e1.LastAccessedClockOffsetSerialPosition, e2.LastAccessedClockOffsetSerialPosition));
foreach (CacheEntry entry in priorityEntries)
{
entry.SetExpired(EvictionReason.Capacity);
Expand All @@ -466,6 +492,17 @@ static void ExpirePriorityBucket(ref long removedSize, long removalSizeTarget, F
break;
}
}

static int Compare(Internal.ClockQuantization.LazyClockOffsetSerialPosition t1, Internal.ClockQuantization.LazyClockOffsetSerialPosition t2)
{
int result = t1.ClockOffset.CompareTo(t2.ClockOffset);
if (result == 0)
{
return t1.SerialPosition.CompareTo(t2.SerialPosition);
}

return result;
}
}
}

Expand Down

0 comments on commit aafd8b6

Please sign in to comment.