Skip to content

Commit 67a9d1e

Browse files
committed
Support MSETEX
1 parent 9c6023f commit 67a9d1e

File tree

13 files changed

+547
-103
lines changed

13 files changed

+547
-103
lines changed

docs/ReleaseNotes.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Current package versions:
88

99
## Unreleased
1010

11+
- Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration (PR pending)
12+
1113
## 2.9.32
1214

1315
- Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2969))

src/StackExchange.Redis/CommandMap.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public sealed class CommandMap
2727
RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY,
2828
RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SCAN,
2929

30-
RedisCommand.BITOP, RedisCommand.MSETNX,
30+
RedisCommand.BITOP, RedisCommand.MSETEX, RedisCommand.MSETNX,
3131

3232
RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither!
3333

@@ -53,7 +53,7 @@ public sealed class CommandMap
5353
RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY,
5454
RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SORT, RedisCommand.SCAN,
5555

56-
RedisCommand.BITOP, RedisCommand.MSETNX,
56+
RedisCommand.BITOP, RedisCommand.MSETEX, RedisCommand.MSETNX,
5757

5858
RedisCommand.BLPOP, RedisCommand.BRPOP, RedisCommand.BRPOPLPUSH, // yeah, me neither!
5959

src/StackExchange.Redis/Enums/RedisCommand.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ internal enum RedisCommand
122122
MONITOR,
123123
MOVE,
124124
MSET,
125+
MSETEX,
125126
MSETNX,
126127
MULTI,
127128

@@ -336,6 +337,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
336337
case RedisCommand.MIGRATE:
337338
case RedisCommand.MOVE:
338339
case RedisCommand.MSET:
340+
case RedisCommand.MSETEX:
339341
case RedisCommand.MSETNX:
340342
case RedisCommand.PERSIST:
341343
case RedisCommand.PEXPIRE:

src/StackExchange.Redis/Interfaces/IDatabase.cs

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3372,8 +3372,29 @@ IEnumerable<SortedSetEntry> SortedSetScan(
33723372
/// See
33733373
/// <seealso href="https://redis.io/commands/mset"/>,
33743374
/// <seealso href="https://redis.io/commands/msetnx"/>.
3375+
/// <seealso href="https://redis.io/commands/msetex"/>.
33753376
/// </remarks>
3376-
bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None);
3377+
bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when, CommandFlags flags);
3378+
3379+
/// <summary>
3380+
/// Sets the given keys to their respective values, optionally including expiration.
3381+
/// If <see cref="When.NotExists"/> is specified, this will not perform any operation at all even if just a single key already exists.
3382+
/// </summary>
3383+
/// <param name="values">The keys and values to set.</param>
3384+
/// <param name="when">Which condition to set the value under (defaults to always).</param>
3385+
/// <param name="expiry">The expiry to set.</param>
3386+
/// <param name="keepTtl">Whether to maintain the existing key's TTL (KEEPTTL flag).</param>
3387+
/// <param name="flags">The flags to use for this operation.</param>
3388+
/// <returns><see langword="true"/> if the keys were set, <see langword="false"/> otherwise.</returns>
3389+
/// <remarks>
3390+
/// See
3391+
/// <seealso href="https://redis.io/commands/mset"/>,
3392+
/// <seealso href="https://redis.io/commands/msetnx"/>.
3393+
/// <seealso href="https://redis.io/commands/msetex"/>.
3394+
/// </remarks>
3395+
#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);
3397+
#pragma warning restore RS0027
33773398

33783399
/// <summary>
33793400
/// Atomically sets key to value and returns the previous value (if any) stored at <paramref name="key"/>.

src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,12 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(
831831
Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);
832832

833833
/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, CommandFlags)"/>
834-
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None);
834+
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when, CommandFlags flags);
835+
836+
/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, TimeSpan?, bool, CommandFlags)"/>
837+
#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);
839+
#pragma warning restore RS0027
835840

836841
/// <inheritdoc cref="IDatabase.StringSetAndGet(RedisKey, RedisValue, TimeSpan?, When, CommandFlags)"/>
837842
Task<RedisValue> StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags);

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,9 @@ 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);
779+
777780
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) =>
778781
Inner.StringSetAsync(ToInner(key), value, expiry, when);
779782
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) =>

src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixedDatabase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,9 @@ 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);
761+
759762
public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) =>
760763
Inner.StringSet(ToInner(key), value, expiry, when);
761764
public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) =>

src/StackExchange.Redis/Message.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,9 @@ 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)
395+
=> new MultiSetMessage(db, flags, command, values, expiry, when);
396+
394397
/// <summary>Gets whether this is primary-only.</summary>
395398
/// <remarks>
396399
/// Note that the constructor runs the switch statement above, so
@@ -1691,6 +1694,55 @@ protected override void WriteImpl(PhysicalConnection physical)
16911694
public override int ArgCount => values.Length;
16921695
}
16931696

1697+
private sealed class MultiSetMessage(int db, CommandFlags flags, RedisCommand command, KeyValuePair<RedisKey, RedisValue>[] values, RedisDatabase.ExpiryToken expiry, When when) : Message(db, flags, command)
1698+
{
1699+
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
1700+
{
1701+
int slot = ServerSelectionStrategy.NoSlot;
1702+
for (int i = 0; i < values.Length; i++)
1703+
{
1704+
slot = serverSelectionStrategy.CombineSlot(slot, values[i].Key);
1705+
}
1706+
return slot;
1707+
}
1708+
1709+
// we support:
1710+
// - MSET {key1} {value1} [{key2} {value2}...]
1711+
// - MSETNX {key1} {value1} [{key2} {value2}...]
1712+
// - MSETEX {count} {key1} {value1} [{key2} {value2}...] [standard-expiry-tokens]
1713+
public override int ArgCount => Command == RedisCommand.MSETEX
1714+
? (1 + (2 * values.Length) + expiry.Tokens + (when is When.Exists or When.NotExists ? 1 : 0))
1715+
: (2 * values.Length); // MSET/MSETNX only support simple syntax
1716+
1717+
protected override void WriteImpl(PhysicalConnection physical)
1718+
{
1719+
var cmd = Command;
1720+
physical.WriteHeader(cmd, ArgCount);
1721+
if (cmd == RedisCommand.MSETEX) // need count prefix
1722+
{
1723+
physical.WriteBulkString(values.Length);
1724+
}
1725+
for (int i = 0; i < values.Length; i++)
1726+
{
1727+
physical.Write(values[i].Key);
1728+
physical.WriteBulkString(values[i].Value);
1729+
}
1730+
if (cmd == RedisCommand.MSETEX) // allow expiry/mode tokens
1731+
{
1732+
expiry.WriteTo(physical);
1733+
switch (when)
1734+
{
1735+
case When.Exists:
1736+
physical.WriteBulkString(RedisLiterals.XX);
1737+
break;
1738+
case When.NotExists:
1739+
physical.WriteBulkString(RedisLiterals.NX);
1740+
break;
1741+
}
1742+
}
1743+
}
1744+
}
1745+
16941746
private sealed class CommandValueChannelMessage : CommandChannelBase
16951747
{
16961748
private readonly RedisValue value;

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,7 @@ StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceWithMatches(StackExc
779779
StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
780780
StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> bool
781781
StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool
782-
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.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
782+
StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair<StackExchange.Redis.RedisKey, StackExchange.Redis.RedisValue>[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool
783783
StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> StackExchange.Redis.RedisValue
784784
StackExchange.Redis.IDatabase.StringSetAndGet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> StackExchange.Redis.RedisValue
785785
StackExchange.Redis.IDatabase.StringSetBit(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
@@ -1026,7 +1026,7 @@ StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.Redi
10261026
StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry = null, bool keepTtl = false, StackExchange.Redis.When when = StackExchange.Redis.When.Always, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
10271027
StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> System.Threading.Tasks.Task<bool>!
10281028
StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task<bool>!
1029-
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.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
1029+
StackExchange.Redis.IDatabaseAsync.StringSetAsync(System.Collections.Generic.KeyValuePair<StackExchange.Redis.RedisKey, StackExchange.Redis.RedisValue>[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> System.Threading.Tasks.Task<bool>!
10301030
StackExchange.Redis.IDatabaseAsync.StringSetBitAsync(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<bool>!
10311031
StackExchange.Redis.IDatabaseAsync.StringSetRangeAsync(StackExchange.Redis.RedisKey key, long offset, StackExchange.Redis.RedisValue value, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> System.Threading.Tasks.Task<StackExchange.Redis.RedisValue>!
10321032
StackExchange.Redis.InternalErrorEventArgs
@@ -2052,3 +2052,5 @@ 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>!

0 commit comments

Comments
 (0)