Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 2 additions & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Current package versions:

## Unreleased

- Support `MSETEX` (Redis 8.4.0) for multi-key operations with expiration ([#2977 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2977))

## 2.9.32

- Fix `SSUBSCRIBE` routing during slot migrations ([#2969 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2969))
Expand Down
4 changes: 2 additions & 2 deletions src/StackExchange.Redis/CommandMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public sealed class CommandMap
RedisCommand.KEYS, RedisCommand.MIGRATE, RedisCommand.MOVE, RedisCommand.OBJECT, RedisCommand.RANDOMKEY,
RedisCommand.RENAME, RedisCommand.RENAMENX, RedisCommand.SCAN,

RedisCommand.BITOP, RedisCommand.MSETNX,
RedisCommand.BITOP, RedisCommand.MSETEX, RedisCommand.MSETNX,

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

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

RedisCommand.BITOP, RedisCommand.MSETNX,
RedisCommand.BITOP, RedisCommand.MSETEX, RedisCommand.MSETNX,

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

Expand Down
2 changes: 2 additions & 0 deletions src/StackExchange.Redis/Enums/RedisCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ internal enum RedisCommand
MONITOR,
MOVE,
MSET,
MSETEX,
MSETNX,
MULTI,

Expand Down Expand Up @@ -336,6 +337,7 @@ internal static bool IsPrimaryOnly(this RedisCommand command)
case RedisCommand.MIGRATE:
case RedisCommand.MOVE:
case RedisCommand.MSET:
case RedisCommand.MSETEX:
case RedisCommand.MSETNX:
case RedisCommand.PERSIST:
case RedisCommand.PEXPIRE:
Expand Down
23 changes: 22 additions & 1 deletion src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3372,8 +3372,29 @@ IEnumerable<SortedSetEntry> SortedSetScan(
/// See
/// <seealso href="https://redis.io/commands/mset"/>,
/// <seealso href="https://redis.io/commands/msetnx"/>.
/// <seealso href="https://redis.io/commands/msetex"/>.
/// </remarks>
bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None);
bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when, CommandFlags flags);

/// <summary>
/// Sets the given keys to their respective values, optionally including expiration.
/// If <see cref="When.NotExists"/> is specified, this will not perform any operation at all even if just a single key already exists.
/// </summary>
/// <param name="values">The keys and values to set.</param>
/// <param name="when">Which condition to set the value under (defaults to always).</param>
/// <param name="expiry">The expiry to set.</param>
/// <param name="keepTtl">Whether to maintain the existing key's TTL (KEEPTTL flag).</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns><see langword="true"/> if the keys were set, <see langword="false"/> otherwise.</returns>
/// <remarks>
/// See
/// <seealso href="https://redis.io/commands/mset"/>,
/// <seealso href="https://redis.io/commands/msetnx"/>.
/// <seealso href="https://redis.io/commands/msetex"/>.
/// </remarks>
#pragma warning disable RS0027 // due to overlap with single-key variant, but: not ambiguous
bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, TimeSpan? expiry = null, bool keepTtl = false, CommandFlags flags = CommandFlags.None);
#pragma warning restore RS0027

/// <summary>
/// Atomically sets key to value and returns the previous value (if any) stored at <paramref name="key"/>.
Expand Down
7 changes: 6 additions & 1 deletion src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,12 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(
Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, bool keepTtl = false, When when = When.Always, CommandFlags flags = CommandFlags.None);

/// <inheritdoc cref="IDatabase.StringSet(KeyValuePair{RedisKey, RedisValue}[], When, CommandFlags)"/>
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None);
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when, CommandFlags flags);

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

/// <inheritdoc cref="IDatabase.StringSetAndGet(RedisKey, RedisValue, TimeSpan?, When, CommandFlags)"/>
Task<RedisValue> StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags);
Expand Down
3 changes: 3 additions & 0 deletions src/StackExchange.Redis/KeyspaceIsolation/KeyPrefixed.cs
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,9 @@ public Task<long> StringLengthAsync(RedisKey key, CommandFlags flags = CommandFl
public Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
Inner.StringSetAsync(ToInner(values), when, flags);

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

public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when) =>
Inner.StringSetAsync(ToInner(key), value, expiry, when);
public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,9 @@ public long StringLength(RedisKey key, CommandFlags flags = CommandFlags.None) =
public bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None) =>
Inner.StringSet(ToInner(values), when, flags);

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

public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when) =>
Inner.StringSet(ToInner(key), value, expiry, when);
public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry, When when, CommandFlags flags) =>
Expand Down
56 changes: 54 additions & 2 deletions src/StackExchange.Redis/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ public static Message Create(
public static Message CreateInSlot(int db, int slot, CommandFlags flags, RedisCommand command, RedisValue[] values) =>
new CommandSlotValuesMessage(db, slot, flags, command, values);

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

/// <summary>Gets whether this is primary-only.</summary>
/// <remarks>
/// Note that the constructor runs the switch statement above, so
Expand Down Expand Up @@ -842,13 +845,13 @@ protected override void WriteImpl(PhysicalConnection physical)
physical.WriteBulkString(_protocolVersion);
if (!string.IsNullOrWhiteSpace(_password))
{
physical.WriteBulkString(RedisLiterals.AUTH);
physical.WriteBulkString("AUTH"u8);
physical.WriteBulkString(string.IsNullOrWhiteSpace(_username) ? RedisLiterals.@default : _username);
physical.WriteBulkString(_password);
}
if (!string.IsNullOrWhiteSpace(_clientName))
{
physical.WriteBulkString(RedisLiterals.SETNAME);
physical.WriteBulkString("SETNAME"u8);
physical.WriteBulkString(_clientName);
}
}
Expand Down Expand Up @@ -1691,6 +1694,55 @@ protected override void WriteImpl(PhysicalConnection physical)
public override int ArgCount => values.Length;
}

private sealed class MultiSetMessage(int db, CommandFlags flags, RedisCommand command, KeyValuePair<RedisKey, RedisValue>[] values, RedisDatabase.ExpiryToken expiry, When when) : Message(db, flags, command)
{
public override int GetHashSlot(ServerSelectionStrategy serverSelectionStrategy)
{
int slot = ServerSelectionStrategy.NoSlot;
for (int i = 0; i < values.Length; i++)
{
slot = serverSelectionStrategy.CombineSlot(slot, values[i].Key);
}
return slot;
}

// we support:
// - MSET {key1} {value1} [{key2} {value2}...]
// - MSETNX {key1} {value1} [{key2} {value2}...]
// - MSETEX {count} {key1} {value1} [{key2} {value2}...] [standard-expiry-tokens]
public override int ArgCount => Command == RedisCommand.MSETEX
? (1 + (2 * values.Length) + expiry.Tokens + (when is When.Exists or When.NotExists ? 1 : 0))
: (2 * values.Length); // MSET/MSETNX only support simple syntax

protected override void WriteImpl(PhysicalConnection physical)
{
var cmd = Command;
physical.WriteHeader(cmd, ArgCount);
if (cmd == RedisCommand.MSETEX) // need count prefix
{
physical.WriteBulkString(values.Length);
}
for (int i = 0; i < values.Length; i++)
{
physical.Write(values[i].Key);
physical.WriteBulkString(values[i].Value);
}
if (cmd == RedisCommand.MSETEX) // allow expiry/mode tokens
{
expiry.WriteTo(physical);
switch (when)
{
case When.Exists:
physical.WriteBulkString("XX"u8);
break;
case When.NotExists:
physical.WriteBulkString("NX"u8);
break;
}
}
}
}

private sealed class CommandValueChannelMessage : CommandChannelBase
{
private readonly RedisValue value;
Expand Down
6 changes: 4 additions & 2 deletions src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ StackExchange.Redis.IDatabase.StringLongestCommonSubsequenceWithMatches(StackExc
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
StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> bool
StackExchange.Redis.IDatabase.StringSet(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool
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
StackExchange.Redis.IDatabase.StringSet(System.Collections.Generic.KeyValuePair<StackExchange.Redis.RedisKey, StackExchange.Redis.RedisValue>[]! values, StackExchange.Redis.When when, StackExchange.Redis.CommandFlags flags) -> bool
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
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
StackExchange.Redis.IDatabase.StringSetBit(StackExchange.Redis.RedisKey key, long offset, bool bit, StackExchange.Redis.CommandFlags flags = StackExchange.Redis.CommandFlags.None) -> bool
Expand Down Expand Up @@ -1026,7 +1026,7 @@ StackExchange.Redis.IDatabaseAsync.StringSetAndGetAsync(StackExchange.Redis.Redi
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>!
StackExchange.Redis.IDatabaseAsync.StringSetAsync(StackExchange.Redis.RedisKey key, StackExchange.Redis.RedisValue value, System.TimeSpan? expiry, StackExchange.Redis.When when) -> System.Threading.Tasks.Task<bool>!
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>!
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>!
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>!
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>!
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>!
StackExchange.Redis.InternalErrorEventArgs
Expand Down Expand Up @@ -2052,3 +2052,5 @@ StackExchange.Redis.IServer.ExecuteAsync(int? database, string! command, System.
[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByMember(StackExchange.Redis.RedisValue member) -> StackExchange.Redis.VectorSetSimilaritySearchRequest!
[SER001]static StackExchange.Redis.VectorSetSimilaritySearchRequest.ByVector(System.ReadOnlyMemory<float> vector) -> StackExchange.Redis.VectorSetSimilaritySearchRequest!
StackExchange.Redis.RedisChannel.WithKeyRouting() -> StackExchange.Redis.RedisChannel
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
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>!
Loading
Loading