Bound how long the blog cache serves stale entries
A failed fetch fell back to the cached value with no limit, so an unreachable blog API kept the post list rendering while every uncached slug returned 404. Stale entries now survive an hour past their TTL, then get dropped and logged at error so the outage surfaces the same way for every key. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
769c7819e0
commit
392b9ec212
@@ -1,4 +1,4 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using JoshHeaps.Net.Models;
|
using JoshHeaps.Net.Models;
|
||||||
using JoshHeaps.Net.Services.Interfaces;
|
using JoshHeaps.Net.Services.Interfaces;
|
||||||
@@ -10,6 +10,7 @@ public class BlogService : IBlogService
|
|||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly ILogger<BlogService> _logger;
|
private readonly ILogger<BlogService> _logger;
|
||||||
private readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(5);
|
private readonly TimeSpan _cacheTtl = TimeSpan.FromMinutes(5);
|
||||||
|
private readonly TimeSpan _staleGrace = TimeSpan.FromHours(1);
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
|
private readonly ConcurrentDictionary<string, CacheEntry> _cache = new();
|
||||||
|
|
||||||
@@ -65,7 +66,9 @@ public class BlogService : IBlogService
|
|||||||
|
|
||||||
private async Task<T?> GetCachedAsync<T>(string key, Func<Task<T?>> factory) where T : class
|
private async Task<T?> GetCachedAsync<T>(string key, Func<Task<T?>> factory) where T : class
|
||||||
{
|
{
|
||||||
if (_cache.TryGetValue(key, out var entry) && entry.ExpiresAt > DateTime.UtcNow)
|
_cache.TryGetValue(key, out var entry);
|
||||||
|
|
||||||
|
if (entry is not null && entry.ExpiresAt > DateTime.UtcNow)
|
||||||
return (T?)entry.Value;
|
return (T?)entry.Value;
|
||||||
|
|
||||||
var result = await factory();
|
var result = await factory();
|
||||||
@@ -73,15 +76,26 @@ public class BlogService : IBlogService
|
|||||||
if (result is not null)
|
if (result is not null)
|
||||||
{
|
{
|
||||||
_cache[key] = new CacheEntry(result, DateTime.UtcNow.Add(_cacheTtl));
|
_cache[key] = new CacheEntry(result, DateTime.UtcNow.Add(_cacheTtl));
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
else if (entry is not null)
|
|
||||||
|
return entry is null ? null : ServeStale<T>(key, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stale entries are dropped once the grace window closes so that a dead API fails the same
|
||||||
|
// way for every key. Serving them indefinitely let a cached post list render next to 404s on
|
||||||
|
// the posts themselves, which reads as a site bug rather than an outage.
|
||||||
|
private T? ServeStale<T>(string key, CacheEntry entry) where T : class
|
||||||
|
{
|
||||||
|
if (entry.ExpiresAt.Add(_staleGrace) > DateTime.UtcNow)
|
||||||
{
|
{
|
||||||
// API unreachable — serve stale cache
|
_logger.LogWarning("Blog API unreachable, serving stale cache for key {Key}", key);
|
||||||
_logger.LogWarning("Serving stale cache for key {Key}", key);
|
|
||||||
return (T?)entry.Value;
|
return (T?)entry.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
_cache.TryRemove(key, out _);
|
||||||
|
_logger.LogError("Blog API unreachable for over {StaleGrace}, dropping stale cache for key {Key}", _staleGrace, key);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearCache()
|
public void ClearCache()
|
||||||
|
|||||||
Reference in New Issue
Block a user