-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathCacheConfigAttribute.cs
More file actions
119 lines (107 loc) · 5.1 KB
/
Copy pathCacheConfigAttribute.cs
File metadata and controls
119 lines (107 loc) · 5.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
using System.Globalization;
namespace CLOOPS.microservices;
/// <summary>
/// Declares cache configuration for a <see cref="BaseCacheService{TValue}"/> implementation.
/// Apply this attribute to every cache service class; it is required and read once at startup.
/// </summary>
/// <example>
/// <code>
/// // L1 + L2 cache (Redis used as L2 when REDIS_CONNECTION_STRING is set):
/// [CacheConfig(
/// name: "patient-cache",
/// l1Ttl: "00:05:00",
/// l2Ttl: "01:00:00",
/// RefreshCron = "0 */15 * * * *")]
/// public class PatientCacheService : BaseCacheService<Patient> { ... }
///
/// // L1-only cache (no L2 even when Redis is configured):
/// [CacheConfig(
/// name: "tigerbeetle-readiness",
/// l1Ttl: "00:05:00",
/// RefreshCron = "0 */4 * * * *")]
/// public class TigerBeetleReadinessCacheService : BaseCacheService<bool> { ... }
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public sealed class CacheConfigAttribute : Attribute
{
/// <summary>
/// Initializes a new <see cref="CacheConfigAttribute"/> with both L1 and L2 TTLs.
/// <see cref="EnableL2"/> defaults to <c>true</c>; the distributed cache is only activated
/// when <see cref="EnableL2"/> is <c>true</c> AND a Redis connection string is configured.
/// </summary>
/// <param name="name">Stable cache name used in cache keys and tags. Must be non-empty and cannot contain ':'.</param>
/// <param name="l1Ttl">Local in-process cache TTL, in <see cref="TimeSpan"/> string format (e.g. "00:05:00" for 5 minutes).</param>
/// <param name="l2Ttl">Distributed cache TTL, in <see cref="TimeSpan"/> string format (e.g. "01:00:00" for 1 hour).</param>
public CacheConfigAttribute(string name, string l1Ttl, string l2Ttl)
{
Name = name;
L1Ttl = ParseTimeSpan(l1Ttl, nameof(l1Ttl));
L2Ttl = ParseTimeSpan(l2Ttl, nameof(l2Ttl));
}
/// <summary>
/// Initializes a new <see cref="CacheConfigAttribute"/> for an L1-only cache.
/// Sets <see cref="EnableL2"/> to <c>false</c>, so the distributed cache is not used
/// even when a Redis connection string is configured.
/// </summary>
/// <param name="name">Stable cache name used in cache keys and tags. Must be non-empty and cannot contain ':'.</param>
/// <param name="l1Ttl">Local in-process cache TTL, in <see cref="TimeSpan"/> string format (e.g. "00:05:00" for 5 minutes).</param>
public CacheConfigAttribute(string name, string l1Ttl)
: this(name, l1Ttl, l1Ttl)
{
EnableL2 = false;
}
/// <summary>
/// Stable cache name used in cache keys (prefix) and HybridCache tags.
/// </summary>
public string Name { get; }
/// <summary>
/// Local in-process cache TTL.
/// </summary>
public TimeSpan L1Ttl { get; }
/// <summary>
/// Distributed cache TTL. Ignored when <see cref="EnableL2"/> is <c>false</c>.
/// </summary>
public TimeSpan L2Ttl { get; }
/// <summary>
/// Whether this cache service may use a distributed L2 cache (Redis).
/// Defaults to <c>true</c>. Set to <c>false</c> to force L1-only behavior even
/// when <c>REDIS_CONNECTION_STRING</c> is configured.
/// The distributed cache is only effectively used when both this flag is <c>true</c>
/// AND a Redis connection string is configured at the host level.
/// </summary>
public bool EnableL2 { get; set; } = true;
/// <summary>
/// Optional cron expression that periodically triggers <c>HydrateAllAsync</c>.
/// Requires the cache service to override <c>HydrateAllAsync</c>.
/// </summary>
public string? RefreshCron { get; set; }
/// <summary>
/// Whether bulk refresh should acquire a NATS-backed distributed lock so only one
/// pod hydrates per refresh cycle. Derived from <see cref="EnableL2"/>:
/// <list type="bullet">
/// <item><c>EnableL2 = true</c>: refresh updates shared distributed state, so the
/// distributed lock is used to avoid every pod hammering the source of truth.</item>
/// <item><c>EnableL2 = false</c>: refresh updates per-pod L1 state only, so every
/// pod must hydrate independently on every cron tick.</item>
/// </list>
/// </summary>
public bool UseDistributedRefreshLock => EnableL2;
/// <summary>
/// When <c>true</c>, the cache will run one best-effort bulk refresh during host startup.
/// Startup hydration BLOCKS host startup until it completes (or until another instance
/// is found holding the refresh lock). Use only for small reference datasets.
/// Requires the cache service to override <c>HydrateAllAsync</c>.
/// </summary>
public bool RefreshOnStartup { get; set; }
private static TimeSpan ParseTimeSpan(string value, string paramName)
{
if (!TimeSpan.TryParse(value, CultureInfo.InvariantCulture, out var parsed))
{
throw new ArgumentException(
$"Could not parse '{value}' as a TimeSpan. Use a format like '00:05:00' (5 minutes) or '01:00:00' (1 hour).",
paramName);
}
return parsed;
}
}