/// <summary> /// Creates a Firestore document that stores the cache value and its /// expiration info, but does not store it in Firestore. /// </summary> /// <param name="value">The value to be stored in the cache.</param> /// <param name="options">Expiration info.</param> /// <returns></returns> private CacheDoc MakeCacheDoc(byte[] value, DistributedCacheEntryOptions options) { CacheDoc doc = new CacheDoc() { LastRefresh = _clock.GetCurrentDateTimeUtc(), Value = value, }; if (options.SlidingExpiration.HasValue) { doc.SlidingExpirationSeconds = options.SlidingExpiration.Value.TotalSeconds; } else { doc.SlidingExpirationSeconds = _forever.TotalSeconds; } if (options.AbsoluteExpiration.HasValue) { doc.AbsoluteExpiration = options.AbsoluteExpiration.Value.UtcDateTime; } if (options.AbsoluteExpirationRelativeToNow.HasValue) { doc.AbsoluteExpiration = _clock.GetCurrentDateTimeUtc() + options.AbsoluteExpirationRelativeToNow.Value; } return(doc); }
/// <summary> /// Extracts the cache value from a Firestore document snapshot. /// Checks all the expiration fields so never returns a stale value. /// </summary> /// <param name="snapshot">The snapshot to pull the cache value from.</param> /// <returns>The cache value or null.</returns> private byte[] ValueFromSnapshot(DocumentSnapshot snapshot) { if (!snapshot.Exists) { return(null); } CacheDoc doc = snapshot.ConvertTo <CacheDoc>(); var now = _clock.GetCurrentDateTimeUtc(); if (doc.AbsoluteExpiration < now) { return(null); } var slidingExpiration = doc.LastRefresh.GetValueOrDefault() + TimeSpan.FromSeconds(doc.SlidingExpirationSeconds.GetValueOrDefault()); if (slidingExpiration < now) { return(null); } return(doc.Value); }
/// <summary> /// Scans the Firestore collection for expired cache entries and /// deletes them. /// </summary> /// <param name="token">A cancellation token.</param> public async Task CollectGarbageAsync(CancellationToken token) { _logger.LogTrace("Begin garbage collection."); // Purge entries whose AbsoluteExpiration has passed. const int pageSize = 40; int batchSize; // Batches of cache entries to be deleted. var now = _clock.GetCurrentDateTimeUtc(); do { QuerySnapshot querySnapshot = await _cacheEntries.OrderByDescending("AbsoluteExpiration") .StartAfter(now) .Limit(pageSize) .GetSnapshotAsync(token) .ConfigureAwait(false); batchSize = 0; WriteBatch writeBatch = _cacheEntries.Database.StartBatch(); foreach (DocumentSnapshot docSnapshot in querySnapshot.Documents) { if (docSnapshot.ConvertTo <CacheDoc>().AbsoluteExpiration.HasValue) { writeBatch.Delete(docSnapshot.Reference, Precondition.LastUpdated( docSnapshot.UpdateTime.GetValueOrDefault())); batchSize += 1; } } if (batchSize > 0) { _logger.LogDebug("Collecting {0} cache entries.", batchSize); await writeBatch.CommitAsync(token).ConfigureAwait(false); } token.ThrowIfCancellationRequested(); } while (batchSize == pageSize); // Purge entries whose SlidingExpiration has passed. do { QuerySnapshot querySnapshot = await _cacheEntries.OrderBy("LastRefresh") .Limit(pageSize) .GetSnapshotAsync(token) .ConfigureAwait(false); batchSize = 0; WriteBatch writeBatch = _cacheEntries.Database.StartBatch(); foreach (DocumentSnapshot docSnapshot in querySnapshot.Documents) { CacheDoc doc = docSnapshot.ConvertTo <CacheDoc>(); if (doc.SlidingExpirationSeconds.HasValue) { var slidingExpiration = doc.LastRefresh.GetValueOrDefault() + TimeSpan.FromSeconds(doc.SlidingExpirationSeconds.Value); if (slidingExpiration < now) { writeBatch.Delete(docSnapshot.Reference, Precondition.LastUpdated( docSnapshot.UpdateTime.GetValueOrDefault())); batchSize += 1; } } } if (batchSize > 0) { _logger.LogDebug("Collecting {batchSize} cache entries.", batchSize); await writeBatch.CommitAsync(token).ConfigureAwait(false); } token.ThrowIfCancellationRequested(); } while (batchSize > 0); _logger.LogTrace("End garbage collection."); }