Read or download the complete SDK project, then follow its setup on your computer. The separate C# companion practice below runs in SharpRune’s .NET 9 console environment.
Compare a stated workload on actual processes
The capstone asks what changes when the same bounded workload targets one real Kestrel process and then two. It does not assume a twofold throughput gain. Each process has its own main lane, queue, permits and counters, while both share the same physical computer and may contend for CPU, memory or other resources. The experiment uses the normal /work path with fixed settings and open gates. Keep failure controls, drain state and ledger experiments out of the baseline so the comparison changes the number of process owners rather than several independent conditions.
Read the reporting contract before comparing numbers
In closed mode the driver starts a bounded number of asynchronous workers, each taking the next logical operation when available. Target choice uses operation index modulo URL count. For this mode each operation makes one attempt to /work; retry mode is a separate workload. The JSON report includes scheduled, attempted, successful, rejected, skipped, elapsedMs and attempt-latency percentiles, plus the set of replica labels found in successful JSON bodies. Rejected counts final non-success HTTP responses, not only 429 overload responses. Transport exceptions are not converted into that field and can terminate the run instead.
Use the routing companion only for its stated invariant
The portable program assigns eight integers to one array slot and then to two slots by modulo. It prints one:8 and two:4,4, establishing only the deterministic assignment counts. There are no worker tasks, requests, service times or completions in those arrays. The external driver and servers are required to measure actual process behavior. The portable result therefore cannot supply the elapsed time, capacity or speedup expected from a second process. It is useful as a clear baseline for target selection, especially when checking that both real owners were configured and actually reached.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
int jobs = 8; int[] one = new int[1], two = new int[2]; for (int i = 0; i < jobs; i++) { one[i % one.Length]++; two[i % two.Length]++; }
Console.WriteLine("one:" + string.Join(",", one)); Console.WriteLine("two:" + string.Join(",", two));
Practice one: make the controlled one-versus-two run
Start the first owned server on port 5081 and a second with a distinct replica label on port 5082, preserving the same lane settings. Run closed against only 5081 with sixteen operations and concurrency four, then against both URLs with those same values. Hint: check /ready and open gates before each run, and save the two JSON reports before calculating differences. The first report should contain one successful replica label and the second both labels if all intended work succeeds. If the outcomes differ, explain admission or failures before drawing a latency conclusion.
Practice two: reconcile independent server-side evidence
Take /stats snapshots for each server before and after another bounded comparison. Reconcile accepted, completed, rejected and canceled deltas with the client's successful and failed outcomes, allowing for an operation still finishing at snapshot time. Hint: wait for active and queued work to settle before the final snapshot, and remember that counters are process-local and cumulative. This exercise checks the measurement boundary independently of comparing elapsed times. It also exposes a mistaken claim that the report's replica-label set contains per-replica request totals; additional counter deltas supply that missing distribution evidence.
Interpret the distribution and elapsed interval carefully
The driver times each attempt from before SendAsync until after the response body is read, then calculates nearest-rank percentiles from the collected samples. Its stopwatch for elapsedMs covers the overall driver interval, so dividing successful outcomes by that interval gives a run-level successful completion rate, not a per-request latency. With only sixteen samples, upper percentiles are sensitive to individual observations and do not characterize a stable production tail. Repeat only bounded controlled runs when needed, preserve their separate reports, and record warmup and machine conditions instead of averaging away an inconvenient outcome.
Explain scaling limits and keep unsupported claims outside the result
The comparison can show real routing and observed outcomes across two operating-system processes. It cannot establish independent-machine capacity, a deployed load balancer, Kubernetes autoscaling or resilient cross-node storage. The shared-file SQLite lesson is an optional local coordination boundary, not a distributed database; the Kubernetes manifests were not deployed and their per-Pod files are ephemeral. A useful conclusion names the exact settings, successful outcomes, admissions, reached labels and measured distributions, then states whether this particular local comparison improved. More replicas duplicate local budgets, but useful throughput still depends on demand, routing and the resources shared by those replicas.
Capstone: measure one process versus two — real bounded loopback testbed
Set up the SDK project
- Save the complete Server and Driver directories plus global.json into one new owned folder. Install official .NET10 SDK10.0.403.
- Restore both projects explicitly: dotnet restore Server/Lab.Server.csproj --source https://api.nuget.org/v3/index.json ; then dotnet restore Driver/Lab.Driver.csproj --source https://api.nuget.org/v3/index.json
- In terminal1 start the first owned server: ASPNETCORE_ENVIRONMENT=Development dotnet run --project Server/Lab.Server.csproj --no-launch-profile --urls http://127.0.0.1:5081 --Lab:Replica=one --Lab:LedgerPath=./owned-ledger.db --Lab:Workers=2 --Lab:QueueCapacity=4 --Lab:PoolSize=2 --Lab:WorkMilliseconds=20
- For two-replica experiments use terminal2 with port5082, Lab:Replica=two and Lab:Partition=1. Resolve both ledger paths to the same owned absolute file only when the lesson explicitly tests shared local coordination.
- Run drivers only against your owned loopback ports. Maximum32 logical operations, maximum8 in flight and at most2 retry attempts keep each run below100 requests. Outputs include variable measured timings.
- Stop only the processes you started using Ctrl+C in their own terminals; clean up only your owned fixture directory.
- Run driver closed http://127.0.0.1:5081 16 4; then closed http://127.0.0.1:5081,http://127.0.0.1:5082 16 4. Record both JSON reports and per-process stats without asserting a universal speedup.
Download complete SDK project ↗
Project files
Server/Lab.Server.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk.Web"><PropertyGroup><TargetFramework>net10.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup><ItemGroup><PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.14" /></ItemGroup></Project>
Server/Program.cs
Download file ↗using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.AspNetCore.Hosting.Server.Features;
using Microsoft.Data.Sqlite;
var builder = WebApplication.CreateBuilder(args);
var settings = new LabSettings {
Replica = builder.Configuration["Lab:Replica"] ?? "one",
Workers = builder.Configuration.GetValue("Lab:Workers", 2),
Partition = builder.Configuration.GetValue("Lab:Partition", 0),
QueueCapacity = builder.Configuration.GetValue("Lab:QueueCapacity", 4),
PoolSize = builder.Configuration.GetValue("Lab:PoolSize", 2),
WorkMilliseconds = builder.Configuration.GetValue("Lab:WorkMilliseconds", 20),
LedgerPath = builder.Configuration["Lab:LedgerPath"] ?? Path.Combine(Path.GetTempPath(), "sharprune-local-ledger.db")
};
settings.Validate(); builder.Services.AddSingleton(settings); builder.Services.AddSingleton<LabRuntime>(); builder.Services.AddSingleton<LocalIo>();
builder.Services.AddSingleton<IHostedService>(services => services.GetRequiredService<LabRuntime>());
builder.Services.AddSingleton<LocalLedger>(); var app = builder.Build();
app.Services.GetRequiredService<LocalLedger>().Initialize();
app.MapGet("/instance", (LabRuntime lab) => new { replica = settings.Replica, visits = lab.Visit() });
app.MapGet("/live", () => TypedResults.Text("live"));
app.MapGet("/io-delay", async (CancellationToken token) => { await Task.Delay(20, token); return TypedResults.Text("controlled loopback reply"); });
app.MapGet("/io", async (CancellationToken token, IServer server, LocalIo io) => {
using var response = await io.Client.GetAsync(LocalAddress(server), token); response.EnsureSuccessStatusCode(); _ = await response.Content.ReadAsByteArrayAsync(token);
return TypedResults.Ok(new { replica = settings.Replica, kind = "async-loopback-http" });
});
app.MapGet("/sync", (CancellationToken token, IServer server, LocalIo io) => {
using var response = io.Client.GetAsync(LocalAddress(server), token).GetAwaiter().GetResult(); response.EnsureSuccessStatusCode(); _ = response.Content.ReadAsByteArrayAsync(token).GetAwaiter().GetResult();
return TypedResults.Ok(new { replica = settings.Replica, kind = "deliberate-blocking-demo" });
});
app.MapGet("/cpu", () => { uint value = 1; for (int i = 0; i < 200000; i++) value = unchecked(value * 1664525 + 1013904223); return TypedResults.Ok(new { replica = settings.Replica, kind = "bounded-cpu", value }); });
app.MapGet("/threadpool", () => new { threads = ThreadPool.ThreadCount, pending = ThreadPool.PendingWorkItemCount, completed = ThreadPool.CompletedWorkItemCount });
app.MapGet("/ready", (LabRuntime lab) => lab.Accepting ? Results.Ok(new { ready = true }) : Results.StatusCode(503));
app.MapGet("/stats", (LabRuntime lab) => new { replica = settings.Replica, accepting = lab.Accepting, main = lab.Main.Snapshot(), batch = lab.Batch.Snapshot(), dependencyCalls = lab.Circuit.DependencyCalls });
app.MapGet("/work", (HttpContext context, LabRuntime lab, int? budgetMs) => Handle(context, lab.Main, lab, budgetMs));
app.MapGet("/batch", (HttpContext context, LabRuntime lab, int? budgetMs) => Handle(context, lab.Batch, lab, budgetMs));
app.MapGet("/unstable", (LabRuntime lab, string? key) => {
if (key != "demo" && key != "always") return Results.BadRequest();
int attempt = lab.Attempt(key); return key == "always" || attempt == 1 ? Results.StatusCode(503) : Results.Ok(new { replica = settings.Replica, attempt });
});
app.MapGet("/session/{name}", (LabRuntime lab, string name) => name == "demo" ? Results.Ok(new { replica = settings.Replica, value = lab.Session() }) : Results.BadRequest());
app.MapPost("/once/{key}", (string key, LedgerInput input, LocalLedger ledger) => {
if (key != "demo" || input.Amount is < 1 or > 100) return Results.BadRequest();
LedgerDecision result = ledger.Record(key, input.Amount); return result.Conflict ? Results.Conflict(new { error = "key amount conflict" }) : Results.Ok(result);
});
app.MapGet("/ledger", (LocalLedger ledger) => new { receipts = ledger.Count() });
app.MapGet("/guarded", (LabRuntime lab) => lab.Circuit.Invoke(lab.DependencyFailing));
app.MapGet("/partition/{key}", (string key, int? owner) => {
if (key != "A" && key != "B") return Results.BadRequest();
int partition = StablePartition.For(key, 2);
return owner == partition && settings.Partition == partition ? Results.Ok(new { replica = settings.Replica, partition, key }) : Results.Conflict(new { expectedPartition = partition });
});
if (app.Environment.IsDevelopment()) {
app.MapPost("/lab/gate/{lane}/{action}", (string lane, string action, LabRuntime lab) => {
WorkLane? target = lane == "main" ? lab.Main : lane == "batch" ? lab.Batch : null;
if (target is null || action != "open" && action != "close") return Results.BadRequest();
if (action == "open") target.Gate.Open(); else target.Gate.Close(); return Results.NoContent();
});
app.MapPost("/lab/drain", (LabRuntime lab) => { lab.Drain(); return Results.NoContent(); });
app.MapPost("/lab/dependency/{failing:bool}", (bool failing, LabRuntime lab) => { lab.DependencyFailing = failing; return Results.NoContent(); });
}
app.Run();
static async Task<IResult> Handle(HttpContext context, WorkLane lane, LabRuntime lab, int? budgetMs) {
if (budgetMs is < 1 or > 1000) return Results.BadRequest();
using var budget = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted);
if (budgetMs is int milliseconds) budget.CancelAfter(milliseconds);
var item = new WorkItem(budget.Token);
AdmissionDecision decision = lab.Admit(lane, item);
if (decision == AdmissionDecision.Draining) return Results.StatusCode(503);
if (decision == AdmissionDecision.Full) return Results.StatusCode(429);
try { await item.Completion.Task.WaitAsync(budget.Token); return Results.Ok(new { replica = lab.Settings.Replica, completed = true }); }
catch (OperationCanceledException) when (!context.RequestAborted.IsCancellationRequested) { return Results.Problem(statusCode: 504, title: "Classroom work budget expired"); }
}
static Uri LocalAddress(IServer server) {
string address = server.Features.Get<IServerAddressesFeature>()!.Addresses.Single();
return new UriBuilder("http", "127.0.0.1", new Uri(address).Port) { Path = "/io-delay" }.Uri;
}
public sealed class LocalIo : IDisposable { public HttpClient Client { get; } = new() { Timeout = TimeSpan.FromSeconds(5) }; public void Dispose() => Client.Dispose(); }
public sealed record LedgerInput(int Amount);
public sealed class LabSettings {
public string Replica { get; init; } = "one"; public int Partition { get; init; } public int Workers { get; init; } public int QueueCapacity { get; init; }
public int PoolSize { get; init; } public int WorkMilliseconds { get; init; } public string LedgerPath { get; init; } = "";
public void Validate() {
if (Partition is < 0 or > 1 || Replica.Length is < 1 or > 32 || Workers is < 1 or > 8 || QueueCapacity is < 1 or > 16 || PoolSize is < 1 or > 8 || WorkMilliseconds is < 1 or > 250 || string.IsNullOrWhiteSpace(LedgerPath)) throw new InvalidOperationException("Invalid bounded lab settings");
}
}
public enum AdmissionDecision { Accepted, Full, Draining }
public sealed class LabRuntime : IHostedService, IDisposable {
private readonly object admissionSync = new();
private int accepting = 1, visits, sessions, demoAttempts, alwaysAttempts, failing = 1, disposed;
public LabSettings Settings { get; } public WorkLane Main { get; } public WorkLane Batch { get; }
public ClassroomCircuit Circuit { get; } = new();
public LabRuntime(LabSettings settings) { Settings = settings; Main = new(settings.Workers, settings.QueueCapacity, settings.PoolSize, settings.WorkMilliseconds); Batch = new(1, 1, 1, settings.WorkMilliseconds); }
public bool Accepting => Volatile.Read(ref accepting) == 1;
public bool DependencyFailing { get => Volatile.Read(ref failing) == 1; set => Volatile.Write(ref failing, value ? 1 : 0); }
public void Drain() { lock (admissionSync) Volatile.Write(ref accepting, 0); }
public AdmissionDecision Admit(WorkLane lane, WorkItem item) {
lock (admissionSync) {
if (!Accepting) return AdmissionDecision.Draining;
return lane.TrySubmit(item) ? AdmissionDecision.Accepted : AdmissionDecision.Full;
}
}
public int Visit() => Interlocked.Increment(ref visits); public int Session() => Interlocked.Increment(ref sessions);
public int Attempt(string key) => key == "demo" ? Interlocked.Increment(ref demoAttempts) : Interlocked.Increment(ref alwaysAttempts);
public Task StartAsync(CancellationToken token) { Main.Start(); Batch.Start(); return Task.CompletedTask; }
public Task StopAsync(CancellationToken token) => Task.WhenAll(Main.StopAsync(token), Batch.StopAsync(token));
public void Dispose() { if (Interlocked.Exchange(ref disposed, 1) != 0) return; Main.Dispose(); Batch.Dispose(); }
}
public static class StablePartition {
public static int For(string key, int count) { uint hash = 2166136261; foreach (byte b in System.Text.Encoding.UTF8.GetBytes(key)) hash = unchecked((hash ^ b) * 16777619); return (int)(hash % (uint)count); }
}
Server/WorkLane.cs
Download file ↗using System.Threading.Channels;
public sealed record WorkItem(CancellationToken Token) { public TaskCompletionSource<bool> Completion { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); }
public sealed class AsyncGate {
private readonly object sync = new(); private TaskCompletionSource<bool> source = Completed();
private static TaskCompletionSource<bool> Completed() { var value = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); value.SetResult(true); return value; }
public void Close() { lock (sync) { if (source.Task.IsCompleted) source = new(TaskCreationOptions.RunContinuationsAsynchronously); } }
public void Open() { lock (sync) source.TrySetResult(true); }
public Task WaitAsync(CancellationToken token) { lock (sync) return source.Task.WaitAsync(token); }
}
public sealed class WorkLane : IDisposable {
private readonly Channel<WorkItem> queue; private readonly SemaphoreSlim pool; private readonly int workers, milliseconds;
private readonly CancellationTokenSource stop = new(); private Task[] running = Array.Empty<Task>();
private int active, inPool, maxActive, maxPool, accepted, rejected, completed, canceled, disposed;
public AsyncGate Gate { get; } = new();
public WorkLane(int workers, int capacity, int poolSize, int milliseconds) {
this.workers = workers; this.milliseconds = milliseconds; pool = new(poolSize, poolSize);
queue = Channel.CreateBounded<WorkItem>(new BoundedChannelOptions(capacity) { FullMode = BoundedChannelFullMode.Wait, SingleWriter = false, SingleReader = workers == 1 });
}
public void Start() { running = Enumerable.Range(0, workers).Select(_ => RunAsync()).ToArray(); }
public bool TrySubmit(WorkItem item) { if (queue.Writer.TryWrite(item)) { Interlocked.Increment(ref accepted); return true; } Interlocked.Increment(ref rejected); return false; }
private async Task RunAsync() {
await foreach (WorkItem item in queue.Reader.ReadAllAsync(stop.Token)) {
int now = Interlocked.Increment(ref active); Maximum(ref maxActive, now); bool entered = false;
using var lifetime = CancellationTokenSource.CreateLinkedTokenSource(item.Token, stop.Token);
try {
lifetime.Token.ThrowIfCancellationRequested(); await Gate.WaitAsync(lifetime.Token);
await pool.WaitAsync(lifetime.Token); entered = true; Maximum(ref maxPool, Interlocked.Increment(ref inPool));
await Task.Delay(milliseconds, lifetime.Token); Interlocked.Increment(ref completed); item.Completion.TrySetResult(true);
} catch (OperationCanceledException) { Interlocked.Increment(ref canceled); item.Completion.TrySetCanceled(lifetime.Token); }
finally { if (entered) { Interlocked.Decrement(ref inPool); pool.Release(); } Interlocked.Decrement(ref active); }
}
}
private static void Maximum(ref int field, int value) { int old; do { old = Volatile.Read(ref field); if (old >= value) return; } while (Interlocked.CompareExchange(ref field, value, old) != old); }
public object Snapshot() => new { active = Volatile.Read(ref active), queued = queue.Reader.Count, inPool = Volatile.Read(ref inPool), maxActive = Volatile.Read(ref maxActive), maxPool = Volatile.Read(ref maxPool), accepted = Volatile.Read(ref accepted), rejected = Volatile.Read(ref rejected), completed = Volatile.Read(ref completed), canceled = Volatile.Read(ref canceled) };
public async Task StopAsync(CancellationToken token) {
queue.Writer.TryComplete();
try { await Task.WhenAll(running).WaitAsync(token); }
catch (OperationCanceledException) when (token.IsCancellationRequested) { stop.Cancel(); Gate.Open(); try { await Task.WhenAll(running); } catch (OperationCanceledException) { } }
}
public void Dispose() { if (Interlocked.Exchange(ref disposed, 1) != 0) return; stop.Cancel(); pool.Dispose(); stop.Dispose(); }
}
Server/LocalLedger.cs
Download file ↗using Microsoft.Data.Sqlite;
public sealed record LedgerDecision(bool Replay, bool Conflict, int Amount);
public sealed class LocalLedger {
private readonly string connectionString;
public LocalLedger(LabSettings settings) { connectionString = new SqliteConnectionStringBuilder { DataSource = settings.LedgerPath, DefaultTimeout = 2 }.ToString(); }
private SqliteConnection Open() { var connection = new SqliteConnection(connectionString); connection.Open(); return connection; }
public void Initialize() { using var connection = Open(); using var command = connection.CreateCommand(); command.CommandText = "CREATE TABLE IF NOT EXISTS receipts (request_key TEXT PRIMARY KEY, amount INTEGER NOT NULL)"; command.ExecuteNonQuery(); }
public LedgerDecision Record(string key, int amount) {
using var connection = Open(); using var transaction = connection.BeginTransaction();
using var insert = connection.CreateCommand(); insert.Transaction = transaction;
insert.CommandText = "INSERT INTO receipts(request_key, amount) VALUES ($key, $amount) ON CONFLICT(request_key) DO NOTHING";
insert.Parameters.AddWithValue("$key", key); insert.Parameters.AddWithValue("$amount", amount); int changed = insert.ExecuteNonQuery();
using var read = connection.CreateCommand(); read.Transaction = transaction; read.CommandText = "SELECT amount FROM receipts WHERE request_key = $key"; read.Parameters.AddWithValue("$key", key);
int stored = Convert.ToInt32(read.ExecuteScalar(), System.Globalization.CultureInfo.InvariantCulture); transaction.Commit();
return new LedgerDecision(changed == 0, stored != amount, stored);
}
public int Count() { using var connection = Open(); using var read = connection.CreateCommand(); read.CommandText = "SELECT COUNT(*) FROM receipts"; return Convert.ToInt32(read.ExecuteScalar(), System.Globalization.CultureInfo.InvariantCulture); }
}
Server/ClassroomCircuit.cs
Download file ↗public sealed class ClassroomCircuit {
private readonly object sync = new(); private int failures, calls; private bool probe; private DateTimeOffset openUntil = DateTimeOffset.MinValue;
public int DependencyCalls { get { lock (sync) return calls; } }
public IResult Invoke(bool failing) {
lock (sync) {
DateTimeOffset now = DateTimeOffset.UtcNow;
if (openUntil != DateTimeOffset.MinValue && now < openUntil) return Results.Json(new { state = "open", called = false }, statusCode: 503);
if (probe) return Results.Json(new { state = "probe-busy", called = false }, statusCode: 503);
bool halfOpen = openUntil != DateTimeOffset.MinValue; if (halfOpen) probe = true;
calls++;
// The dependency is an explicitly controlled in-process classroom outcome, not a real remote service.
if (failing) {
failures++; if (halfOpen || failures >= 2) openUntil = now.AddMilliseconds(250); probe = false;
return Results.Json(new { state = openUntil == DateTimeOffset.MinValue ? "closed" : "open", called = true }, statusCode: 503);
}
failures = 0; openUntil = DateTimeOffset.MinValue; probe = false; return Results.Ok(new { state = "closed", called = true });
}
}
}
Driver/Lab.Driver.csproj
Download file ↗<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net10.0</TargetFramework><Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings></PropertyGroup></Project>
Driver/Program.cs
Download file ↗using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Text.Json;
if (args.Length < 2) throw new ArgumentException("Usage: closed|open|retry|shard|hot|io|cpu|sync baseUrl[,baseUrl] [count<=32] [concurrency<=8] [intervalMs]");
string mode = args[0];
if (!new[] { "closed", "open", "retry", "shard", "hot", "io", "cpu", "sync" }.Contains(mode)) throw new ArgumentException("Unknown mode");
Uri[] targets = args[1].Split(',').Select(s => new Uri(s.TrimEnd('/') + "/")).ToArray();
if (targets.Length is < 1 or > 2 || targets.Any(u => !u.IsLoopback || u.Scheme != "http" || u.AbsolutePath != "/" || !string.IsNullOrEmpty(u.Query) || !string.IsNullOrEmpty(u.UserInfo))) throw new ArgumentException("Only one or two owned HTTP loopback roots are allowed");
int count = args.Length > 2 ? int.Parse(args[2], CultureInfo.InvariantCulture) : 16;
int concurrency = args.Length > 3 ? int.Parse(args[3], CultureInfo.InvariantCulture) : 4;
int interval = args.Length > 4 ? int.Parse(args[4], CultureInfo.InvariantCulture) : 2;
if (count is < 1 or > 32 || concurrency is < 1 or > 8 || interval is < 1 or > 100 || (mode == "shard" || mode == "hot") && targets.Length != 2) throw new ArgumentException("Bounded local settings required");
using var client = new HttpClient { Timeout = TimeSpan.FromSeconds(5) };
var latencies = new ConcurrentBag<double>(); var replicas = new ConcurrentDictionary<string, byte>(StringComparer.Ordinal);
int attempted = 0, successful = 0, rejected = 0, skipped = 0, logical = 0;
async Task Invoke(int index) {
string key = mode == "hot" || index % 2 == 0 ? "A" : "B"; int partition = Partition(key);
Uri target = targets[(mode == "shard" || mode == "hot") ? partition : index % targets.Length];
string route = mode switch { "retry" => "unstable?key=demo", "shard" or "hot" => "partition/" + key + "?owner=" + partition, "io" => "io", "cpu" => "cpu", "sync" => "sync", _ => "work" };
int maximumAttempts = mode == "retry" ? 2 : 1;
for (int attempt = 1; attempt <= maximumAttempts; attempt++) {
var begin = Stopwatch.GetTimestamp(); Interlocked.Increment(ref attempted);
using var request = new HttpRequestMessage(HttpMethod.Get, new Uri(target, route));
using var response = await client.SendAsync(request); string body = await response.Content.ReadAsStringAsync();
latencies.Add(Stopwatch.GetElapsedTime(begin).TotalMilliseconds);
if ((int)response.StatusCode == 503 && attempt < maximumAttempts) { await Task.Delay(10 + index % 7); continue; }
if (response.IsSuccessStatusCode) {
Interlocked.Increment(ref successful); using var doc = JsonDocument.Parse(body);
if (doc.RootElement.TryGetProperty("replica", out var name)) replicas.TryAdd(name.GetString()!, 0);
} else Interlocked.Increment(ref rejected);
break;
}
}
var elapsed = Stopwatch.StartNew();
if (mode == "open") {
using var slots = new SemaphoreSlim(concurrency, concurrency); var running = new List<Task>();
for (int i = 0; i < count; i++) {
double wait = i * interval - elapsed.Elapsed.TotalMilliseconds; if (wait > 0) await Task.Delay(TimeSpan.FromMilliseconds(wait));
Interlocked.Increment(ref logical);
if (!slots.Wait(0)) { Interlocked.Increment(ref skipped); continue; }
int index = i; running.Add(Release(index));
}
await Task.WhenAll(running);
async Task Release(int index) { try { await Invoke(index); } finally { slots.Release(); } }
} else {
int next = -1;
await Task.WhenAll(Enumerable.Range(0, concurrency).Select(async _ => {
while (true) { int index = Interlocked.Increment(ref next); if (index >= count) return; Interlocked.Increment(ref logical); await Invoke(index); }
}));
}
elapsed.Stop(); double[] sorted = latencies.OrderBy(x => x).ToArray();
double Percentile(double probability) => sorted.Length == 0 ? 0 : sorted[(int)Math.Ceiling(probability * sorted.Length) - 1];
Console.WriteLine(JsonSerializer.Serialize(new { mode, scheduled = logical, attempted, successful, rejected, skipped, concurrency, elapsedMs = elapsed.Elapsed.TotalMilliseconds, p50Ms = Percentile(.5), p95Ms = Percentile(.95), p99Ms = Percentile(.99), replicas = replicas.Keys.OrderBy(x => x).ToArray() }));
static int Partition(string key) { uint hash = 2166136261; foreach (byte b in System.Text.Encoding.UTF8.GetBytes(key)) hash = unchecked((hash ^ b) * 16777619); return (int)(hash % 2); }
Server/appsettings.json
Download file ↗{
"Lab": {
"Replica": "one",
"Workers": 2,
"QueueCapacity": 4,
"PoolSize": 2,
"WorkMilliseconds": 20
}
}
Expected result
One-process report reaches one replica label; two-process report reaches both. Each run schedules16 operations at concurrency4. Exact elapsed times and percentiles vary and may not improve.
Use the listed SDK and dependencies for this project. Open C# practice runs the separate portable exercise.
C# companion practice
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
int jobs = 8; int[] one = new int[1], two = new int[2]; for (int i = 0; i < jobs; i++) { one[i % one.Length]++; two[i % two.Length]++; }
Console.WriteLine("one:" + string.Join(",", one)); Console.WriteLine("two:" + string.Join(",", two));
C# practice output
one:8 two:4,4
Verify actual process participation and fixed workload settings before comparing variable measured durations.