Skip to content

feat: add Connection method to RedisStoreBuilder for external Redis connections #147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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 pkgs/dotnet-server-sdk-redis/dotnet-core
Submodule dotnet-core added at 40e641
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ internal sealed class RedisBigSegmentStoreImpl : RedisStoreImplBase, IBigSegment
private readonly string _excludedKeyPrefix;

internal RedisBigSegmentStoreImpl(
ConfigurationOptions redisConfig,
IConnectionMultiplexer redis,
string prefix,
Logger log
) : base(redisConfig, prefix, log)
) : base(redis, prefix, log)
{
_syncTimeKey = prefix + ":big_segments_synchronized_on";
_includedKeyPrefix = prefix + ":big_segment_include:";
Expand Down
4 changes: 2 additions & 2 deletions pkgs/dotnet-server-sdk-redis/src/RedisDataStoreImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ internal sealed class RedisDataStoreImpl : RedisStoreImplBase, IPersistentDataSt
private readonly string _initedKey;

internal RedisDataStoreImpl(
ConfigurationOptions redisConfig,
IConnectionMultiplexer redis,
string prefix,
Logger log
) : base(redisConfig, prefix, log)
) : base(redis, prefix, log)
{
_initedKey = prefix + ":$inited";
}
Expand Down
58 changes: 54 additions & 4 deletions pkgs/dotnet-server-sdk-redis/src/RedisStoreBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using LaunchDarkly.Logging;
using LaunchDarkly.Sdk.Server.Subsystems;
using StackExchange.Redis;

Expand Down Expand Up @@ -57,6 +59,7 @@ namespace LaunchDarkly.Sdk.Server.Integrations
public abstract class RedisStoreBuilder<T> : IComponentConfigurer<T>, IDiagnosticDescription
{
internal ConfigurationOptions _redisConfig = new ConfigurationOptions();
internal IConnectionMultiplexer _externalConnection;
internal string _prefix = Redis.DefaultPrefix;

internal RedisStoreBuilder()
Expand Down Expand Up @@ -224,6 +227,47 @@ public RedisStoreBuilder<T> RedisConfigChanges(Action<ConfigurationOptions> modi
return this;
}

/// <summary>
/// Specifies a pre-configured Redis connection multiplexer to use instead of creating one
/// from configuration options. This allows for advanced scenarios such as Azure AAD authentication.
/// </summary>
/// <param name="connection">the pre-configured connection multiplexer</param>
/// <returns>the builder</returns>
public RedisStoreBuilder<T> Connection(IConnectionMultiplexer connection)
{
_externalConnection = connection ?? throw new ArgumentNullException(nameof(connection));
return this;
}

/// <summary>
/// Gets the connection to use - either the externally provided one or creates a new one from configuration.
/// </summary>
/// <param name="log">Logger for connection creation</param>
/// <returns>The Redis connection multiplexer to use</returns>
protected IConnectionMultiplexer GetOrCreateConnection(Logger log)
{
if (_externalConnection != null)
{
log.Info("Using pre-configured Redis connection with prefix \"{0}\"", _prefix);
return _externalConnection;
}

log.Info("Creating Redis connection to {0} with prefix \"{1}\"",
string.Join(", ", _redisConfig.EndPoints.Select(DescribeEndPoint)), _prefix);

var redisConfigCopy = _redisConfig.Clone();
return ConnectionMultiplexer.Connect(redisConfigCopy);
}

private string DescribeEndPoint(EndPoint e)
{
// The default ToString() method of DnsEndPoint adds a prefix of "Unspecified", which looks
// confusing in our log messages.
return (e is DnsEndPoint de) ?
string.Format("{0}:{1}", de.Host, de.Port) :
e.ToString();
}

/// <inheritdoc/>
public abstract T Build(LdClientContext context);

Expand All @@ -234,13 +278,19 @@ public LdValue DescribeConfiguration(LdClientContext context) =>

internal sealed class BuilderForDataStore : RedisStoreBuilder<IPersistentDataStore>
{
public override IPersistentDataStore Build(LdClientContext context) =>
new RedisDataStoreImpl(_redisConfig, _prefix, context.Logger.SubLogger("DataStore.Redis"));
public override IPersistentDataStore Build(LdClientContext context)
{
var connection = GetOrCreateConnection(context.Logger.SubLogger("DataStore.Redis"));
return new RedisDataStoreImpl(connection, _prefix, context.Logger.SubLogger("DataStore.Redis"));
}
}

internal sealed class BuilderForBigSegments : RedisStoreBuilder<IBigSegmentStore>
{
public override IBigSegmentStore Build(LdClientContext context) =>
new RedisBigSegmentStoreImpl(_redisConfig, _prefix, context.Logger.SubLogger("BigSegments.Redis"));
public override IBigSegmentStore Build(LdClientContext context)
{
var connection = GetOrCreateConnection(context.Logger.SubLogger("BigSegments.Redis"));
return new RedisBigSegmentStoreImpl(connection, _prefix, context.Logger.SubLogger("BigSegments.Redis"));
}
}
}
18 changes: 4 additions & 14 deletions pkgs/dotnet-server-sdk-redis/src/RedisStoreImplBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,20 @@ namespace LaunchDarkly.Sdk.Server.Integrations
{
internal abstract class RedisStoreImplBase : IDisposable
{
protected readonly ConnectionMultiplexer _redis;
protected readonly IConnectionMultiplexer _redis;
protected readonly string _prefix;
protected readonly Logger _log;

protected RedisStoreImplBase(
ConfigurationOptions redisConfig,
IConnectionMultiplexer redis,
string prefix,
Logger log
)
{
_log = log;
var redisConfigCopy = redisConfig.Clone();
_redis = ConnectionMultiplexer.Connect(redisConfigCopy);
_redis = redis;
_prefix = prefix;
_log.Info("Using Redis data store at {0} with prefix \"{1}\"",
string.Join(", ", redisConfig.EndPoints.Select(DescribeEndPoint)), prefix);
_log.Info("Using Redis connection with prefix \"{0}\"", prefix);
}

public void Dispose()
Expand All @@ -42,13 +40,5 @@ private void Dispose(bool disposing)
}
}

private string DescribeEndPoint(EndPoint e)
{
// The default ToString() method of DnsEndPoint adds a prefix of "Unspecified", which looks
// confusing in our log messages.
return (e is DnsEndPoint de) ?
string.Format("{0}:{1}", de.Host, de.Port) :
e.ToString();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Reflection;
using LaunchDarkly.Sdk.Server.Subsystems;
using StackExchange.Redis;
using Xunit;

namespace LaunchDarkly.Sdk.Server.Integrations
{
public class RedisBigSegmentStoreBuilderTest
{
[Fact]
public void ConnectionWithNull()
{
var builder = Redis.BigSegmentStore();

// Setting null connection should throw ArgumentNullException
Assert.Throws<ArgumentNullException>(() => builder.Connection(null));
}

[Fact]
public void ConnectionMethodExists()
{
var builder = Redis.BigSegmentStore();

// Initially no external connection
Assert.Null(builder._externalConnection);

// Test that the Connection method exists by checking if we can call it
// Use reflection to verify the method exists without requiring a real connection
var connectionMethod = typeof(RedisStoreBuilder<IBigSegmentStore>)
.GetMethod("Connection", new[] { typeof(IConnectionMultiplexer) });

Assert.NotNull(connectionMethod);
Assert.Equal(typeof(RedisStoreBuilder<IBigSegmentStore>), connectionMethod.ReturnType);
}

[Fact]
public void ConnectionWorksWithOtherBuilderMethods()
{
var builder = Redis.BigSegmentStore();

// Chain with other builder methods
builder.Prefix("test-prefix");

Assert.Equal("test-prefix", builder._prefix);
}
}
}
44 changes: 44 additions & 0 deletions pkgs/dotnet-server-sdk-redis/test/RedisDataStoreBuilderTest.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using LaunchDarkly.Sdk.Server.Subsystems;
using StackExchange.Redis;
using Xunit;

namespace LaunchDarkly.Sdk.Server.Integrations
Expand Down Expand Up @@ -113,5 +116,46 @@ public void Prefix()
builder.Prefix(null);
Assert.Equal(Redis.DefaultPrefix, builder._prefix);
}

[Fact]
public void ConnectionWithNull()
{
var builder = Redis.DataStore();

// Setting null connection should throw ArgumentNullException
Assert.Throws<ArgumentNullException>(() => builder.Connection(null));
}

[Fact]
public void ConnectionMethodExists()
{
var builder = Redis.DataStore();

// Initially no external connection
Assert.Null(builder._externalConnection);

// Test that the Connection method exists by checking if we can call it
// Use reflection to verify the method exists without requiring a real connection
var connectionMethod = typeof(RedisStoreBuilder<IPersistentDataStore>)
.GetMethod("Connection", new[] { typeof(IConnectionMultiplexer) });

Assert.NotNull(connectionMethod);
Assert.Equal(typeof(RedisStoreBuilder<IPersistentDataStore>), connectionMethod.ReturnType);
}

[Fact]
public void ConnectionWorksWithOtherBuilderMethods()
{
var builder = Redis.DataStore();

// Chain with other builder methods
builder.HostAndPort("test", 9999)
.Prefix("test-prefix");

// Config should be set
Assert.Collection(builder._redisConfig.EndPoints,
e => Assert.Equal(new DnsEndPoint("test", 9999), e));
Assert.Equal("test-prefix", builder._prefix);
}
}
}
10 changes: 8 additions & 2 deletions pkgs/dotnet-server-sdk-redis/test/RedisDataStoreTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,16 @@ public void LogMessageAtStartup()
{
Assert.Equal(LogLevel.Info, m.Level);
Assert.Equal("BaseLoggerName.DataStore.Redis", m.LoggerName);
Assert.Equal("Using Redis data store at localhost:6379 with prefix \"my-prefix\"",
m.Text);
Assert.Contains("Creating Redis connection to localhost:6379 with prefix \"my-prefix\"", m.Text);
},
m =>
{
Assert.Equal(LogLevel.Info, m.Level);
Assert.Equal("BaseLoggerName.DataStore.Redis", m.LoggerName);
Assert.Equal("Using Redis connection with prefix \"my-prefix\"", m.Text);
});
}
}

}
}