diff --git a/scripts/start-local.ps1 b/scripts/start-local.ps1 index d3b7348..28af2cc 100644 --- a/scripts/start-local.ps1 +++ b/scripts/start-local.ps1 @@ -65,6 +65,11 @@ $ErrorActionPreference = 'Stop' $RepoRoot = Split-Path -Parent $PSScriptRoot $StateDir = Join-Path $RepoRoot '.local' $PidFile = Join-Path $StateDir 'pids.json' + +# Whether this run is starting from an empty database. Set below, and read by +# Start-Backend, which must refuse to reuse a backend that is still serving the +# previous one. See #257. +$script:DatabaseIsNew = $false # Which database this run uses. Everything downstream reads these four, so the # choice is made once here rather than branched at each use. if ($E2eDb) { @@ -184,14 +189,22 @@ function Start-Database { Write-Step 'Removing the existing database (-Fresh)' docker rm -f $Container *>$null Write-Good 'removed' + $script:DatabaseIsNew = $true } + # The e2e container stores its data on tmpfs, so it comes up empty whether it + # is created or merely restarted. There is no case where it carries anything + # over, which is the point of it (#186) — and it means a backend from a + # previous run is always stale against it. + if ($E2eDb) { $script:DatabaseIsNew = $true } + $existing = (docker ps -a --filter "name=^/$Container$" --format '{{.Names}}') if ($existing -eq $Container) { Write-Step "Reusing the database container" docker start $Container *>$null } else { + $script:DatabaseIsNew = $true Write-Step "Creating the database container on port $DbPort" # tmpfs for the e2e database only: its contents are worthless the # moment the run ends, and storing nothing is what makes it start empty @@ -269,6 +282,41 @@ function Start-Backend { finally { Pop-Location } if (Test-Listening -Port $ApiPort) { + # Leaving a stranger's backend alone is only safe when the database has + # not just changed underneath it. When it has, that process is serving a + # database it no longer owns, and its in-memory state describes rows that + # no longer exist. + # + # That is not theoretical. The rate limiter keys on customer id and keeps + # its buckets in memory for an hour; a recreated database restarts ids at + # 1, so a new customer inherits a previous run's spent allowance. It + # reproduced exactly the intermittent resend-verification failure in #257 + # — three refusal toasts where one was expected — and the control (same + # id restart, restarted backend) passed. Whatever else is stale, a + # process serving the wrong database gives wrong answers confidently, + # which is the worst kind. + # + # So this refuses rather than continuing, and says what to do. A run that + # stops loudly is recoverable; one that quietly tests the wrong thing is + # what cost two wrong measurements in this project already. + $tracked = Get-TrackedProcesses + $ours = $tracked.ContainsKey('backend') -and + (Get-Process -Id $tracked['backend'] -ErrorAction SilentlyContinue) + + if ($script:DatabaseIsNew -and -not $ours) { + throw @" +Something is already listening on $ApiPort, and this run just created a new database. + +That process is serving the previous database. Its in-memory state — rate-limit +buckets keyed on customer id, most obviously — describes rows that no longer +exist, and recycled ids will inherit them (#257). + +Stop it and run this again: + + .\scripts\start-local.ps1 -Stop +"@ + } + Write-Note "something is already listening on $ApiPort; leaving it alone" return }