Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/StackExchange.Redis/Enums/ExpirationFlags.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;

namespace StackExchange.Redis
{
/// <summary>
/// Additional options for expiration-bearing commands.
/// </summary>
[Flags]
public enum ExpirationFlags
{
/// <summary>
/// No options specified.
/// </summary>
None = 0,

/// <summary>
/// Apply the expiration only if no expiration already exists.
/// </summary>
ExpireIfNotExists = 1 << 0,
}
}
2 changes: 2 additions & 0 deletions src/StackExchange.Redis/Enums/RedisCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ internal enum RedisCommand
INCR,
INCRBY,
INCRBYFLOAT,
INCREX,
INFO,

KEYS,
Expand Down Expand Up @@ -347,6 +348,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.INCR:
case RedisCommand.INCRBY:
case RedisCommand.INCRBYFLOAT:
case RedisCommand.INCREX:
case RedisCommand.LINSERT:
case RedisCommand.LMOVE:
case RedisCommand.LMPOP:
Expand Down
213 changes: 125 additions & 88 deletions src/StackExchange.Redis/Expiration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ public readonly struct Expiration
- PX {ms} - relative expiry in milliseconds
- EXAT {s} - absolute expiry in seconds
- PXAT {ms} - absolute expiry in milliseconds
- ENX - only apply the expiration if no expiration currently exists

We need to distinguish between these 6 scenarios, which we can logically do with 3 bits (8 options).
So; we'll use a ulong for the value, reserving the top 3 bits for the mode.
Historically this packed the mode and value into a single ulong. We now keep the raw long
separate from explicit flags so we can extend expiration behavior without stealing more bits
from the numeric payload.
*/

/// <summary>
Expand All @@ -39,22 +41,29 @@ public readonly struct Expiration
/// <summary>
/// Expire at the specified absolute time.
/// </summary>
public Expiration(DateTime when)
public Expiration(DateTime when) : this(when, ExpirationFlags.None) { }

/// <summary>
/// Expire at the specified absolute time.
/// </summary>
public Expiration(DateTime when, ExpirationFlags flags)
{
if (when == DateTime.MaxValue)
{
_valueAndMode = s_Default._valueAndMode;
_value = s_Default._value;
_flags = s_Default._flags;
return;
}

long millis = GetUnixTimeMilliseconds(when);
var extraFlags = ToStateFlags(flags);
if ((millis % 1000) == 0)
{
Init(ExpirationMode.AbsoluteSeconds, millis / 1000, out _valueAndMode);
Init(ExpirationState.HasExpiration | ExpirationState.IsAbsolute | extraFlags, millis / 1000, out _value, out _flags);
}
else
{
Init(ExpirationMode.AbsoluteMilliseconds, millis, out _valueAndMode);
Init(ExpirationState.HasExpiration | ExpirationState.IsAbsolute | ExpirationState.IsMillis | extraFlags, millis, out _value, out _flags);
}
}

Expand All @@ -71,70 +80,88 @@ public Expiration(DateTime when)
/// <summary>
/// Expire at the specified relative time.
/// </summary>
public Expiration(TimeSpan ttl)
public Expiration(TimeSpan ttl) : this(ttl, ExpirationFlags.None) { }

/// <summary>
/// Expire at the specified relative time.
/// </summary>
public Expiration(TimeSpan ttl, ExpirationFlags flags)
{
if (ttl == TimeSpan.MaxValue)
{
_valueAndMode = s_Default._valueAndMode;
_value = s_Default._value;
_flags = s_Default._flags;
return;
}

var millis = ttl.Ticks / TimeSpan.TicksPerMillisecond;
var extraFlags = ToStateFlags(flags);
if ((millis % 1000) == 0)
{
Init(ExpirationMode.RelativeSeconds, millis / 1000, out _valueAndMode);
Init(ExpirationState.HasExpiration | extraFlags, millis / 1000, out _value, out _flags);
}
else
{
Init(ExpirationMode.RelativeMilliseconds, millis, out _valueAndMode);
Init(ExpirationState.HasExpiration | ExpirationState.IsMillis | extraFlags, millis, out _value, out _flags);
}
}

private readonly ulong _valueAndMode;
private readonly long _value;
private readonly ExpirationState _flags;

private static void Init(ExpirationMode mode, long value, out ulong valueAndMode)
[Flags]
private enum ExpirationState : byte
{
// check the caller isn't using the top 3 bits that we have reserved; this includes checking for -ve values
ulong uValue = (ulong)value;
if ((uValue & ~ValueMask) != 0) Throw();
valueAndMode = (uValue & ValueMask) | ((ulong)mode << 61);
static void Throw() => throw new ArgumentOutOfRangeException(nameof(value));
None = 0,
ExpireIfNotExists = (byte)ExpirationFlags.ExpireIfNotExists,
HasExpiration = 1 << 1,
IsMillis = 1 << 2,
IsAbsolute = 1 << 3,
KeepTtl = 1 << 4,
Persist = 1 << 5,
}

private Expiration(ExpirationMode mode, long value) => Init(mode, value, out _valueAndMode);
private static ExpirationState ToStateFlags(ExpirationFlags flags)
{
const ExpirationFlags validFlags = ExpirationFlags.ExpireIfNotExists;
if ((flags & ~validFlags) != 0) Throw();
return (ExpirationState)flags;

private enum ExpirationMode : byte
static void Throw() => throw new ArgumentOutOfRangeException(nameof(flags));
}

private static void Init(ExpirationState flags, long value, out long rawValue, out ExpirationState rawFlags)
{
Default = 0,
RelativeSeconds = 1,
RelativeMilliseconds = 2,
AbsoluteSeconds = 3,
AbsoluteMilliseconds = 4,
KeepTtl = 5,
Persist = 6,
NotUsed = 7, // just to ensure all 8 possible values are covered
if (value < 0) Throw();
rawValue = value;
rawFlags = flags;
static void Throw() => throw new ArgumentOutOfRangeException(nameof(value));
}

private const ulong ValueMask = (~0UL) >> 3;
internal long Value => unchecked((long)(_valueAndMode & ValueMask));
private ExpirationMode Mode => (ExpirationMode)(_valueAndMode >> 61); // note unsigned, no need to mask
private Expiration(ExpirationState flags, long value)
{
_value = value;
_flags = flags;
}

internal bool IsKeepTtl => Mode is ExpirationMode.KeepTtl;
internal bool IsPersist => Mode is ExpirationMode.Persist;
internal bool IsNone => Mode is ExpirationMode.Default;
internal bool IsNoneOrKeepTtl => Mode is ExpirationMode.Default or ExpirationMode.KeepTtl;
internal bool IsAbsolute => Mode is ExpirationMode.AbsoluteSeconds or ExpirationMode.AbsoluteMilliseconds;
internal bool IsRelative => Mode is ExpirationMode.RelativeSeconds or ExpirationMode.RelativeMilliseconds;
internal long Value => _value;

internal bool IsMilliseconds =>
Mode is ExpirationMode.RelativeMilliseconds or ExpirationMode.AbsoluteMilliseconds;
internal bool IsKeepTtl => (_flags & ExpirationState.KeepTtl) != 0;
internal bool IsPersist => (_flags & ExpirationState.Persist) != 0;
internal bool IsExpireIfNotExists => (_flags & ExpirationState.ExpireIfNotExists) != 0;
internal bool IsNone => _flags == ExpirationState.None;
internal bool IsNoneOrKeepTtl => IsNone || IsKeepTtl;
internal bool IsAbsolute => (_flags & ExpirationState.IsAbsolute) != 0;
internal bool IsRelative => (_flags & ExpirationState.HasExpiration) != 0 && !IsAbsolute;

internal bool IsSeconds => Mode is ExpirationMode.RelativeSeconds or ExpirationMode.AbsoluteSeconds;
internal bool IsMilliseconds => (_flags & ExpirationState.IsMillis) != 0;

private static readonly Expiration s_Default = new(ExpirationMode.Default, 0);
internal bool IsSeconds => (_flags & (ExpirationState.HasExpiration | ExpirationState.IsMillis)) == ExpirationState.HasExpiration;

private static readonly Expiration s_KeepTtl = new(ExpirationMode.KeepTtl, 0),
s_Persist = new(ExpirationMode.Persist, 0);
private static readonly Expiration s_Default = new(ExpirationState.None, 0);

private static readonly Expiration s_KeepTtl = new(ExpirationState.KeepTtl, 0),
s_Persist = new(ExpirationState.Persist, 0);

private static void ThrowExpiryAndKeepTtl() =>
// ReSharper disable once NotResolvedInText
Expand Down Expand Up @@ -206,68 +233,78 @@ internal static Expiration CreateOrKeepTtl(in DateTime? ttl, bool keepTtl)
internal RedisValue GetOperand(out long value)
{
value = Value;
var mode = Mode;
return mode switch
if (IsKeepTtl) return RedisLiterals.KEEPTTL;
if (IsPersist) return RedisLiterals.PERSIST;
if ((_flags & ExpirationState.HasExpiration) == 0) return RedisValue.Null;

return (IsAbsolute, IsMilliseconds) switch
{
ExpirationMode.KeepTtl => RedisLiterals.KEEPTTL,
ExpirationMode.Persist => RedisLiterals.PERSIST,
ExpirationMode.RelativeSeconds => RedisLiterals.EX,
ExpirationMode.RelativeMilliseconds => RedisLiterals.PX,
ExpirationMode.AbsoluteSeconds => RedisLiterals.EXAT,
ExpirationMode.AbsoluteMilliseconds => RedisLiterals.PXAT,
_ => RedisValue.Null,
(false, false) => RedisLiterals.EX,
(false, true) => RedisLiterals.PX,
(true, false) => RedisLiterals.EXAT,
(true, true) => RedisLiterals.PXAT,
};
}

private static void ThrowMode(ExpirationMode mode) =>
throw new InvalidOperationException("Unknown mode: " + mode);

/// <inheritdoc/>
public override string ToString() => Mode switch
public override string ToString()
{
ExpirationMode.Default or ExpirationMode.NotUsed => "",
ExpirationMode.KeepTtl => "KEEPTTL",
ExpirationMode.Persist => "PERSIST",
_ => $"{Operand} {Value}",
};
if (IsNone) return "";
if (IsKeepTtl) return "KEEPTTL";
if (IsPersist) return "PERSIST";
return IsExpireIfNotExists ? $"{Operand} {Value} {RedisLiterals.ENX}" : $"{Operand} {Value}";
}

/// <inheritdoc/>
public override int GetHashCode() => _valueAndMode.GetHashCode();
public override int GetHashCode()
{
unchecked
{
return (_value.GetHashCode() * 397) ^ (int)_flags;
}
}

/// <inheritdoc/>
public override bool Equals(object? obj) => obj is Expiration other && _valueAndMode == other._valueAndMode;
public override bool Equals(object? obj) => obj is Expiration other && _value == other._value && _flags == other._flags;

internal int TokenCount => Mode switch
internal int GetTokenCount(bool allowEnx)
{
ExpirationMode.Default or ExpirationMode.NotUsed => 0,
ExpirationMode.KeepTtl or ExpirationMode.Persist => 1,
_ => 2,
};
if (!allowEnx && IsExpireIfNotExists) return ThrowEnxNotSupported();
return IsNone ? 0 : (IsKeepTtl || IsPersist ? 1 : (IsExpireIfNotExists ? 3 : 2));

static int ThrowEnxNotSupported() => throw new NotSupportedException("ENX is not supported for this command.");
}

internal void WriteTo(PhysicalConnection physical)
{
var mode = Mode;
switch (Mode)
if (IsNone)
{
return;
}

if (IsKeepTtl)
{
physical.WriteBulkString("KEEPTTL"u8);
return;
}

if (IsPersist)
{
physical.WriteBulkString("PERSIST"u8);
return;
}

physical.WriteBulkString((IsAbsolute, IsMilliseconds) switch
{
(false, false) => "EX"u8,
(false, true) => "PX"u8,
(true, false) => "EXAT"u8,
(true, true) => "PXAT"u8,
});
physical.WriteBulkString(Value);
if (IsExpireIfNotExists)
{
case ExpirationMode.Default or ExpirationMode.NotUsed:
break;
case ExpirationMode.KeepTtl:
physical.WriteBulkString("KEEPTTL"u8);
break;
case ExpirationMode.Persist:
physical.WriteBulkString("PERSIST"u8);
break;
default:
physical.WriteBulkString(mode switch
{
ExpirationMode.RelativeSeconds => "EX"u8,
ExpirationMode.RelativeMilliseconds => "PX"u8,
ExpirationMode.AbsoluteSeconds => "EXAT"u8,
ExpirationMode.AbsoluteMilliseconds => "PXAT"u8,
_ => default,
});
physical.WriteBulkString(Value);
break;
physical.WriteBulkString("ENX"u8);
}
}
}
Loading
Loading