Skip to content

Commit 7ad0add

Browse files
authored
DefaultOptionsProvider: allow providing User/Password (#2445)
For wrappers which intend to provide this (e.g. managed service accounts and such), the intent was for them to override these and return their "current" values (e.g. as a token rotates), but I missed them in the initial pass...even though this was the original extensibility reason, because I suck! Fixing.
1 parent 571d832 commit 7ad0add

File tree

8 files changed

+141
-119
lines changed

8 files changed

+141
-119
lines changed

docs/ReleaseNotes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Current package versions:
99
## Unreleased
1010

1111
- Fix [#2426](https://github.com/StackExchange/StackExchange.Redis/issues/2426): Don't restrict multi-slot operations on Envoy proxy; let the proxy decide ([#2428 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2428))
12+
- Add: Support for `User`/`Password` in `DefaultOptionsProvider` to support token rotation scenarios ([#2445 by NickCraver](https://github.com/StackExchange/StackExchange.Redis/pull/2445))
1213

1314
## 2.6.104
1415

src/StackExchange.Redis/Configuration/DefaultOptionsProvider.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ public static void AddProvider(DefaultOptionsProvider provider)
172172
/// </summary>
173173
public virtual TimeSpan ConfigCheckInterval => TimeSpan.FromMinutes(1);
174174

175+
/// <summary>
176+
/// The username to use to authenticate with the server.
177+
/// </summary>
178+
public virtual string? User => null;
179+
180+
/// <summary>
181+
/// The password to use to authenticate with the server.
182+
/// </summary>
183+
public virtual string? Password => null;
184+
175185
// We memoize this to reduce cost on re-access
176186
private string? defaultClientName;
177187
/// <summary>

src/StackExchange.Redis/ConfigurationOptions.cs

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ public static string TryNormalize(string value)
145145
private bool? allowAdmin, abortOnConnectFail, resolveDns, ssl, checkCertificateRevocation,
146146
includeDetailInExceptions, includePerformanceCountersInExceptions, setClientLibrary;
147147

148-
private string? tieBreaker, sslHost, configChannel;
148+
private string? tieBreaker, sslHost, configChannel, user, password;
149149

150150
private TimeSpan? heartbeatInterval;
151151

@@ -440,14 +440,22 @@ public int KeepAlive
440440
}
441441

442442
/// <summary>
443-
/// The user to use to authenticate with the server.
443+
/// The username to use to authenticate with the server.
444444
/// </summary>
445-
public string? User { get; set; }
445+
public string? User
446+
{
447+
get => user ?? Defaults.User;
448+
set => user = value;
449+
}
446450

447451
/// <summary>
448452
/// The password to use to authenticate with the server.
449453
/// </summary>
450-
public string? Password { get; set; }
454+
public string? Password
455+
{
456+
get => password ?? Defaults.Password;
457+
set => password = value;
458+
}
451459

452460
/// <summary>
453461
/// Specifies whether asynchronous operations should be invoked in a way that guarantees their original delivery order.
@@ -634,8 +642,8 @@ public static ConfigurationOptions Parse(string configuration, bool ignoreUnknow
634642
allowAdmin = allowAdmin,
635643
defaultVersion = defaultVersion,
636644
connectTimeout = connectTimeout,
637-
User = User,
638-
Password = Password,
645+
user = user,
646+
password = password,
639647
tieBreaker = tieBreaker,
640648
ssl = ssl,
641649
sslHost = sslHost,
@@ -726,8 +734,8 @@ public string ToString(bool includePassword)
726734
Append(sb, OptionKeys.AllowAdmin, allowAdmin);
727735
Append(sb, OptionKeys.Version, defaultVersion);
728736
Append(sb, OptionKeys.ConnectTimeout, connectTimeout);
729-
Append(sb, OptionKeys.User, User);
730-
Append(sb, OptionKeys.Password, (includePassword || string.IsNullOrEmpty(Password)) ? Password : "*****");
737+
Append(sb, OptionKeys.User, user);
738+
Append(sb, OptionKeys.Password, (includePassword || string.IsNullOrEmpty(password)) ? password : "*****");
731739
Append(sb, OptionKeys.TieBreaker, tieBreaker);
732740
Append(sb, OptionKeys.Ssl, ssl);
733741
Append(sb, OptionKeys.SslProtocols, SslProtocols?.ToString().Replace(',', '|'));
@@ -778,7 +786,7 @@ private static void Append(StringBuilder sb, string prefix, object? value)
778786

779787
private void Clear()
780788
{
781-
ClientName = ServiceName = User = Password = tieBreaker = sslHost = configChannel = null;
789+
ClientName = ServiceName = user = password = tieBreaker = sslHost = configChannel = null;
782790
keepAlive = syncTimeout = asyncTimeout = connectTimeout = connectRetry = configCheckSeconds = DefaultDatabase = null;
783791
allowAdmin = abortOnConnectFail = resolveDns = ssl = setClientLibrary = null;
784792
SslProtocols = null;
@@ -873,10 +881,10 @@ private ConfigurationOptions DoParse(string configuration, bool ignoreUnknown)
873881
DefaultVersion = OptionKeys.ParseVersion(key, value);
874882
break;
875883
case OptionKeys.User:
876-
User = value;
884+
user = value;
877885
break;
878886
case OptionKeys.Password:
879-
Password = value;
887+
password = value;
880888
break;
881889
case OptionKeys.TieBreaker:
882890
TieBreaker = value;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,9 +1788,11 @@ virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludeDetailIn
17881788
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IncludePerformanceCountersInExceptions.get -> bool
17891789
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.IsMatch(System.Net.EndPoint! endpoint) -> bool
17901790
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.KeepAliveInterval.get -> System.TimeSpan
1791+
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Password.get -> string?
17911792
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.Proxy.get -> StackExchange.Redis.Proxy
17921793
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ReconnectRetryPolicy.get -> StackExchange.Redis.IReconnectRetryPolicy?
17931794
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.ResolveDns.get -> bool
17941795
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SetClientLibrary.get -> bool
17951796
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.SyncTimeout.get -> System.TimeSpan
17961797
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.TieBreaker.get -> string!
1798+
virtual StackExchange.Redis.Configuration.DefaultOptionsProvider.User.get -> string?

tests/StackExchange.Redis.Tests/CommandTimeoutTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public async Task DefaultHeartbeatTimeout()
3535
await pauseTask;
3636
}
3737

38+
#if DEBUG
3839
[Fact]
3940
public async Task DefaultHeartbeatLowTimeout()
4041
{
@@ -60,4 +61,5 @@ public async Task DefaultHeartbeatLowTimeout()
6061
// Await as to not bias the next test
6162
await pauseTask;
6263
}
64+
#endif
6365
}

tests/StackExchange.Redis.Tests/DefaultOptionsTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public class TestOptionsProvider : DefaultOptionsProvider
3636
public override bool ResolveDns => true;
3737
public override TimeSpan SyncTimeout => TimeSpan.FromSeconds(126);
3838
public override string TieBreaker => "TestTiebreaker";
39+
public override string? User => "TestUser";
40+
public override string? Password => "TestPassword";
3941
}
4042

4143
public class TestRetryPolicy : IReconnectRetryPolicy
@@ -99,6 +101,8 @@ private static void AssertAllOverrides(ConfigurationOptions options)
99101
Assert.True(options.ResolveDns);
100102
Assert.Equal(TimeSpan.FromSeconds(126), TimeSpan.FromMilliseconds(options.SyncTimeout));
101103
Assert.Equal("TestTiebreaker", options.TieBreaker);
104+
Assert.Equal("TestUser", options.User);
105+
Assert.Equal("TestPassword", options.Password);
102106
}
103107

104108
public class TestAfterConnectOptionsProvider : DefaultOptionsProvider

tests/StackExchange.Redis.Tests/FailoverTests.cs

Lines changed: 103 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace StackExchange.Redis.Tests;
99

10+
[Collection(NonParallelCollection.Name)]
1011
public class FailoverTests : TestBase, IAsyncLifetime
1112
{
1213
protected override string GetConfiguration() => GetPrimaryReplicaConfig().ToString();
@@ -196,6 +197,104 @@ public async Task DereplicateGoesToPrimary()
196197
}
197198

198199
#if DEBUG
200+
[Fact]
201+
public async Task SubscriptionsSurviveConnectionFailureAsync()
202+
{
203+
using var conn = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!;
204+
205+
var profiler = conn.AddProfiler();
206+
RedisChannel channel = Me();
207+
var sub = conn.GetSubscriber();
208+
int counter = 0;
209+
Assert.True(sub.IsConnected());
210+
await sub.SubscribeAsync(channel, delegate
211+
{
212+
Interlocked.Increment(ref counter);
213+
}).ConfigureAwait(false);
214+
215+
var profile1 = Log(profiler);
216+
217+
Assert.Equal(1, conn.GetSubscriptionsCount());
218+
219+
await Task.Delay(200).ConfigureAwait(false);
220+
221+
await sub.PublishAsync(channel, "abc").ConfigureAwait(false);
222+
sub.Ping();
223+
await Task.Delay(200).ConfigureAwait(false);
224+
225+
var counter1 = Thread.VolatileRead(ref counter);
226+
Log($"Expecting 1 message, got {counter1}");
227+
Assert.Equal(1, counter1);
228+
229+
var server = GetServer(conn);
230+
var socketCount = server.GetCounters().Subscription.SocketCount;
231+
Log($"Expecting 1 socket, got {socketCount}");
232+
Assert.Equal(1, socketCount);
233+
234+
// We might fail both connections or just the primary in the time period
235+
SetExpectedAmbientFailureCount(-1);
236+
237+
// Make sure we fail all the way
238+
conn.AllowConnect = false;
239+
Log("Failing connection");
240+
// Fail all connections
241+
server.SimulateConnectionFailure(SimulatedFailureType.All);
242+
// Trigger failure (RedisTimeoutException or RedisConnectionException because
243+
// of backlog behavior)
244+
var ex = Assert.ThrowsAny<Exception>(() => sub.Ping());
245+
Assert.True(ex is RedisTimeoutException or RedisConnectionException);
246+
Assert.False(sub.IsConnected(channel));
247+
248+
// Now reconnect...
249+
conn.AllowConnect = true;
250+
Log("Waiting on reconnect");
251+
// Wait until we're reconnected
252+
await UntilConditionAsync(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel));
253+
Log("Reconnected");
254+
// Ensure we're reconnected
255+
Assert.True(sub.IsConnected(channel));
256+
257+
// Ensure we've sent the subscribe command after reconnecting
258+
var profile2 = Log(profiler);
259+
//Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE)));
260+
261+
Log("Issuing ping after reconnected");
262+
sub.Ping();
263+
264+
var muxerSubCount = conn.GetSubscriptionsCount();
265+
Log($"Muxer thinks we have {muxerSubCount} subscriber(s).");
266+
Assert.Equal(1, muxerSubCount);
267+
268+
var muxerSubs = conn.GetSubscriptions();
269+
foreach (var pair in muxerSubs)
270+
{
271+
var muxerSub = pair.Value;
272+
Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetCurrentServer()}, Connected: {muxerSub.IsConnected})");
273+
}
274+
275+
Log("Publishing");
276+
var published = await sub.PublishAsync(channel, "abc").ConfigureAwait(false);
277+
278+
Log($"Published to {published} subscriber(s).");
279+
Assert.Equal(1, published);
280+
281+
// Give it a few seconds to get our messages
282+
Log("Waiting for 2 messages");
283+
await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2);
284+
285+
var counter2 = Thread.VolatileRead(ref counter);
286+
Log($"Expecting 2 messages, got {counter2}");
287+
Assert.Equal(2, counter2);
288+
289+
// Log all commands at the end
290+
Log("All commands since connecting:");
291+
var profile3 = profiler.FinishProfiling();
292+
foreach (var command in profile3)
293+
{
294+
Log($"{command.EndPoint}: {command}");
295+
}
296+
}
297+
199298
[Fact]
200299
public async Task SubscriptionsSurvivePrimarySwitchAsync()
201300
{
@@ -215,14 +314,8 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync()
215314
var subB = bConn.GetSubscriber();
216315

217316
long primaryChanged = 0, aCount = 0, bCount = 0;
218-
aConn.ConfigurationChangedBroadcast += delegate
219-
{
220-
Log("A noticed config broadcast: " + Interlocked.Increment(ref primaryChanged));
221-
};
222-
bConn.ConfigurationChangedBroadcast += delegate
223-
{
224-
Log("B noticed config broadcast: " + Interlocked.Increment(ref primaryChanged));
225-
};
317+
aConn.ConfigurationChangedBroadcast += (s, args) => Log("A noticed config broadcast: " + Interlocked.Increment(ref primaryChanged) + " (Endpoint:" + args.EndPoint + ")");
318+
bConn.ConfigurationChangedBroadcast += (s, args) => Log("B noticed config broadcast: " + Interlocked.Increment(ref primaryChanged) + " (Endpoint:" + args.EndPoint + ")");
226319
subA.Subscribe(channel, (_, message) =>
227320
{
228321
Log("A got message: " + message);
@@ -333,8 +426,8 @@ public async Task SubscriptionsSurvivePrimarySwitchAsync()
333426

334427
Assert.Equal(2, Interlocked.Read(ref aCount));
335428
Assert.Equal(2, Interlocked.Read(ref bCount));
336-
// Expect 12, because a sees a, but b sees a and b due to replication
337-
Assert.Equal(12, Interlocked.CompareExchange(ref primaryChanged, 0, 0));
429+
// Expect 12, because a sees a, but b sees a and b due to replication, but contenders may add their own
430+
Assert.True(Interlocked.CompareExchange(ref primaryChanged, 0, 0) >= 12);
338431
}
339432
catch
340433
{

tests/StackExchange.Redis.Tests/PubSubTests.cs

Lines changed: 0 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -782,102 +782,4 @@ public async Task AzureRedisEventsAutomaticSubscribe()
782782
Assert.True(didUpdate);
783783
}
784784
}
785-
786-
[Fact]
787-
public async Task SubscriptionsSurviveConnectionFailureAsync()
788-
{
789-
using var conn = (Create(allowAdmin: true, shared: false, log: Writer, syncTimeout: 1000) as ConnectionMultiplexer)!;
790-
791-
var profiler = conn.AddProfiler();
792-
RedisChannel channel = Me();
793-
var sub = conn.GetSubscriber();
794-
int counter = 0;
795-
Assert.True(sub.IsConnected());
796-
await sub.SubscribeAsync(channel, delegate
797-
{
798-
Interlocked.Increment(ref counter);
799-
}).ConfigureAwait(false);
800-
801-
var profile1 = Log(profiler);
802-
803-
Assert.Equal(1, conn.GetSubscriptionsCount());
804-
805-
await Task.Delay(200).ConfigureAwait(false);
806-
807-
await sub.PublishAsync(channel, "abc").ConfigureAwait(false);
808-
sub.Ping();
809-
await Task.Delay(200).ConfigureAwait(false);
810-
811-
var counter1 = Thread.VolatileRead(ref counter);
812-
Log($"Expecting 1 message, got {counter1}");
813-
Assert.Equal(1, counter1);
814-
815-
var server = GetServer(conn);
816-
var socketCount = server.GetCounters().Subscription.SocketCount;
817-
Log($"Expecting 1 socket, got {socketCount}");
818-
Assert.Equal(1, socketCount);
819-
820-
// We might fail both connections or just the primary in the time period
821-
SetExpectedAmbientFailureCount(-1);
822-
823-
// Make sure we fail all the way
824-
conn.AllowConnect = false;
825-
Log("Failing connection");
826-
// Fail all connections
827-
server.SimulateConnectionFailure(SimulatedFailureType.All);
828-
// Trigger failure (RedisTimeoutException or RedisConnectionException because
829-
// of backlog behavior)
830-
var ex = Assert.ThrowsAny<Exception>(() => sub.Ping());
831-
Assert.True(ex is RedisTimeoutException or RedisConnectionException);
832-
Assert.False(sub.IsConnected(channel));
833-
834-
// Now reconnect...
835-
conn.AllowConnect = true;
836-
Log("Waiting on reconnect");
837-
// Wait until we're reconnected
838-
await UntilConditionAsync(TimeSpan.FromSeconds(10), () => sub.IsConnected(channel));
839-
Log("Reconnected");
840-
// Ensure we're reconnected
841-
Assert.True(sub.IsConnected(channel));
842-
843-
// Ensure we've sent the subscribe command after reconnecting
844-
var profile2 = Log(profiler);
845-
//Assert.Equal(1, profile2.Count(p => p.Command == nameof(RedisCommand.SUBSCRIBE)));
846-
847-
Log("Issuing ping after reconnected");
848-
sub.Ping();
849-
850-
var muxerSubCount = conn.GetSubscriptionsCount();
851-
Log($"Muxer thinks we have {muxerSubCount} subscriber(s).");
852-
Assert.Equal(1, muxerSubCount);
853-
854-
var muxerSubs = conn.GetSubscriptions();
855-
foreach (var pair in muxerSubs)
856-
{
857-
var muxerSub = pair.Value;
858-
Log($" Muxer Sub: {pair.Key}: (EndPoint: {muxerSub.GetCurrentServer()}, Connected: {muxerSub.IsConnected})");
859-
}
860-
861-
Log("Publishing");
862-
var published = await sub.PublishAsync(channel, "abc").ConfigureAwait(false);
863-
864-
Log($"Published to {published} subscriber(s).");
865-
Assert.Equal(1, published);
866-
867-
// Give it a few seconds to get our messages
868-
Log("Waiting for 2 messages");
869-
await UntilConditionAsync(TimeSpan.FromSeconds(5), () => Thread.VolatileRead(ref counter) == 2);
870-
871-
var counter2 = Thread.VolatileRead(ref counter);
872-
Log($"Expecting 2 messages, got {counter2}");
873-
Assert.Equal(2, counter2);
874-
875-
// Log all commands at the end
876-
Log("All commands since connecting:");
877-
var profile3 = profiler.FinishProfiling();
878-
foreach (var command in profile3)
879-
{
880-
Log($"{command.EndPoint}: {command}");
881-
}
882-
}
883785
}

0 commit comments

Comments
 (0)