Skip to content

Commit

Permalink
add new AddLibraryNameSuffix API for annotating connections with usage (
Browse files Browse the repository at this point in the history
#2659)

* add new AddLibraryNameSuffix API for annotating connections with usage

* fixup test

* new partial for lib-name bits

* use hashing rather than array shenanigans

* move to shipped; fix comment typo

* comment example

* reverse comment owner
  • Loading branch information
mgravell committed Feb 28, 2024
1 parent a517561 commit 18c057b
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 12 deletions.
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

0 comments on commit 18c057b

Please sign in to comment.