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(); var replicas = new ConcurrentDictionary(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(); 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); }