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
7 changes: 7 additions & 0 deletions src/StackExchange.Redis/RedisBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ internal virtual Task<T> ExecuteAsync<T>(Message? message, ResultProcessor<T>? p

internal virtual RedisFeatures GetFeatures(in RedisKey key, CommandFlags flags, RedisCommand command, out ServerEndPoint? server)
{
// the server picked here is the one the message is sent to, so a primary-only command has to pick the primary
// here as well, the same way Message.SetPrimaryOnly does when the message is created; otherwise PreferReplica
// picks a replica that then rejects it (DemandReplica is left alone so that it still fails the same way)
if (command.IsPrimaryOnly() && Message.GetPrimaryReplicaFlags(flags) != CommandFlags.DemandReplica)
{
flags = Message.SetPrimaryReplicaFlags(flags, CommandFlags.DemandMaster);
}
server = multiplexer.SelectServer(command, flags, key);
var version = server == null ? multiplexer.RawConfig.DefaultVersion : server.Version;
return new RedisFeatures(version);
Expand Down
31 changes: 31 additions & 0 deletions tests/StackExchange.Redis.Tests/KeyTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,37 @@ public async Task KeyTypeOfMissingKeyIsNone() // see #3156
Assert.Equal(RedisType.None, await db.KeyTypeAsync(key));
}

[Fact]
public async Task PrimaryOnlyCommandsWithPreferReplicaRunOnThePrimary() // see #2821
{
await using var conn = Create(configuration: TestConfig.Current.PrimaryServerAndPort + "," + TestConfig.Current.ReplicaServerAndPort);
Assert.SkipUnless(
conn.GetEndPoints().Any(ep => conn.GetServer(ep).IsReplica),
"No replica in this configuration");

var db = conn.GetDatabase();
var key = Me();
RedisValue token = Guid.NewGuid().ToString();

// these look at the server's features before building the message, which used to pick the replica;
// the fractional-second expiry forces the PEXPIRE path, which is the one that checks features
db.StringSet(key, "abc");
Assert.True(db.KeyExpire(key, TimeSpan.FromMilliseconds(100_500), CommandFlags.PreferReplica));
Assert.True(db.KeyDelete(key, CommandFlags.PreferReplica));

db.StringSet(key, "abc");
Assert.True(await db.KeyExpireAsync(key, TimeSpan.FromMilliseconds(100_500), CommandFlags.PreferReplica));
Assert.True(await db.KeyDeleteAsync(key, CommandFlags.PreferReplica));

Assert.True(db.LockTake(key, token, TimeSpan.FromMinutes(1)));
Assert.True(db.LockExtend(key, token, TimeSpan.FromMinutes(1), CommandFlags.PreferReplica));
Assert.True(await db.LockExtendAsync(key, token, TimeSpan.FromMinutes(1), CommandFlags.PreferReplica));
Assert.True(db.LockRelease(key, token, CommandFlags.PreferReplica));

Assert.True(db.LockTake(key, token, TimeSpan.FromMinutes(1)));
Assert.True(await db.LockReleaseAsync(key, token, CommandFlags.PreferReplica));
}

[Fact]
public async Task Zeros()
{
Expand Down
Loading