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(); builder.Services.AddSingleton(); builder.Services.AddSingleton(services => services.GetRequiredService()); builder.Services.AddSingleton(); var app = builder.Build(); app.Services.GetRequiredService().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 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()!.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); } }