Skip to content
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

add new AddLibraryNameSuffix API for annotating connections with usage #2659

Merged
merged 7 commits into from
Feb 28, 2024
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 @@ -9,6 +9,7 @@ Current package versions:
## Unreleased

- Support `HeartbeatConsistencyChecks` and `HeartbeatInterval` in `Clone()` ([#2658 by mgravell](https://github.com/StackExchange/StackExchange.Redis/pull/2658))
- Add `AddLibraryNameSuffix` to multiplexer; allows usage-specific tokens to be appended *after connect*

## 2.7.23

Expand Down
2 changes: 1 addition & 1 deletion src/StackExchange.Redis/ConfigurationOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ public bool SetClientLibrary
/// Gets or sets the library name to use for CLIENT SETINFO lib-name calls to Redis during handshake.
/// Defaults to "SE.Redis".
/// </summary>
/// <remarks>If the value is null, empty or whitespace, then the value from the options-provideer is used;
/// <remarks>If the value is null, empty or whitespace, then the value from the options-provider is used;
/// to disable the library name feature, use <see cref="SetClientLibrary"/> instead.</remarks>
public string? LibraryName { get; set; }

Expand Down
76 changes: 76 additions & 0 deletions src/StackExchange.Redis/ConnectionMultiplexer.LibraryName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;

namespace StackExchange.Redis;

public partial class ConnectionMultiplexer
{
private readonly HashSet<string> _libraryNameSuffixHash = new();
private string _libraryNameSuffixCombined = "";

/// <inheritdoc cref="IConnectionMultiplexer.AddLibraryNameSuffix(string)" />
public void AddLibraryNameSuffix(string suffix)
{
if (string.IsNullOrWhiteSpace(suffix)) return; // trivial

// sanitize and re-check
suffix = ServerEndPoint.ClientInfoSanitize(suffix ?? "").Trim();
if (string.IsNullOrWhiteSpace(suffix)) return; // trivial

lock (_libraryNameSuffixHash)
{
if (!_libraryNameSuffixHash.Add(suffix)) return; // already cited; nothing to do

_libraryNameSuffixCombined = "-" + string.Join("-", _libraryNameSuffixHash.OrderBy(_ => _));
}

// if we get here, we *actually changed something*; we can retroactively fixup the connections
var libName = GetFullLibraryName(); // note this also checks SetClientLibrary
if (string.IsNullOrWhiteSpace(libName) || !CommandMap.IsAvailable(RedisCommand.CLIENT)) return; // disabled on no lib name

// note that during initial handshake we use raw Message; this is low frequency - no
// concern over overhead of Execute here
var args = new object[] { RedisLiterals.SETINFO, RedisLiterals.lib_name, libName };
foreach (var server in GetServers())
{
try
{
// note we can only fixup the *interactive* channel; that's tolerable here
if (server.IsConnected)
{
// best effort only
server.Execute("CLIENT", args, CommandFlags.FireAndForget);
}
}
catch (Exception ex)
{
// if an individual server trips, that's fine - best effort; note we're using
// F+F here anyway, so we don't *expect* any failures
Debug.WriteLine(ex.Message);
}
}
}

internal string GetFullLibraryName()
{
var config = RawConfig;
if (!config.SetClientLibrary) return ""; // disabled

var libName = config.LibraryName;
if (string.IsNullOrWhiteSpace(libName))
{
// defer to provider if missing (note re null vs blank; if caller wants to disable
// it, they should set SetClientLibrary to false, not set the name to empty string)
libName = config.Defaults.LibraryName;
}

libName = ServerEndPoint.ClientInfoSanitize(libName);
// if no primary name, return nothing, even if suffixes exist
if (string.IsNullOrWhiteSpace(libName)) return "";

return libName + Volatile.Read(ref _libraryNameSuffixCombined);
}
}
8 changes: 8 additions & 0 deletions src/StackExchange.Redis/Interfaces/IConnectionMultiplexer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -294,5 +294,13 @@ public interface IConnectionMultiplexer : IDisposable, IAsyncDisposable
/// <param name="destination">The destination stream to write the export to.</param>
/// <param name="options">The options to use for this export.</param>
void ExportConfiguration(Stream destination, ExportOptions options = ExportOptions.All);

/// <summary>
/// Append a usage-specific modifier to the advertised library name; suffixes are de-duplicated
/// and sorted alphabetically (so adding 'a', 'b' and 'a' will result in suffix '-a-b').
/// Connections will be updated as necessary (RESP2 subscription
/// connections will not show updates until those connections next connect).
/// </summary>
void AddLibraryNameSuffix(string suffix);
}
}
4 changes: 3 additions & 1 deletion src/StackExchange.Redis/PublicAPI/PublicAPI.Shipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1845,4 +1845,6 @@ StackExchange.Redis.ResultType.VerbatimString = 12 -> StackExchange.Redis.Result
static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisResult![]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult!
static StackExchange.Redis.RedisResult.Create(StackExchange.Redis.RedisValue[]! values, StackExchange.Redis.ResultType resultType) -> StackExchange.Redis.RedisResult!
virtual StackExchange.Redis.RedisResult.Length.get -> int
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
virtual StackExchange.Redis.RedisResult.this[int index].get -> StackExchange.Redis.RedisResult!
StackExchange.Redis.ConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void
StackExchange.Redis.IConnectionMultiplexer.AddLibraryNameSuffix(string! suffix) -> void
12 changes: 2 additions & 10 deletions src/StackExchange.Redis/ServerEndPoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log)
var config = Multiplexer.RawConfig;
string? user = config.User;
string password = config.Password ?? "";

string clientName = Multiplexer.ClientName;
if (!string.IsNullOrWhiteSpace(clientName))
{
Expand Down Expand Up @@ -1017,15 +1017,7 @@ private async Task HandshakeAsync(PhysicalConnection connection, ILogger? log)
// server version, so we will use this speculatively and hope for the best
log?.LogInformation($"{Format.ToString(this)}: Setting client lib/ver");

var libName = config.LibraryName;
if (string.IsNullOrWhiteSpace(libName))
{
// defer to provider if missing (note re null vs blank; if caller wants to disable
// it, they should set SetClientLibrary to false, not set the name to empty string)
libName = config.Defaults.LibraryName;
}

libName = ClientInfoSanitize(libName);
var libName = Multiplexer.GetFullLibraryName();
if (!string.IsNullOrWhiteSpace(libName))
{
msg = Message.Create(-1, CommandFlags.FireAndForget, RedisCommand.CLIENT,
Expand Down
35 changes: 35 additions & 0 deletions tests/StackExchange.Redis.Tests/ConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,41 @@ public void ClientName()
Assert.Equal("TestRig", name);
}

[Fact]
public async Task ClientLibraryName()
{
using var conn = Create(allowAdmin: true, shared: false);
var server = GetAnyPrimary(conn);

await server.PingAsync();
var possibleId = conn.GetConnectionId(server.EndPoint, ConnectionType.Interactive);

if (possibleId is null)
{
Log("(client id not available)");
return;
}
var id = possibleId.Value;
var libName = server.ClientList().Single(x => x.Id == id).LibraryName;
if (libName is not null) // server-version dependent
{
Log("library name: {0}", libName);
Assert.Equal("SE.Redis", libName);

conn.AddLibraryNameSuffix("foo");
conn.AddLibraryNameSuffix("bar");
conn.AddLibraryNameSuffix("foo");

libName = (await server.ClientListAsync()).Single(x => x.Id == id).LibraryName;
Log("library name: {0}", libName);
Assert.Equal("SE.Redis-bar-foo", libName);
}
else
{
Log("(library name not available)");
}
}

[Fact]
public void DefaultClientName()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public bool IgnoreConnect
public int GetSubscriptionsCount() => _inner.GetSubscriptionsCount();
public ConcurrentDictionary<RedisChannel, ConnectionMultiplexer.Subscription> GetSubscriptions() => _inner.GetSubscriptions();

public void AddLibraryNameSuffix(string suffix) => _inner.AddLibraryNameSuffix(suffix);

public string ClientName => _inner.ClientName;

public string Configuration => _inner.Configuration;
Expand Down
Loading