Skip to content
Merged
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
1 change: 1 addition & 0 deletions docs/ReleaseNotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Adds bounds checking for `ExponentialRetry` backoff policy (#1921 via gliljas)
- Adds Envoy proxy support (#1989 via rkarthick)
- When `SUBSCRIBE` is disabled, give proper errors and connect faster (#2001 via NickCraver)
- Adds `GET` on `SET` command support (present in Redis 6.2+ - #2003 via martinekvili)
- Improve concurrent load performance when backlogs are utilized (#2008 via NickCraver)

## 2.5.27 (prerelease)
Expand Down
13 changes: 13 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2202,6 +2202,19 @@ IEnumerable<SortedSetEntry> SortedSetScan(RedisKey key,
/// <remarks>https://redis.io/commands/msetnx</remarks>
bool StringSet(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Atomically sets key to value and returns the previous value (if any) stored at <paramref name="key"/>.
/// </summary>
/// <param name="key">The key of the string.</param>
/// <param name="value">The value to set.</param>
/// <param name="expiry">The expiry to set.</param>
/// <param name="when">Which condition to set the value under (defaults to <see cref="When.Always"/>).</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The previous value stored at <paramref name="key"/>, or nil when key did not exist.</returns>
/// <remarks>This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command.</remarks>
/// <remarks>https://redis.io/commands/set</remarks>
RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Sets or clears the bit at offset in the string value stored at key.
/// The bit is either set or cleared depending on value, which can be either 0 or 1.
Expand Down
13 changes: 13 additions & 0 deletions src/StackExchange.Redis/Interfaces/IDatabaseAsync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2155,6 +2155,19 @@ IAsyncEnumerable<SortedSetEntry> SortedSetScanAsync(RedisKey key,
/// <remarks>https://redis.io/commands/msetnx</remarks>
Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, When when = When.Always, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Atomically sets key to value and returns the previous value (if any) stored at <paramref name="key"/>.
/// </summary>
/// <param name="key">The key of the string.</param>
/// <param name="value">The value to set.</param>
/// <param name="expiry">The expiry to set.</param>
/// <param name="when">Which condition to set the value under (defaults to <see cref="When.Always"/>).</param>
/// <param name="flags">The flags to use for this operation.</param>
/// <returns>The previous value stored at <paramref name="key"/>, or nil when key did not exist.</returns>
/// <remarks>This method uses the SET command with the GET option introduced in Redis 6.2.0 instead of the deprecated GETSET command.</remarks>
/// <remarks>https://redis.io/commands/set</remarks>
Task<RedisValue> StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None);

/// <summary>
/// Sets or clears the bit at offset in the string value stored at key.
/// The bit is either set or cleared depending on value, which can be either 0 or 1.
Expand Down
5 changes: 5 additions & 0 deletions src/StackExchange.Redis/KeyspaceIsolation/DatabaseWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,11 @@ public bool StringSet(RedisKey key, RedisValue value, TimeSpan? expiry = null, W
return Inner.StringSet(ToInner(key), value, expiry, when, flags);
}

public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None)
{
return Inner.StringSetAndGet(ToInner(key), value, expiry, when, flags);
}

public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None)
{
return Inner.StringSetBit(ToInner(key), offset, bit, flags);
Expand Down
5 changes: 5 additions & 0 deletions src/StackExchange.Redis/KeyspaceIsolation/WrapperBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -867,6 +867,11 @@ public Task<bool> StringSetAsync(RedisKey key, RedisValue value, TimeSpan? expir
return Inner.StringSetAsync(ToInner(key), value, expiry, when, flags);
}

public Task<RedisValue> StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None)
{
return Inner.StringSetAndGetAsync(ToInner(key), value, expiry, when, flags);
}

public Task<bool> StringSetBitAsync(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None)
{
return Inner.StringSetBitAsync(ToInner(key), offset, bit, flags);
Expand Down
33 changes: 33 additions & 0 deletions src/StackExchange.Redis/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ public static Message Create(int db, CommandFlags flags, RedisCommand command, i
public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3) =>
new CommandKeyValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3);

public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) =>
new CommandKeyValueValueValueValueValueMessage(db, flags, command, key, value0, value1, value2, value3, value4);

public static Message Create(int db, CommandFlags flags, RedisCommand command, in RedisValue value0, in RedisValue value1) =>
new CommandValueValueMessage(db, flags, command, value0, value1);

Expand Down Expand Up @@ -1106,6 +1109,36 @@ protected override void WriteImpl(PhysicalConnection physical)
public override int ArgCount => 5;
}

private sealed class CommandKeyValueValueValueValueValueMessage : CommandKeyBase
{
private readonly RedisValue value0, value1, value2, value3, value4;
public CommandKeyValueValueValueValueValueMessage(int db, CommandFlags flags, RedisCommand command, in RedisKey key, in RedisValue value0, in RedisValue value1, in RedisValue value2, in RedisValue value3, in RedisValue value4) : base(db, flags, command, key)
{
value0.AssertNotNull();
value1.AssertNotNull();
value2.AssertNotNull();
value3.AssertNotNull();
value4.AssertNotNull();
this.value0 = value0;
this.value1 = value1;
this.value2 = value2;
this.value3 = value3;
this.value4 = value4;
}

protected override void WriteImpl(PhysicalConnection physical)
{
physical.WriteHeader(Command, 6);
physical.Write(Key);
physical.WriteBulkString(value0);
physical.WriteBulkString(value1);
physical.WriteBulkString(value2);
physical.WriteBulkString(value3);
physical.WriteBulkString(value4);
}
public override int ArgCount => 6;
}

private sealed class CommandMessage : Message
{
public CommandMessage(int db, CommandFlags flags, RedisCommand command) : base(db, flags, command) { }
Expand Down
49 changes: 49 additions & 0 deletions src/StackExchange.Redis/RedisDatabase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2588,6 +2588,18 @@ public Task<bool> StringSetAsync(KeyValuePair<RedisKey, RedisValue>[] values, Wh
return ExecuteAsync(msg, ResultProcessor.Boolean);
}

public RedisValue StringSetAndGet(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None)
{
var msg = GetStringSetAndGetMessage(key, value, expiry, when, flags);
return ExecuteSync(msg, ResultProcessor.RedisValue);
}

public Task<RedisValue> StringSetAndGetAsync(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None)
{
var msg = GetStringSetAndGetMessage(key, value, expiry, when, flags);
return ExecuteAsync(msg, ResultProcessor.RedisValue);
}

public bool StringSetBit(RedisKey key, long offset, bool bit, CommandFlags flags = CommandFlags.None)
{
var msg = Message.Create(Database, flags, RedisCommand.SETBIT, key, offset, bit);
Expand Down Expand Up @@ -3527,6 +3539,43 @@ private Message GetStringSetMessage(RedisKey key, RedisValue value, TimeSpan? ex
};
}

private Message GetStringSetAndGetMessage(RedisKey key, RedisValue value, TimeSpan? expiry = null, When when = When.Always, CommandFlags flags = CommandFlags.None)
{
WhenAlwaysOrExistsOrNotExists(when);
if (value.IsNull) return Message.Create(Database, flags, RedisCommand.GETDEL, key);

if (expiry == null || expiry.Value == TimeSpan.MaxValue)
{ // no expiry
switch (when)
{
case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.GET);
case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.XX, RedisLiterals.GET);
case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.NX, RedisLiterals.GET);
}
}
long milliseconds = expiry.Value.Ticks / TimeSpan.TicksPerMillisecond;

if ((milliseconds % 1000) == 0)
{
// a nice round number of seconds
long seconds = milliseconds / 1000;
switch (when)
{
case When.Always: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.GET);
case When.Exists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.XX, RedisLiterals.GET);
case When.NotExists: return Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.EX, seconds, RedisLiterals.NX, RedisLiterals.GET);
}
}

return when switch
{
When.Always => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.GET),
When.Exists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.XX, RedisLiterals.GET),
When.NotExists => Message.Create(Database, flags, RedisCommand.SET, key, value, RedisLiterals.PX, milliseconds, RedisLiterals.NX, RedisLiterals.GET),
_ => throw new NotSupportedException(),
};
}

private Message IncrMessage(RedisKey key, long value, CommandFlags flags)
{
switch (value)
Expand Down
13 changes: 12 additions & 1 deletion src/StackExchange.Redis/RedisFeatures.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ public readonly struct RedisFeatures
v4_0_0 = new Version(4, 0, 0),
v4_9_1 = new Version(4, 9, 1), // 5.0 RC1 is version 4.9.1; // 5.0 RC1 is version 4.9.1
v5_0_0 = new Version(5, 0, 0),
v6_2_0 = new Version(6, 2, 0);
v6_2_0 = new Version(6, 2, 0),
v6_9_240 = new Version(6, 9, 240); // 7.0 RC1 is version 6.9.240

private readonly Version version;

Expand Down Expand Up @@ -74,6 +75,16 @@ public RedisFeatures(Version version)
/// </summary>
public bool GetDelete => Version >= v6_2_0;

/// <summary>
/// Does SET support the GET option?
/// </summary>
public bool SetAndGet => Version >= v6_2_0;

/// <summary>
/// Does SET allow the NX and GET options to be used together?
/// </summary>
public bool SetNotExistsAndGet => Version >= v6_9_240;

/// <summary>
/// Is HSTRLEN available?
/// </summary>
Expand Down
93 changes: 93 additions & 0 deletions tests/StackExchange.Redis.Tests/Strings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,99 @@ public async Task SetNotExists()
}
}

[Fact]
public async Task SetAndGet()
{
using (var muxer = Create())
{
Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SetAndGet), r => r.SetAndGet);

var conn = muxer.GetDatabase();
var prefix = Me();
conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "4", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "5", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "6", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "7", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "8", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "9", CommandFlags.FireAndForget);
conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget);
conn.StringSet(prefix + "2", "abc", flags: CommandFlags.FireAndForget);
conn.StringSet(prefix + "4", "abc", flags: CommandFlags.FireAndForget);
conn.StringSet(prefix + "6", "abc", flags: CommandFlags.FireAndForget);
conn.StringSet(prefix + "7", "abc", flags: CommandFlags.FireAndForget);
conn.StringSet(prefix + "8", "abc", flags: CommandFlags.FireAndForget);
conn.StringSet(prefix + "9", "abc", flags: CommandFlags.FireAndForget);

var x0 = conn.StringSetAndGetAsync(prefix + "1", RedisValue.Null);
var x1 = conn.StringSetAndGetAsync(prefix + "2", "def");
var x2 = conn.StringSetAndGetAsync(prefix + "3", "def");
var x3 = conn.StringSetAndGetAsync(prefix + "4", "def", when: When.Exists);
var x4 = conn.StringSetAndGetAsync(prefix + "5", "def", when: When.Exists);
var x5 = conn.StringSetAndGetAsync(prefix + "6", "def", expiry: TimeSpan.FromSeconds(4));
var x6 = conn.StringSetAndGetAsync(prefix + "7", "def", expiry: TimeSpan.FromMilliseconds(4001));
var x7 = conn.StringSetAndGetAsync(prefix + "8", "def", expiry: TimeSpan.FromSeconds(4), when: When.Exists);
var x8 = conn.StringSetAndGetAsync(prefix + "9", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.Exists);

var s0 = conn.StringGetAsync(prefix + "1");
var s1 = conn.StringGetAsync(prefix + "2");
var s2 = conn.StringGetAsync(prefix + "3");
var s3 = conn.StringGetAsync(prefix + "4");
var s4 = conn.StringGetAsync(prefix + "5");

Assert.Equal("abc", await x0);
Assert.Equal("abc", await x1);
Assert.Equal(RedisValue.Null, await x2);
Assert.Equal("abc", await x3);
Assert.Equal(RedisValue.Null, await x4);
Assert.Equal("abc", await x5);
Assert.Equal("abc", await x6);
Assert.Equal("abc", await x7);
Assert.Equal("abc", await x8);

Assert.Equal(RedisValue.Null, await s0);
Assert.Equal("def", await s1);
Assert.Equal("def", await s2);
Assert.Equal("def", await s3);
Assert.Equal(RedisValue.Null, await s4);
}
}

[Fact]
public async Task SetNotExistsAndGet()
{
using (var muxer = Create())
{
Skip.IfMissingFeature(muxer, nameof(RedisFeatures.SetNotExistsAndGet), r => r.SetNotExistsAndGet);

var conn = muxer.GetDatabase();
var prefix = Me();
conn.KeyDelete(prefix + "1", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "2", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "3", CommandFlags.FireAndForget);
conn.KeyDelete(prefix + "4", CommandFlags.FireAndForget);
conn.StringSet(prefix + "1", "abc", flags: CommandFlags.FireAndForget);

var x0 = conn.StringSetAndGetAsync(prefix + "1", "def", when: When.NotExists);
var x1 = conn.StringSetAndGetAsync(prefix + "2", "def", when: When.NotExists);
var x2 = conn.StringSetAndGetAsync(prefix + "3", "def", expiry: TimeSpan.FromSeconds(4), when: When.NotExists);
var x3 = conn.StringSetAndGetAsync(prefix + "4", "def", expiry: TimeSpan.FromMilliseconds(4001), when: When.NotExists);

var s0 = conn.StringGetAsync(prefix + "1");
var s1 = conn.StringGetAsync(prefix + "2");

Assert.Equal("abc", await x0);
Assert.Equal(RedisValue.Null, await x1);
Assert.Equal(RedisValue.Null, await x2);
Assert.Equal(RedisValue.Null, await x3);

Assert.Equal("abc", await s0);
Assert.Equal("def", await s1);
}
}

[Fact]
public async Task Ranges()
{
Expand Down