Skip to content

Commit c168276

Browse files
committed
create Expiration as top-level concept
1 parent 94ec702 commit c168276

File tree

10 files changed

+346
-314
lines changed

10 files changed

+346
-314
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
using System;
2+
3+
namespace StackExchange.Redis;
4+
5+
/// <summary>
6+
/// Configures the expiration behaviour of a command.
7+
/// </summary>
8+
public readonly struct Expiration
9+
{
10+
/*
11+
Redis expiration supports different modes:
12+
- (nothing) - do nothing; implicit wipe for writes, nothing for reads
13+
- PERSIST - explicit wipe of expiry
14+
- KEEPTTL - sets no expiry, but leaves any existing expiry alone
15+
- EX {s} - relative expiry in seconds
16+
- PX {ms} - relative expiry in milliseconds
17+
- EXAT {s} - absolute expiry in seconds
18+
- PXAT {ms} - absolute expiry in milliseconds
19+
20+
We need to distinguish between these 6 scenarios, which we can logically do with 3 bits (8 options).
21+
So; we'll use a ulong for the value, reserving the top 3 bits for the mode.
22+
*/
23+
24+
/// <summary>
25+
/// Default expiration behaviour. For writes, this is typically no expiration. For reads, this is typically no action.
26+
/// </summary>
27+
public static Expiration Default => s_Default;
28+
29+
/// <summary>
30+
/// Explicitly retain the existing expiry, if one. This is valid in some (not all) write scenarios.
31+
/// </summary>
32+
public static Expiration KeepTtl => s_KeepTtl;
33+
34+
/// <summary>
35+
/// Explicitly remove the existing expiry, if one. This is valid in some (not all) read scenarios.
36+
/// </summary>
37+
public static Expiration Persist => s_Persist;
38+
39+
/// <summary>
40+
/// Expire at the specified absolute time.
41+
/// </summary>
42+
public Expiration(DateTime when)
43+
{
44+
if (when == DateTime.MaxValue)
45+
{
46+
_valueAndMode = s_Default._valueAndMode;
47+
return;
48+
}
49+
50+
long millis = GetUnixTimeMilliseconds(when);
51+
if ((millis % 1000) == 0)
52+
{
53+
Init(ExpirationMode.AbsoluteSeconds, millis / 1000, out _valueAndMode);
54+
}
55+
else
56+
{
57+
Init(ExpirationMode.AbsoluteMilliseconds, millis, out _valueAndMode);
58+
}
59+
}
60+
61+
/// <summary>
62+
/// Expire at the specified absolute time.
63+
/// </summary>
64+
public static implicit operator Expiration(DateTime when) => new(when);
65+
66+
/// <summary>
67+
/// Expire at the specified absolute time.
68+
/// </summary>
69+
public static implicit operator Expiration(TimeSpan ttl) => new(ttl);
70+
71+
/// <summary>
72+
/// Expire at the specified relative time.
73+
/// </summary>
74+
public Expiration(TimeSpan ttl)
75+
{
76+
if (ttl == TimeSpan.MaxValue)
77+
{
78+
_valueAndMode = s_Default._valueAndMode;
79+
return;
80+
}
81+
82+
var millis = ttl.Ticks / TimeSpan.TicksPerMillisecond;
83+
if ((millis % 1000) == 0)
84+
{
85+
Init(ExpirationMode.RelativeSeconds, millis / 1000, out _valueAndMode);
86+
}
87+
else
88+
{
89+
Init(ExpirationMode.RelativeMilliseconds, millis, out _valueAndMode);
90+
}
91+
}
92+
93+
private readonly ulong _valueAndMode;
94+
95+
private static void Init(ExpirationMode mode, long value, out ulong valueAndMode)
96+
{
97+
// check the caller isn't using the top 3 bits that we have reserved; this includes checking for -ve values
98+
ulong uValue = (ulong)value;
99+
if ((uValue & ~ValueMask) != 0) Throw();
100+
valueAndMode = (uValue & ValueMask) | ((ulong)mode << 61);
101+
static void Throw() => throw new ArgumentOutOfRangeException(nameof(value));
102+
}
103+
104+
private Expiration(ExpirationMode mode, long value) => Init(mode, value, out _valueAndMode);
105+
106+
private enum ExpirationMode : byte
107+
{
108+
Default = 0,
109+
RelativeSeconds = 1,
110+
RelativeMilliseconds = 2,
111+
AbsoluteSeconds = 3,
112+
AbsoluteMilliseconds = 4,
113+
KeepTtl = 5,
114+
Persist = 6,
115+
NotUsed = 7, // just to ensure all 8 possible values are covered
116+
}
117+
118+
private const ulong ValueMask = (~0UL) >> 3;
119+
internal long Value => unchecked((long)(_valueAndMode & ValueMask));
120+
private ExpirationMode Mode => (ExpirationMode)(_valueAndMode >> 61); // note unsigned, no need to mask
121+
122+
internal bool IsKeepTtl => Mode is ExpirationMode.KeepTtl;
123+
internal bool IsPersist => Mode is ExpirationMode.Persist;
124+
internal bool IsNone => Mode is ExpirationMode.Default;
125+
internal bool IsNoneOrKeepTtl => Mode is ExpirationMode.Default or ExpirationMode.KeepTtl;
126+
internal bool IsAbsolute => Mode is ExpirationMode.AbsoluteSeconds or ExpirationMode.AbsoluteMilliseconds;
127+
internal bool IsRelative => Mode is ExpirationMode.RelativeSeconds or ExpirationMode.RelativeMilliseconds;
128+
129+
internal bool IsMilliseconds =>
130+
Mode is ExpirationMode.RelativeMilliseconds or ExpirationMode.AbsoluteMilliseconds;
131+
132+
internal bool IsSeconds => Mode is ExpirationMode.RelativeSeconds or ExpirationMode.AbsoluteSeconds;
133+
134+
private static readonly Expiration s_Default = new(ExpirationMode.Default, 0);
135+
136+
private static readonly Expiration s_KeepTtl = new(ExpirationMode.KeepTtl, 0),
137+
s_Persist = new(ExpirationMode.Persist, 0);
138+
139+
private static void ThrowExpiryAndKeepTtl() =>
140+
// ReSharper disable once NotResolvedInText
141+
throw new ArgumentException(message: "Cannot specify both expiry and keepTtl.", paramName: "keepTtl");
142+
143+
private static void ThrowExpiryAndPersist() =>
144+
// ReSharper disable once NotResolvedInText
145+
throw new ArgumentException(message: "Cannot specify both expiry and persist.", paramName: "persist");
146+
147+
internal static Expiration CreateOrPersist(in TimeSpan? ttl, bool persist)
148+
{
149+
if (persist)
150+
{
151+
if (ttl.HasValue) ThrowExpiryAndPersist();
152+
return s_Persist;
153+
}
154+
155+
return ttl.HasValue ? new(ttl.GetValueOrDefault()) : s_Default;
156+
}
157+
158+
internal static Expiration CreateOrKeepTtl(in TimeSpan? ttl, bool keepTtl)
159+
{
160+
if (keepTtl)
161+
{
162+
if (ttl.HasValue) ThrowExpiryAndKeepTtl();
163+
return s_KeepTtl;
164+
}
165+
166+
return ttl.HasValue ? new(ttl.GetValueOrDefault()) : s_Default;
167+
}
168+
169+
internal static long GetUnixTimeMilliseconds(DateTime when)
170+
{
171+
return when.Kind switch
172+
{
173+
DateTimeKind.Local or DateTimeKind.Utc => (when.ToUniversalTime() - RedisBase.UnixEpoch).Ticks /
174+
TimeSpan.TicksPerMillisecond,
175+
_ => ThrowKind(),
176+
};
177+
178+
static long ThrowKind() =>
179+
throw new ArgumentException("Expiry time must be either Utc or Local", nameof(when));
180+
}
181+
182+
internal static Expiration CreateOrPersist(in DateTime? when, bool persist)
183+
{
184+
if (persist)
185+
{
186+
if (when.HasValue) ThrowExpiryAndPersist();
187+
return s_Persist;
188+
}
189+
190+
return when.HasValue ? new(when.GetValueOrDefault()) : s_Default;
191+
}
192+
193+
internal static Expiration CreateOrKeepTtl(in DateTime? ttl, bool keepTtl)
194+
{
195+
if (keepTtl)
196+
{
197+
if (ttl.HasValue) ThrowExpiryAndKeepTtl();
198+
return s_KeepTtl;
199+
}
200+
201+
return ttl.HasValue ? new(ttl.GetValueOrDefault()) : s_Default;
202+
}
203+
204+
internal RedisValue Operand => GetOperand(out _);
205+
206+
internal RedisValue GetOperand(out long value)
207+
{
208+
value = Value;
209+
var mode = Mode;
210+
return mode switch
211+
{
212+
ExpirationMode.KeepTtl => RedisLiterals.KEEPTTL,
213+
ExpirationMode.Persist => RedisLiterals.PERSIST,
214+
ExpirationMode.RelativeSeconds => RedisLiterals.EX,
215+
ExpirationMode.RelativeMilliseconds => RedisLiterals.PX,
216+
ExpirationMode.AbsoluteSeconds => RedisLiterals.EXAT,
217+
ExpirationMode.AbsoluteMilliseconds => RedisLiterals.PXAT,
218+
_ => RedisValue.Null,
219+
};
220+
}
221+
222+
private static void ThrowMode(ExpirationMode mode) =>
223+
throw new InvalidOperationException("Unknown mode: " + mode);
224+
225+
/// <inheritdoc/>
226+
public override string ToString() => Mode switch
227+
{
228+
ExpirationMode.Default or ExpirationMode.NotUsed => "",
229+
ExpirationMode.KeepTtl => "KEEPTTL",
230+
ExpirationMode.Persist => "PERSIST",
231+
_ => $"{Operand} {Value}",
232+
};
233+
234+
/// <inheritdoc/>
235+
public override int GetHashCode() => _valueAndMode.GetHashCode();
236+
237+
/// <inheritdoc/>
238+
public override bool Equals(object? obj) => obj is Expiration other && _valueAndMode == other._valueAndMode;
239+
240+
internal int Tokens => Mode switch
241+
{
242+
ExpirationMode.Default or ExpirationMode.NotUsed => 0,
243+
ExpirationMode.KeepTtl or ExpirationMode.Persist => 1,
244+
_ => 2,
245+
};
246+
247+
internal void WriteTo(PhysicalConnection physical)
248+
{
249+
var mode = Mode;
250+
switch (Mode)
251+
{
252+
case ExpirationMode.Default or ExpirationMode.NotUsed:
253+
break;
254+
case ExpirationMode.KeepTtl:
255+
physical.WriteBulkString("KEEPTTL"u8);
256+
break;
257+
case ExpirationMode.Persist:
258+
physical.WriteBulkString("PERSIST"u8);
259+
break;
260+
default:
261+
physical.WriteBulkString(mode switch
262+
{
263+
ExpirationMode.RelativeSeconds => "EX"u8,
264+
ExpirationMode.RelativeMilliseconds => "PX"u8,
265+
ExpirationMode.AbsoluteSeconds => "EXAT"u8,
266+
ExpirationMode.AbsoluteMilliseconds => "PXAT"u8,
267+
_ => default,
268+
});
269+
physical.WriteBulkString(Value);
270+
break;
271+
}
272+
}
273+
}

src/StackExchange.Redis/Interfaces/IDatabase.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3383,7 +3383,6 @@ IEnumerable<SortedSetEntry> SortedSetScan(
33833383
/// <param name="values">The keys and values to set.</param>
33843384
/// <param name="when">Which condition to set the value under (defaults to always).</param>
33853385
/// <param name="expiry">The expiry to set.</param>
3386-
/// <param name="keepTtl">Whether to maintain the existing key's TTL (KEEPTTL flag).</param>
33873386
/// <param name="flags">The flags to use for this operation.</param>
33883387
/// <returns><see langword="true"/> if the keys were set, <see langword="false"/> otherwise.</returns>
33893388
/// <remarks>
@@ -3393,7 +3392,7 @@ IEnumerable<SortedSetEntry> SortedSetScan(
33933392
/// <seealso href="https://redis.io/commands/msetex"/>.
33943393
/// </remarks>
33953394
#pragma warning disable RS0027 // due to overlap with single-key variant, but: not ambiguous
3396-
bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, TimeSpan? expiry = null, bool keepTtl = false, CommandFlags flags = CommandFlags.None);
3395+
bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, Expiration expiry = default, CommandFlags flags = CommandFlags.None);
33973396
#pragma warning restore RS0027
33983397

33993398
/// <summary>

src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -833,9 +833,9 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(
833833
/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, CommandFlags)"/>
834834
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when, CommandFlags flags);
835835

836-
/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, TimeSpan?, bool, CommandFlags)"/>
836+
/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, Expiration, CommandFlags)"/>
837837
#pragma warning disable RS0027 // due to overlap with single-key variant, but: not ambiguous
838-
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, TimeSpan? expiry = null, bool keepTtl = false, CommandFlags flags = CommandFlags.None);
838+
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, Expiration expiry = default, CommandFlags flags = CommandFlags.None);
839839
#pragma warning restore RS0027
840840

841841
/// <inheritdoc cref="IDatabase.StringSetAndGet(RedisKey, RedisValue, TimeSpan?, When, CommandFlags)"/>

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -774,8 +774,8 @@ public Task<long> StringLengthAsync(RedisKey key, CommandFlags flags = CommandFl
774774
public Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
775775
Inner.StringSetAsync(ToInner(values), when, flags);
776776

777-
public Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when, TimeSpan? expiry, bool keepTtl, CommandFlags flags) =>
778-
Inner.StringSetAsync(ToInner(values), when, expiry, keepTtl, flags);
777+
public Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when, Expiration expiry, CommandFlags flags) =>
778+
Inner.StringSetAsync(ToInner(values), when, expiry, flags);
779779

780780
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) =>
781781
Inner.StringSetAsync(ToInner(key), value, expiry, when);

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -756,8 +756,8 @@ public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) =
756756
public bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
757757
Inner.StringSet(ToInner(values), when, flags);
758758

759-
public bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when, TimeSpan? expiry, bool keepTtl, CommandFlags flags) =>
760-
Inner.StringSet(ToInner(values), when, expiry, keepTtl, flags);
759+
public bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when, Expiration expiry, CommandFlags flags) =>
760+
Inner.StringSet(ToInner(values), when, expiry, flags);
761761

762762
public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) =>
763763
Inner.StringSet(ToInner(key), value, expiry, when);

src/StackExchange.Redis/Message.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ public static Message Create(
391391
public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) =>
392392
new CommandSlotValuesMessage(db, slot, flags, command, values);
393393

394-
public static Message Create(int db, CommandFlags flags, RedisCommand command, KeyValuePair<RedisKey, RedisValue>[] values, RedisDatabase.ExpiryToken expiry, When when)
394+
public static Message Create(int db, CommandFlags flags, RedisCommand command, KeyValuePair<RedisKey, RedisValue>[] values, Expiration expiry, When when)
395395
=> new MultiSetMessage(db, flags, command, values, expiry, when);
396396

397397
/// <summary>Gets whether this is primary-only.</summary>
@@ -1694,7 +1694,7 @@ protected override void WriteImpl(PhysicalConnection physical)
16941694
public override int ArgCount => values.Length;
16951695
}
16961696

1697-
private sealed class MultiSetMessage(int db, CommandFlags flags, RedisCommand command, KeyValuePair<RedisKey, RedisValue>[] values, RedisDatabase.ExpiryToken expiry, When when) : Message(db, flags, command)
1697+
private sealed class MultiSetMessage(int db, CommandFlags flags, RedisCommand command, KeyValuePair<RedisKey, RedisValue>[] values, Expiration expiry, When when) : Message(db, flags, command)
16981698
{
16991699
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
17001700
{

src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2052,5 +2052,17 @@ StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System.
20522052
[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest!
20532053
[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory<float> vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest!
20542054
StackExchange.Redis.RedisChannel.WithKeyRouting() -> StackExchange.Redis.RedisChannel
2055-
StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair<StackExchange.Redis.RedisKey, StackExchange.Redis.RedisValue>[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
2056-
StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair<StackExchange.Redis.RedisKey, StackExchange.Redis.RedisValue>[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
2055+
StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair<StackExchange.Redis.RedisKey, StackExchange.Redis.RedisValue>[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
2056+
StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair<StackExchange.Redis.RedisKey, StackExchange.Redis.RedisValue>[]! values, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.Expiration expiry = default(StackExchange.Redis.Expiration), StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
2057+
StackExchange.Redis.Expiration
2058+
StackExchange.Redis.Expiration.Expiration() -> void
2059+
StackExchange.Redis.Expiration.Expiration(System.DateTime when) -> void
2060+
StackExchange.Redis.Expiration.Expiration(System.TimeSpan ttl) -> void
2061+
override StackExchange.Redis.Expiration.Equals(object? obj) -> bool
2062+
override StackExchange.Redis.Expiration.GetHashCode() -> int
2063+
override StackExchange.Redis.Expiration.ToString() -> string!
2064+
static StackExchange.Redis.Expiration.Default.get -> StackExchange.Redis.Expiration
2065+
static StackExchange.Redis.Expiration.KeepTtl.get -> StackExchange.Redis.Expiration
2066+
static StackExchange.Redis.Expiration.Persist.get -> StackExchange.Redis.Expiration
2067+
static StackExchange.Redis.Expiration.implicit operator StackExchange.Redis.Expiration(System.DateTime when) -> StackExchange.Redis.Expiration
2068+
static StackExchange.Redis.Expiration.implicit operator StackExchange.Redis.Expiration(System.TimeSpan ttl) -> StackExchange.Redis.Expiration

0 commit comments

Comments
 (0)