# ADR: The ports learn to wait

Status: accepted, 2026-09-08. The first decision of the Cosmos DB work that
reaches the Application layer: every member of the three ports in `Ports.cs`
now returns a `Task`, `BidService` holds a semaphore where it held a lock, and
the host warms the catalogue and the bids before it serves. Parent: ADR: A
second store on Cosmos DB, and what it costs.

## In plain words

The parts of the code that read and write data (the ports) now wait for the database without holding a thread (every port returns a `Task`). The new store has no way to answer without waiting, and on a one-CPU container a blocked thread stalls every request behind it.

What that is worth: a developer can add another store later without a slow database freezing the site, and the organization gets a site where a slow store slows bidding and nothing else.

## Context

`IVehicleSource.Load()`, `IPhotoManifestSource.Load()` and the three members
of `IBidStore` were synchronous, and they were synchronous for a reason that was
true when they were written: the first store was a pair of JSON files, and
reading a file is a synchronous thing to do. The second store, SQLite and then
Azure SQL Database through EF Core, has a synchronous driver, so the ports kept
their shape and `EfBidStore.Save` called `SaveChanges` on a request thread.

The third store has no synchronous driver. The Cosmos DB SDK is asynchronous
throughout, and the EF Core provider for it throws on `ToList` and
`SaveChanges`. So a Cosmos adapter behind a synchronous port has exactly one way
to exist: call the asynchronous method and block until it finishes.

## Why blocking was not taken

Blocking on a task in ASP.NET Core does not deadlock, because there is no
synchronization context to deadlock on. It is still the wrong answer here, and
the reason is arithmetic about the container rather than a rule about style.

The live container has one vCPU, so the thread pool starts with one worker
thread. A bid that blocks that thread while the store answers, five to ten
milliseconds in region, holds every other request behind it until the pool
notices it is starved and injects another thread, which it does roughly twice a
second. Under a burst of bids the site would stall in half-second steps, and the
stall would be invisible in any measurement taken one request at a time.
`PlaceBid` also held a `lock` across the store call, so the bids behind the
first one would be blocked threads waiting on a blocked thread. On a lane whose
subject is performance, that is the defect the interview would be about.

## Decision

**Every port member returns a `Task`.** `LoadAsync`, `SaveAsync`, `ClearAsync`.
No cancellation tokens, deliberately: a bid write that is halfway through the
store-then-memory sequence must finish, and the two loads run once at startup.

```csharp
// The three seams the layers meet at. Application declares what it needs and
// never learns where the data lives; Infrastructure implements these against
// EF Core and SQLite, against Cosmos DB, the tests against in-memory arrays,
// and the 100,000-record scale-up is a decorator over IVehicleSource that
// nothing above it can see.
//
// Every member returns a Task, because the document store has no synchronous
// driver, and a port that held a request thread while a cloud store answered
// would stall a one-vCPU container (ADR: The ports learn to wait).
/// <summary>Port: where the vehicle dataset comes from.</summary>
public interface IVehicleSource
{
    Task<IReadOnlyList<Vehicle>> LoadAsync();
}

/// <summary>Port: where the photo manifest comes from.</summary>
public interface IPhotoManifestSource
{
    Task<IReadOnlyList<PhotoEntry>> LoadAsync();
}

/// <summary>
/// Port: where everybody's bids are kept between one run of this process and
/// the next. Read once at startup and written through on every accepted bid,
/// which is the shape the bidding path can afford (ADR: The relational store).
/// A bid belongs to a user (ADR: Accounts and per-user bids), so the key is the
/// pair and not the vehicle.
/// </summary>
public interface IBidStore
{
    Task<IReadOnlyList<StoredBid>> LoadAsync();

    Task SaveAsync(string userId, string vehicleId, BidState state);

    /// <summary>
    /// Forget one person's bids. Not everybody's: this is what the reset button
    /// on a page a stranger can also be looking at is allowed to do
    /// (ADR: Reset is one person's start-over).
    /// </summary>
    Task ClearAsync(string userId);
}

/// <summary>
/// The port wired to nothing: bids live in memory and end with the process.
/// The unit tests use it, and so does a backend whose store has not come up
/// yet; those bids are dropped when the store attaches.
/// </summary>
public sealed class NullBidStore : IBidStore
{
    public static readonly NullBidStore Instance = new();

    private NullBidStore()
    {
    }

    public Task<IReadOnlyList<StoredBid>> LoadAsync() => Task.FromResult<IReadOnlyList<StoredBid>>([]);

    public Task SaveAsync(string userId, string vehicleId, BidState state) => Task.CompletedTask;

    public Task ClearAsync(string userId) => Task.CompletedTask;
}
```
*Live from [`api/TheYard.Application/Ports.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Application/Ports.cs#L6-L66), region `ports`, read from this build at 1d6b50b.*

**`BidService` holds a semaphore, one at a time, across the await.** A `lock`
cannot be held across an `await`, and the store is awaited inside the critical
section on purpose: the store is written first and memory second, so a store
that throws leaves nothing behind (ADR: The relational store). Same guarantee,
same shape, one bidder at a time:

```csharp
/// <summary>
/// Places one bid for one buyer. The rules run against the vehicle as
/// everybody's bids leave it, under the gate, and an accepted bid is
/// written to the store before it is recorded in memory.
/// </summary>
public async Task<BidOutcome> PlaceBidAsync(Vehicle vehicle, int amount, AuctionClock clock, string userId)
{
    await LoadAsync();
    await _gate.WaitAsync();
    try
    {
        var merged = Apply(vehicle);
        // Sold is read under the same gate as the write that makes it true,
        // so two buyers cannot both find it false.
        var outcome = BidRules.ResolveBid(merged, amount, clock, IsSold(vehicle.Id));
        if (outcome.Kind != BidOutcomeKind.Rejected)
        {
            var state = new BidState(
                outcome.Amount,
                merged.BidCount + 1,
                WonBuyNow: outcome.Kind == BidOutcomeKind.Won,
                AtMs: clock.NowMs);
            // The store first, then memory. The other order looks harmless
            // and is not: a store that throws would leave the dictionaries
            // holding a bid the caller was just told had failed, shown as
            // winning until the next restart deleted it. This way a failed
            // write means the bid did not happen anywhere, which is the
            // answer the caller already has.
            await _store.SaveAsync(userId, vehicle.Id, state);
            Record(userId, vehicle.Id, state);
        }
        return outcome;
    }
    finally
    {
        _gate.Release();
    }
}
```
*Live from [`api/TheYard.Application/BidService.Bidding.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Application/BidService.Bidding.cs#L14-L51), region `place`, read from this build at 1d6b50b.*

**The constructor stopped reading the store.** A constructor cannot wait, and
the store now has to be waited for. `LoadAsync` replays the store once, whoever
asks first; the host asks at startup and the writing methods ask again, which
costs nothing once the load is done and means a service nobody warmed loads
itself on its first bid:

```csharp
/// <summary>
/// Read once, through <see cref="LoadAsync"/>. Every read after that one is
/// a dictionary: Apply runs over a hundred thousand vehicles on a listing
/// request, and a per-row query would end the feature rather than persist
/// it (ADR: The relational store).
///
/// The constructor does not read the store, because a constructor cannot
/// wait and the store has to be waited for (ADR: The ports learn to
/// wait). The host calls LoadAsync once at startup; the writing methods
/// call it too, so a service nobody warmed loads itself on its first bid.
/// </summary>
public BidService(IBidStore store)
{
    _store = store;
    _standing = new ConcurrentDictionary<string, VehicleStanding>(StringComparer.Ordinal);
    _byUser = new ConcurrentDictionary<string, ConcurrentDictionary<string, BidState>>(StringComparer.Ordinal);
}

/// <summary>Held only while deciding whether a load has to start, so two first callers share one replay.</summary>
private readonly object _loadGate = new();

/// <summary>The replay in flight or finished, or null before anybody has asked for one.</summary>
private Task? _loaded;

/// <summary>
/// Replay the store into both indexes, once, whoever asks first. A replay
/// that failed is not kept: the next caller starts another, so a store
/// that was down at startup is read the first time it is up rather than
/// never (ADR: The ports learn to wait, addendum). Replaying twice is safe,
/// because Record keeps the higher standing and the same bid twice is the
/// same standing.
/// </summary>
public Task LoadAsync()
{
    lock (_loadGate)
    {
        if (_loaded is null || _loaded.IsFaulted || _loaded.IsCanceled)
        {
            _loaded = LoadFromStoreAsync();
        }
        return _loaded;
    }
}

/// <summary>Every stored bid, recorded into both indexes in the order the store returns them.</summary>
private async Task LoadFromStoreAsync()
{
    foreach (var bid in await _store.LoadAsync())
    {
        Record(bid.UserId, bid.VehicleId, bid.State);
    }
}
```
*Live from [`api/TheYard.Application/BidService.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Application/BidService.cs#L38-L89), region `store`, read from this build at 1d6b50b.*

**`InventoryService` warms once and reads a finished task after that.** The
`Lazy` that held the catalogue now holds the task that loads it. `WarmAsync`
starts the load; every synchronous accessor reads the task's result, and after
the host has awaited `WarmAsync` that read is a field access on a task that
finished at startup. A caller that skips the warm-up, which is what a unit test
over an in-memory source does, blocks on a task the memory source has already
completed, a wait of no time. The only way to block a thread here for real is to
skip the warm-up against a store that goes over the network, and the host does
not:

```csharp
// One load, shared by every caller, started by whoever asks first. The
// task is what is shared rather than the result, so two callers arriving
// together wait on the same load instead of running two.
//
// The host starts WarmAsync beside the server (Composition/Startup.cs) and the
// pipeline awaits it before any store-bound endpoint runs (Warmth, Stores.cs), so on a
// running site the accessors below read a task that has already finished and
// never block. A caller that skips the warm-up, which is what a unit test
// over an in-memory source does, blocks on a task that a memory source has
// already completed, which is a wait of no time. The only way to block a
// thread here for real is to skip the warm-up against a store that has to
// go over the network, and the host does not (ADR: The ports learn to wait).
//
// A load that failed is not kept. A Lazy would keep a faulted task forever, so
// a store that was unreachable for one second at startup would answer every
// request until the next roll with that second's exception. Here the next
// caller after a failure starts a fresh load; a load in flight or finished is
// shared, and the lock is what makes the start single.
private readonly object _warmGate = new();
private Task<(IReadOnlyList<Vehicle> All, IReadOnlyDictionary<string, Vehicle> ById, VehicleSearchIndex Index, InventoryFacets Facets)>? _inventory;

private Task<(IReadOnlyList<Vehicle> All, IReadOnlyDictionary<string, Vehicle> ById, VehicleSearchIndex Index, InventoryFacets Facets)> InventoryTask()
{
    lock (_warmGate)
    {
        if (_inventory is null || _inventory.IsFaulted || _inventory.IsCanceled)
        {
            _inventory = BuildAsync(vehicleSource, manifestSource, imagePathPrefix);
        }
        return _inventory;
    }
}

/// <summary>Load the catalogue now, so the first visitor does not pay for it.</summary>
public Task WarmAsync() => InventoryTask();

/// <summary>Whether the catalogue has been loaded, which the host asserts before serving.</summary>
public bool IsWarm => _inventory is { IsCompletedSuccessfully: true };

private (IReadOnlyList<Vehicle> All, IReadOnlyDictionary<string, Vehicle> ById, VehicleSearchIndex Index, InventoryFacets Facets) Inventory =>
    InventoryTask().GetAwaiter().GetResult();
```
*Live from [`api/TheYard.Application/InventoryService.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Application/InventoryService.cs#L34-L74), region `warm`, read from this build at 1d6b50b.*

The host's side of that bargain is two lines, before the pipeline is built:

```csharp
await app.Services.GetRequiredService<InventoryService>().WarmAsync();
await app.Services.GetRequiredService<BidService>().LoadAsync();
```

**The relational adapters became honest about what they always were.** EF Core
has had `ToListAsync`, `FindAsync`, `SaveChangesAsync` and `ExecuteDeleteAsync`
all along; `EfSources.cs` uses them now, and `YardDatabase.Prepare` and
`YardSeed.EnsureSeeded` are `PrepareAsync` and `EnsureSeededAsync`. The JSON
readers use `File.ReadAllTextAsync`. Nothing about what any of them does changed.

## What the tests hold

The shape, by reflection, so that a synchronous member added next year fails
here rather than on a one-vCPU container:

```csharp
/// <summary>
/// Every member of every port returns a Task. The first store was a file and
/// the ports were synchronous because reading a file is; the third store has
/// no synchronous driver at all, and a port that blocks a request thread while
/// a cloud store answers is a performance defect on a one-vCPU container. A
/// synchronous member added later fails here rather than in production.
/// </summary>
[Theory]
[InlineData(typeof(IVehicleSource))]
[InlineData(typeof(IPhotoManifestSource))]
[InlineData(typeof(IBidStore))]
public void Every_port_member_returns_a_task(Type port)
{
    var methods = port.GetMethods(BindingFlags.Public | BindingFlags.Instance);
    Assert.NotEmpty(methods);
    foreach (var method in methods)
    {
        Assert.True(
            typeof(Task).IsAssignableFrom(method.ReturnType),
            $"{port.Name}.{method.Name} returns {method.ReturnType.Name}, and every port member has to be awaitable");
    }
}
```
*Live from [`api/TheYard.Tests/PortsTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/PortsTests.cs#L15-L36), region `port-surface`, read from this build at 1d6b50b.*

And the gate, with fifty bids at the minimum arriving together through a store
that takes a moment to answer. Exactly one is accepted and the store is written
exactly once; without the gate two bidders read the same price, both pass the
rules, and the lower one lands second:

```csharp
/// <summary>
/// The semaphore that replaced the lock still admits one bidder at a time.
/// Fifty bids at the minimum arrive together on one vehicle through a store
/// that takes a moment to answer; with the gate, exactly one of them is
/// accepted at each price and the rest are refused as too low, so the count
/// of accepted bids equals the count of distinct amounts that landed. Without
/// it, two bidders read the same standing price, both pass the rules, and the
/// lower one lands second.
/// </summary>
[Fact]
public async Task The_gate_admits_one_bidder_at_a_time_while_the_store_is_answering()
{
    var clock = TestData.ClockAt(new DateTimeOffset(2026, 8, 15, 12, 0, 0, TimeSpan.FromHours(-4)));
    var vehicle = LiveVehicleFor(clock);
    var store = new SlowStore();
    var service = new BidService(store);
    int minimum = BidRules.MinNextBid(vehicle);

    var outcomes = await Task.WhenAll(Enumerable.Range(0, 50).Select(i =>
        service.PlaceBidAsync(vehicle, minimum, clock, $"buyer-{i}")));

    int accepted = outcomes.Count(o => o.Kind == BidOutcomeKind.Accepted);
    Assert.Equal(1, accepted);
    Assert.Equal(1, store.Writes);
    Assert.Equal(49, outcomes.Count(o => o.Kind == BidOutcomeKind.Rejected));
}

private sealed class SlowStore : IBidStore
{
    public int Writes;

    public Task<IReadOnlyList<StoredBid>> LoadAsync() => Task.FromResult<IReadOnlyList<StoredBid>>([]);

    public async Task SaveAsync(string userId, string vehicleId, BidState state)
    {
        await Task.Delay(5);
        Interlocked.Increment(ref Writes);
    }

    public Task ClearAsync(string userId) => Task.CompletedTask;
}
```
*Live from [`api/TheYard.Tests/PortsTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/PortsTests.cs#L40-L80), region `gate`, read from this build at 1d6b50b.*

The rest of the suite followed the ports mechanically: the in-memory fakes in
four test files return `Task.FromResult`, the store tests await their saves, and
the bidding tests await their bids. 325 tests, all green, after the change.

## Consequences

- A request thread is never blocked on a store. The bid endpoint, the reset
  endpoint and the two startup loads are asynchronous end to end.
- The critical section in `BidService` now includes the store's answer, which
  it always did in practice; the difference is that the thread waiting its turn
  is not a thread.
- `IBidStore.SaveAsync` is called inside the gate, so a slow store slows
  bidding rather than the site, which is the right thing for it to slow.
- Every adapter, present and future, has to be asynchronous. That is the point.
- `RunChecks` on the health endpoint is still synchronous; the Cosmos probe it
  gains is the one place a health check awaits a store, and the health record
  says how.

## Where it sits

This decision starts in Application, where every member of `IVehicleSource`, `IPhotoManifestSource` and `IBidStore` in `Ports.cs` now returns a `Task` and `BidService` guards its writes with a `SemaphoreSlim`, then spreads outward to the async adapters in Infrastructure and to the host, which awaits `WarmAsync` and `LoadAsync` from `Composition/Startup.cs` before serving. The ports had been shaped by the first store, a file read that never waits; a network store behind them would have blocked the only thread on a one-vCPU container. The change cost every adapter, fake and test a mechanical rewrite, and the shared warm-up task later needed fixing so a failed load could be retried. A store that only ever read local files on a many-core host would not have needed this, though any network store makes it the right shape.

## Files

- [`api/TheYard.Application/Ports.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/Ports.cs): the three seams, awaitable.
- [`api/TheYard.Application/BidService.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/BidService.cs): the semaphore, and the load that moved out of the constructor.
- [`api/TheYard.Application/InventoryService.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Application/InventoryService.cs): the warm-up.
- [`api/TheYard.Infrastructure/EfSources.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Infrastructure/EfSources.cs): the relational adapters, asynchronous.
- [`api/TheYard.Infrastructure/JsonFileSources.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Infrastructure/JsonFileSources.cs): the file readers, the same.
- [`api/TheYard.Api/Program.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/Program.cs): the two awaits before the pipeline.
- [`api/TheYard.Api/Endpoints/BidEndpoints.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Api/Endpoints/BidEndpoints.cs): the bid endpoints.
- [`api/TheYard.Tests/PortsTests.cs`](https://github.com/SteveStout/TheYard/blob/main/api/TheYard.Tests/PortsTests.cs): the shape and the gate.

## Addendum, 2026-09-09: a failed warm is tried again

The `Lazy<Task>` that shared one load between every caller kept a faulted
task as faithfully as a finished one, so a store that was unreachable for one
second at startup would have answered every request until the next roll with
that second's exception, while the host's log said the store's first visitor
would try again (a review with no context read both and put them side by
side). `InventoryService.WarmAsync` and `BidService.LoadAsync` now keep the
task themselves: a load in flight or finished is shared as before, and a load
that failed is replaced by the next caller's, under a lock so the start stays
single. Replaying the bid store twice is safe because `Record` keeps the
higher standing. Two tests hold it, one per service, each with a source that
is down for its first call and up for its second. The live blocks above show
the code as it is.

## Addendum, 2026-09-09: the one path the claim did not cover

"A request thread is never blocked on a store" was true of the host this
record describes, which warmed its one store before it served. Since one
container runs both stores (ADR: One container, both stores) there is a
second store, warmed in the background on a deployed group and not at all in
a test application, and a request that reached it before the warm finished
read `InventoryService`'s synchronous accessor and blocked its thread on the
load, for the seconds after a roll on the live site and on every first
request to the other store under test. A review with no context read the
accessor and this record together and asked. The pipeline now awaits the
request's store's warm before any endpoint runs, which after the first time
is an await on a task that finished at startup; the accessor is unchanged and
is reached only warm (ADR: Three readers with no memory of the project).

```csharp
/// <summary>
/// The request's store, warm before any endpoint reads it.
///
/// <para>The host starts the default store's catalogue loading beside the
/// server and warms the others in the background on a deployed group, so a
/// request that arrives before a store's load has finished, any request in the
/// first minutes after a roll, a visitor following the toggle, or any request
/// to the other store in a test application where the background warm is off,
/// finds a catalogue still loading. <c>InventoryService</c>'s synchronous accessors then blocked the
/// request thread on the load, on a container with one vCPU and therefore one
/// thread pool worker to start with: the exact shape ADR: The ports learn to
/// wait argued against (ADR: Three readers with no memory of the project).</para>
///
/// <para>So the pipeline awaits the warm, once per request, before the
/// endpoint runs. After the first time it is an await on a task that finished
/// at startup, which is no wait at all. The bid replay is awaited by the
/// writing methods already and read by the reads as whatever has loaded, so
/// it is not gated here; a listing during the seconds a store's bids replay
/// shows the dataset's prices until the replay lands.</para>
/// </summary>
public static class Warmth
{
    /// <summary>Warms the backend's catalogue if it is not warm; a warm one is a task already finished.</summary>
    public static Task EnsureAsync(Backend backend) =>
        backend.Inventory.IsWarm ? Task.CompletedTask : backend.Inventory.WarmAsync();
}
```
*Live from [`api/TheYard.Api/Stores.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Stores.cs#L137-L162), region `warm-before-reading`, read from this build at 1d6b50b.*

```csharp
[Fact]
public async Task A_request_on_a_cold_store_waits_for_the_catalogue_without_blocking()
{
    var source = new GatedVehicles();
    var backend = ColdBackend(source);

    var waiting = Warmth.EnsureAsync(backend);

    // Still loading: the task is pending and the store says so. A version
    // that blocked would never have returned this task at all.
    Assert.False(waiting.IsCompleted);
    Assert.False(backend.Inventory.IsWarm);

    source.Release();
    await waiting;

    Assert.True(backend.Inventory.IsWarm);
    Assert.Empty(backend.Inventory.GetAll());
}

[Fact]
public async Task A_warm_store_is_waited_on_for_no_time_at_all()
{
    var source = new GatedVehicles();
    var backend = ColdBackend(source);
    source.Release();
    await backend.Inventory.WarmAsync();

    var again = Warmth.EnsureAsync(backend);

    Assert.True(again.IsCompletedSuccessfully);
    Assert.Equal(1, source.Loads);
}
```
*Live from [`api/TheYard.Tests/WarmthTests.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Tests/WarmthTests.cs#L18-L50), region `warm-before-reading`, read from this build at 1d6b50b.*

## Addendum, 2026-10-07 (1.0.3.89): the server listens first

The host no longer loads the default store's catalogue before it listens. Measured on the three rolls App Insights held in full (1.0.3.84, 1.0.3.85 and 1.0.3.87), App Service stopped the old container 1 to 3.5 minutes before the new one was listening, because the new one loaded a hundred thousand vehicles first, on the one core both sites share, and for that stretch nothing answered at all. Steve's call on 7 October: answer first, load after.

So the warm runs beside the server. The page's own files and the platform's probes, `/healthz` and `/readyz`, answer within seconds of the container starting; every API read waits for the load, and waits without holding a thread, because the pipeline already awaits the request's store before any `/api` endpoint runs (the addendum above). The bids replay before the catalogue loads, not after: they are a few rows and done in well under the catalogue's time, so a read let through by the warm finds them in place, and nobody sees a sold vehicle offered or their own bids missing after a roll. Readiness now includes the catalogue, so `/readyz` says 503 until it has loaded, and the deploy's Verify step polls it the way it polls the version. One promise of this record is kept in a different shape: a dataset that cannot be loaded still ends the process, but by logging the failure and stopping the host after it has started, rather than by never starting it.

```csharp
// The default store's bids and catalogue load beside the server, not before
// it. The server listens at once, so a roll's new container answers the
// platform's probes and serves the page's files in seconds. Every API read
// waits for the catalogue, and waits without holding a thread, because the
// pipeline awaits the warm before any /api endpoint runs (Warmth, Stores.cs).
// /readyz says not ready until the catalogue is in, so the deploy and a
// blue-green swap wait for the same thing a visitor would.
//
// The bids replay first. It is a few rows against a hundred thousand
// vehicles, so it is done long before the catalogue, and an API read let
// through by the catalogue's warm finds every bid already in place: no
// visitor sees a sold vehicle offered, or their own bids missing, in the
// first minutes after a roll.
//
// Until this, the process loaded a hundred thousand vehicles before it
// listened, and a roll answered nothing for the minutes that took (ADR: The
// ports learn to wait, the addendum on listening first). A dataset that
// cannot be loaded still ends the process, loudly, rather than serving a
// site with no catalogue: the failure is logged and the host is stopped,
// so the platform restarts it and says so. A host already stopping, as a
// test's host does when it is disposed mid-load, is left to stop.
_ = Task.Run(async () =>
{
    try
    {
        await backends.Default.Startup.Time("bids", backends.Default.Bids.LoadAsync);
        await backends.Default.Startup.Time("catalogue", backends.Default.Inventory.WarmAsync);
        backends.Default.Startup.Ready();
    }
    catch (Exception ex) when (!app.Lifetime.ApplicationStopping.IsCancellationRequested)
    {
        app.Logger.LogCritical(ex, "The {Store} store could not be loaded; stopping, so the platform restarts the container", backends.Default.Name);
        app.Lifetime.StopApplication();
    }
});
```
*Live from [`api/TheYard.Api/Composition/Startup.cs`](https://github.com/SteveStout/TheYard/blob/1d6b50b/api/TheYard.Api/Composition/Startup.cs#L126-L160), region `listen-first`, read from this build at 1d6b50b.*


## Addendum, 2026-10-07 (1.0.3.91): what listening first changed, measured

The addendum above blamed the dark stretch on the catalogue loading before the server listened. The roll of 1.0.3.89 says that was most of the wrong answer. A probe on Steve's machine read each site's origin and domain every two seconds through the roll (`greenlane-probe-10389.log`): the old containers' last answer arrived at 14:51:08 CDT, and nothing answered, `/healthz` included, until 14:54:15, 186 seconds. `/healthz` answers the instant the process listens and reads no store, so those three minutes are App Service replacing the container on its one instance: stopping the old one, then pulling and starting the new one. Both domains answered 504 after 28 seconds, repeatedly, through that stretch.

What listening first did change is everything after it. The new version's API answered within seconds of `/healthz` returning, and the slowest read after that was 2 to 7 seconds with one first read of 29 seconds, against worst reads of 18 to 112 seconds in the first 150 seconds of the three rolls Application Insights held in full. The dark stretch is the platform's, and no change inside the container removes it; two containers overlapping does, which on this plan needs memory it does not have: the plan reads 92 per cent used at the median over ten days.
