From d901d4f34a9f945ebb6198bbaffeac1fa83dc039 Mon Sep 17 00:00:00 2001 From: mohsinaliwork686-rgb <229973101+mohsinaliwork686-rgb@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:33:15 +0500 Subject: [PATCH] Profile commands sent in a batch (#2175) --- .../ConnectionMultiplexer.Profiling.cs | 14 +++++++++ .../ConnectionMultiplexer.cs | 7 +---- src/StackExchange.Redis/RedisBatch.cs | 6 ++++ .../ProfilingTests.cs | 29 +++++++++++++++++++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs index c60966234b..a7ab10bf25 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.Profiling.cs @@ -14,4 +14,18 @@ public partial class ConnectionMultiplexer /// /// The session provider to register. public void RegisterProfiler(Func profilingSessionProvider) => _profilingSessionProvider = profilingSessionProvider; + + /// + /// Gives the message somewhere to record its timings, if a profiler is registered and has a session for the + /// calling context. Every path that hands a message to a server calls this once the server is known: single + /// commands via PrepareToPushMessageToBridge, and batches, which go to the bridge directly (#2175). + /// + internal void AttachProfiling(Message message, ServerEndPoint server) + { + var profilingSession = _profilingSessionProvider?.Invoke(); + if (profilingSession != null) + { + message.SetProfileStorage(ProfiledCommand.NewWithContext(profilingSession, server)); + } + } } diff --git a/src/StackExchange.Redis/ConnectionMultiplexer.cs b/src/StackExchange.Redis/ConnectionMultiplexer.cs index 87f383fb93..49a5682e5d 100644 --- a/src/StackExchange.Redis/ConnectionMultiplexer.cs +++ b/src/StackExchange.Redis/ConnectionMultiplexer.cs @@ -13,7 +13,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using StackExchange.Redis.Profiling; namespace StackExchange.Redis { @@ -2629,11 +2628,7 @@ private bool PrepareToPushMessageToBridge(Message message, ResultProcessor if (server != null) { - var profilingSession = _profilingSessionProvider?.Invoke(); - if (profilingSession != null) - { - message.SetProfileStorage(ProfiledCommand.NewWithContext(profilingSession, server)); - } + AttachProfiling(message, server); if (message.Db >= 0) { diff --git a/src/StackExchange.Redis/RedisBatch.cs b/src/StackExchange.Redis/RedisBatch.cs index a7783edcd6..9d38377f00 100644 --- a/src/StackExchange.Redis/RedisBatch.cs +++ b/src/StackExchange.Redis/RedisBatch.cs @@ -75,6 +75,12 @@ public void Execute() foreach (var pair in byBridge) { + // attached here, not in the loop above: that loop can still abandon the whole batch for want of a + // server, and commands that never reach a bridge should not show up in a profiling session + foreach (var message in pair.Value) + { + multiplexer.AttachProfiling(message, pair.Key.ServerEndPoint); + } if (!pair.Key.TryEnqueue(pair.Value, pair.Key.ServerEndPoint.IsReplica)) { FailNoServer(multiplexer, pair.Value); diff --git a/tests/StackExchange.Redis.Tests/ProfilingTests.cs b/tests/StackExchange.Redis.Tests/ProfilingTests.cs index 65bb09bde0..269f39aa09 100644 --- a/tests/StackExchange.Redis.Tests/ProfilingTests.cs +++ b/tests/StackExchange.Redis.Tests/ProfilingTests.cs @@ -95,6 +95,35 @@ private static void AssertProfiledCommandValues(IProfiledCommand command, IConne Assert.True(command.RetransmissionReason == null, nameof(command.RetransmissionReason)); } + [Fact] + public async Task Batch() + { + await using var conn = Create(); + + var key = Me(); + var session = new ProfilingSession(); + conn.RegisterProfiler(() => session); + + var dbId = TestConfig.GetDedicatedDB(conn); + var db = conn.GetDatabase(dbId); + + // a batch is handed to the bridge directly, so it used to skip profiling altogether (#2175) + var batch = db.CreateBatch(); + var set = batch.StringSetAsync(key, "world"); + var get = batch.StringGetAsync(key); + var echo = batch.ExecuteAsync("ECHO", "fii"); + batch.Execute(); + await Task.WhenAll(set, get, echo); + Assert.Equal("world", await get); + + var cmds = session.FinishProfiling(); + Assert.Equal("ECHO,GET,SET", string.Join(",", cmds.Select(x => x.Command).OrderBy(x => x))); + + AssertProfiledCommandValues(cmds.Single(cmd => cmd.Command == "SET"), conn, dbId); + AssertProfiledCommandValues(cmds.Single(cmd => cmd.Command == "GET"), conn, dbId); + AssertProfiledCommandValues(cmds.Single(cmd => cmd.Command == "ECHO"), conn, -1); + } + [Fact] public async Task ManyThreads() {