Skip to content

Commit 9aad6bd

Browse files
committed
Tweaks to load tool
1 parent 33aad4c commit 9aad6bd

8 files changed

Lines changed: 83 additions & 12 deletions

File tree

‎tools/testing-tool/TestingTool.AppHost/AppHost.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
using TestingTool.AppHost;
44

55
var options = CliOptions.Parse(args);
6-
var persistenceType = options.GetEnumOrDefault<PersistenceType>("persistence", PersistenceType.PostgreSql);
6+
var persistenceType = options.GetEnumOrDefault("persistence", PersistenceType.PostgreSql);
77
Console.WriteLine($"Using persistence type: {persistenceType}");
88

99
var builder = DistributedApplication.CreateBuilder(args);
@@ -26,6 +26,7 @@
2626

2727
var errorInstance = platform
2828
.AddServiceControlErrorInstance("error", raven)
29+
.WithEnvironment("SERVICECONTROL_DISABLEEXTERNALINTEGRATIONSPUBLISHING", "true")
2930
.WithEnvironment("OTEL_EXPORTER_OTLP_ENDPOINT", observability.Collector.GetEndpoint("otlp-grpc"))
3031
.WithPersistenceType(persistenceType)
3132
.WithRunMode(PlatformRunMode.SetupAndRun);

‎tools/testing-tool/TestingTool.Contracts/BypassStatus.cs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ public sealed class BypassStatus
1818
/// <summary>Total failed-message envelopes written directly to the error queue since process start.</summary>
1919
public long ErrorsWritten { get; init; }
2020

21+
/// <summary>Total bypass sends that failed since process start. A non-zero value indicates
22+
/// transport/broker issues; see logs (Warning level) for details. Previously these failures
23+
/// were swallowed at Debug level, making the bypass appear idle when sends were failing.</summary>
24+
public long ErrorsFailed { get; init; }
25+
2126
/// <summary>When the current bypass run started (UTC ISO 8601, null if idle).</summary>
2227
public string? StartedAt { get; init; }
2328
}

‎tools/testing-tool/TestingTool.Contracts/StartBypassRequest.cs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,12 @@ public sealed class StartBypassRequest
1515

1616
/// <summary>Optional auto-stop duration in seconds. Null/0 = run until explicitly stopped.</summary>
1717
public double? DurationSeconds { get; init; }
18+
19+
/// <summary>
20+
/// Number of parallel worker tasks to use for sending. Each task runs its own timer at
21+
/// <c>rate / parallelism</c> msg/s so the aggregate approaches the target rate. Defaults to
22+
/// <see cref="Environment.ProcessorCount"/> when omitted. Increase this if the bypass is
23+
/// not hitting the target rate due to send latency on a single thread.
24+
/// </summary>
25+
public int? Parallelism { get; init; }
1826
}

‎tools/testing-tool/TestingTool.Contracts/TestingToolStatus.cs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ public sealed class TestingToolStatus
2424
/// <summary>Total errors emitted via the direct error-queue bypass writer since process start.</summary>
2525
public long BypassErrorsWritten { get; init; }
2626

27+
/// <summary>Total bypass sends that failed since process start. Non-zero values indicate
28+
/// transport/broker issues on the bypass path — the handler path may be unaffected.</summary>
29+
public long BypassErrorsFailed { get; init; }
30+
2731
/// <summary>The shard id this replica owns, used for disjoint scenario slices when scaled out.</summary>
2832
public string? ShardId { get; init; }
2933

‎tools/testing-tool/TestingTool/DirectErrorQueueWriter.cs‎

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,16 @@ public sealed class DirectErrorQueueWriter
3333
private readonly Counter<long> _bypassCounter;
3434

3535
private CancellationTokenSource? _cts;
36-
private Task? _loop;
36+
private Task[]? _loops;
3737
private long _errorsWritten;
38+
private long _errorsFailed;
3839
private double _currentRate;
3940
private string? _activeScenario;
4041
private DateTimeOffset _startedAt;
4142

4243
public bool IsRunning => _cts is not null;
4344
public long ErrorsWritten => Interlocked.Read(ref _errorsWritten);
45+
public long ErrorsFailed => Interlocked.Read(ref _errorsFailed);
4446
public double CurrentRate => _currentRate;
4547
public string? ActiveScenario => _activeScenario;
4648

@@ -62,7 +64,7 @@ public DirectErrorQueueWriter(
6264
}
6365

6466
/// <summary>Starts writing failed-message envelopes directly to the error queue.</summary>
65-
public bool TryStart(string scenarioName, double rate, TimeSpan? duration, out string? error)
67+
public bool TryStart(string scenarioName, double rate, TimeSpan? duration, int? parallelism, out string? error)
6668
{
6769
var scenario = _registry.Get(scenarioName);
6870
if (scenario is null)
@@ -83,6 +85,11 @@ public bool TryStart(string scenarioName, double rate, TimeSpan? duration, out s
8385
return false;
8486
}
8587

88+
// Default to ProcessorCount workers. Each worker runs its own timer at rate/N so the
89+
// aggregate approaches the target. More workers = more concurrent sends = higher
90+
// throughput when individual sends have latency.
91+
var workerCount = parallelism is { } p and > 0 ? p : Environment.ProcessorCount;
92+
8693
_activeScenario = scenarioName;
8794
_currentRate = rate;
8895
_startedAt = DateTimeOffset.UtcNow;
@@ -92,10 +99,15 @@ public bool TryStart(string scenarioName, double rate, TimeSpan? duration, out s
9299
: new CancellationTokenSource();
93100
_cts = cts;
94101

95-
_loop = Task.Run(() => WriteLoop(scenario, rate, cts.Token));
102+
_loops = new Task[workerCount];
103+
for (var i = 0; i < workerCount; i++)
104+
{
105+
var workerIndex = i;
106+
_loops[i] = Task.Run(() => WriteLoop(scenario, rate / workerCount, workerIndex, cts.Token));
107+
}
96108

97-
_logger.LogInformation("Started bypass writer for scenario {Scenario} at {Rate:F1} msg/s{Duration}",
98-
scenarioName, rate, duration is null ? "" : $" for {duration.Value}");
109+
_logger.LogInformation("Started bypass writer for scenario {Scenario} at {Rate:F1} msg/s{Duration} across {Workers} workers",
110+
scenarioName, rate, duration is null ? "" : $" for {duration.Value}", workerCount);
99111

100112
error = null;
101113
return true;
@@ -107,10 +119,27 @@ public void Stop()
107119
if (_cts is null) return;
108120

109121
_cts.Cancel();
122+
123+
// Await all worker loops before disposing the token so we don't dispose a CTS that's still
124+
// in flight inside _session.Send. Swallow the expected cancellation/timeout.
125+
var loops = _loops;
126+
if (loops is not null)
127+
{
128+
try
129+
{
130+
Task.WaitAll(loops, TimeSpan.FromSeconds(5));
131+
}
132+
catch
133+
{
134+
// Loops were cancelled or timed out — expected during stop.
135+
}
136+
}
137+
110138
_cts.Dispose();
111139
_cts = null;
140+
_loops = null;
112141

113-
_logger.LogInformation("Stopped bypass writer after {Errors} errors written", ErrorsWritten);
142+
_logger.LogInformation("Stopped bypass writer: {Errors} written, {Failed} failed", ErrorsWritten, ErrorsFailed);
114143

115144
_activeScenario = null;
116145
_currentRate = 0;
@@ -123,14 +152,16 @@ public void Stop()
123152
Scenario = _activeScenario,
124153
Rate = _currentRate,
125154
ErrorsWritten = ErrorsWritten,
155+
ErrorsFailed = ErrorsFailed,
126156
StartedAt = IsRunning ? _startedAt.ToString("O") : null
127157
};
128158

129159
/// <summary>
130160
/// The load generation loop: sends <see cref="LoadMessage"/> directly to the error queue
131-
/// with failure headers at the target rate until cancelled.
161+
/// with failure headers at the target rate until cancelled. Multiple instances run in
162+
/// parallel, each handling a fraction of the total rate.
132163
/// </summary>
133-
private async Task WriteLoop(IScenario scenario, double rate, CancellationToken ct)
164+
private async Task WriteLoop(IScenario scenario, double rate, int workerIndex, CancellationToken ct)
134165
{
135166
var interval = TimeSpan.FromSeconds(1.0 / rate);
136167
using var timer = new PeriodicTimer(interval);
@@ -184,9 +215,20 @@ private async Task WriteLoop(IScenario scenario, double rate, CancellationToken
184215
_metrics.AddBypassErrorsWritten(1);
185216
_bypassCounter.Add(1, new KeyValuePair<string, object?>("scenario", scenario.Name));
186217
}
187-
catch (Exception ex) when (ex is not OperationCanceledException)
218+
catch (OperationCanceledException)
188219
{
189-
_logger.LogDebug(ex, "Bypass send failed for scenario {Scenario} seq {Seq}", scenario.Name, seq);
220+
// Cancellation is expected on stop/timeout — let it propagate to the outer handler.
221+
throw;
222+
}
223+
catch (Exception ex)
224+
{
225+
Interlocked.Increment(ref _errorsFailed);
226+
_metrics.AddBypassErrorsFailed(1);
227+
228+
// Log at Warning so send failures are visible in default logging configs.
229+
// Previously this was LogDebug, which silently swallowed transport/broker
230+
// failures and made the bypass appear idle when sends were actually failing.
231+
_logger.LogWarning(ex, "Bypass send failed for scenario {Scenario} worker {Worker} seq {Seq}", scenario.Name, workerIndex, seq);
190232
}
191233
}
192234
}

‎tools/testing-tool/TestingTool/Program.cs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
ErrorsArchived = metrics.TotalErrorsArchived,
104104
SearchesExecuted = metrics.TotalSearches,
105105
BypassErrorsWritten = metrics.TotalBypassErrorsWritten,
106+
BypassErrorsFailed = metrics.TotalBypassErrorsFailed,
106107
ShardId = shardId,
107108
ActiveScenarios = metrics.ActiveScenarios,
108109
ActiveJobs = jobRunner.GetSnapshot().Count(j => j.Running),
@@ -192,7 +193,7 @@
192193
? TimeSpan.FromSeconds(secs)
193194
: (TimeSpan?)null;
194195

195-
if (!writer.TryStart(scenarioName, rate, duration, out var error))
196+
if (!writer.TryStart(scenarioName, rate, duration, request?.Parallelism, out var error))
196197
return Results.BadRequest(new { error });
197198

198199
return Results.Ok(writer.GetStatus());

‎tools/testing-tool/TestingTool/TestingToolMetrics.cs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ public sealed class TestingToolMetrics
1212
private long _totalErrorsArchived;
1313
private long _totalSearches;
1414
private long _totalBypassErrorsWritten;
15+
private long _totalBypassErrorsFailed;
1516
private long _activeScenarios;
1617
private double _currentRate;
1718

@@ -20,6 +21,7 @@ public sealed class TestingToolMetrics
2021
public long TotalErrorsArchived => Interlocked.Read(ref _totalErrorsArchived);
2122
public long TotalSearches => Interlocked.Read(ref _totalSearches);
2223
public long TotalBypassErrorsWritten => Interlocked.Read(ref _totalBypassErrorsWritten);
24+
public long TotalBypassErrorsFailed => Interlocked.Read(ref _totalBypassErrorsFailed);
2325
public int ActiveScenarios => (int)Interlocked.Read(ref _activeScenarios);
2426
public double CurrentRate => _currentRate;
2527

@@ -28,6 +30,7 @@ public sealed class TestingToolMetrics
2830
public void AddErrorsArchived(long count) => Interlocked.Add(ref _totalErrorsArchived, count);
2931
public void AddSearches(long count) => Interlocked.Add(ref _totalSearches, count);
3032
public void AddBypassErrorsWritten(long count) => Interlocked.Add(ref _totalBypassErrorsWritten, count);
33+
public void AddBypassErrorsFailed(long count) => Interlocked.Add(ref _totalBypassErrorsFailed, count);
3134
public void SetActiveScenarios(int count) => Interlocked.Exchange(ref _activeScenarios, count);
3235
public void SetCurrentRate(double rate) => _currentRate = rate;
3336
}

‎tools/testing-tool/TestingTool/wwwroot/index.html‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,12 @@ <h2>Direct Error-Queue Bypass</h2>
234234
</select>
235235
<span class="field-label">Rate (msg/s):</span>
236236
<input type="number" id="bypass-rate" value="100" min="1" step="1" />
237+
<span class="field-label">Workers:</span>
238+
<input type="number" id="bypass-parallelism" value="" min="1" step="1" placeholder="auto" title="Number of parallel worker tasks (default: CPU count)" />
237239
<span class="field-label">Duration (sec, optional):</span>
238240
<input type="number" id="bypass-duration" value="" min="1" step="1" placeholder="∞" />
241+
</div>
242+
<div class="controls">
239243
<button class="btn start" onclick="startBypass()" id="btn-bypass-start">▶ Start Bypass</button>
240244
<button class="btn stop" onclick="stopBypass()" id="btn-bypass-stop" disabled>⏹ Stop Bypass</button>
241245
</div>
@@ -554,8 +558,10 @@ <h2>Direct Error-Queue Bypass</h2>
554558
const scenario = document.getElementById('bypass-scenario').value;
555559
const rate = parseFloat(document.getElementById('bypass-rate').value);
556560
const dur = parseInt(document.getElementById('bypass-duration').value);
561+
const par = parseInt(document.getElementById('bypass-parallelism').value);
557562
const body = { scenario };
558563
if (rate && rate > 0) body.rate = rate;
564+
if (par && par > 0) body.parallelism = par;
559565
if (dur && dur > 0) body.durationSeconds = dur;
560566
try {
561567
await api('POST', '/api/bypass/start', body);
@@ -592,6 +598,7 @@ <h2>Direct Error-Queue Bypass</h2>
592598
${bypass.scenario ? `<span>Scenario: <strong>${bypass.scenario}</strong></span>` : ''}
593599
${bypass.running ? `<span>Rate: <strong>${bypass.rate ?? 0} msg/s</strong></span>` : ''}
594600
<span>Errors written: <strong>${fmtNum(bypass.errorsWritten)}</strong></span>
601+
${bypass.errorsFailed ? `<span style="color:var(--err)">Failed: <strong>${fmtNum(bypass.errorsFailed)}</strong></span>` : ''}
595602
`;
596603
} catch (e) { /* ignore */ }
597604
}

0 commit comments

Comments
 (0)