diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b5fcf9c..92989ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Added a calm Latest state/Evolution flow to the map-first explorer, including period selection, metric comparison and added/removed/modified overlays where source identities support them. - Added explicit operator provisioners for official Statbel Mol population snapshots (2021-2025) and Digitaal Vlaanderen historical land-use snapshots (1778, 1873 and 1969); no source is fetched during application startup. - Preserved methodological honesty: partial statistical sectors are labelled area-weighted estimates, historical land-use identity changes are not fabricated and all source URLs, versions and processing limitations are persisted. +- Serialized Tower startup and migration-smoke validation by waiting for container health, preventing concurrent Alembic upgrades from racing on the same PostGIS schema. ## Sprint 186 Map-first Mol geographic explorer (2026-07-14) diff --git a/backend/tests/test_sprint187_temporal_map_foundation.py b/backend/tests/test_sprint187_temporal_map_foundation.py index fe40af64..76ed41f5 100644 --- a/backend/tests/test_sprint187_temporal_map_foundation.py +++ b/backend/tests/test_sprint187_temporal_map_foundation.py @@ -248,3 +248,13 @@ def test_temporal_frontend_and_official_operator_contracts_exist() -> None: assert "provision_mol_population_history.py" in dockerfile assert "provision_mol_historical_landuse.py" in dockerfile assert "fake" not in population.lower() + + +def test_tower_deploy_waits_for_startup_migration_before_live_smoke() -> None: + for relative_path in ("scripts/deploy_tower.ps1", "scripts/deploy_tower.sh"): + script = (ROOT / relative_path).read_text(encoding="utf-8") + wait_position = script.index("wait_for_geointel_health") + invocation_position = script.index("\nwait_for_geointel_health", wait_position) + smoke_position = script.index("LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh") + assert "docker inspect --format" in script + assert invocation_position < smoke_position diff --git a/docs/CODEX_EXECUTION_LOG.md b/docs/CODEX_EXECUTION_LOG.md index 5b4a0621..5265d042 100644 --- a/docs/CODEX_EXECUTION_LOG.md +++ b/docs/CODEX_EXECUTION_LOG.md @@ -7794,3 +7794,8 @@ Validation before live deployment: Next: - Run the complete readiness gate, deploy to Tower/PostGIS, provision the official snapshots and verify current/evolution selection end to end in the internal browser. + +Live deployment correction: +- The first Tower rollout exposed a deployment race: all-in-one startup and `live_migration_smoke.sh` both began `alembic upgrade head` after PostgreSQL became reachable. +- Startup committed head `202607140001`; the concurrent smoke transaction rolled back on a duplicate first column. Database contents and the successful migration remained healthy. +- Both Tower deploy entry points now wait for the `geointel` container healthcheck, which includes completed startup migrations and backend readiness, before launching the independent migration smoke. diff --git a/scripts/deploy_tower.ps1 b/scripts/deploy_tower.ps1 index 58c616d7..1c9778b0 100644 --- a/scripts/deploy_tower.ps1 +++ b/scripts/deploy_tower.ps1 @@ -49,6 +49,30 @@ docker compose -f docker-compose.unraid.yml config >/dev/null docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest . bash deploy/unraid/run-dockerman-container.sh +wait_for_geointel_health() { + local status='' + for attempt in $(seq 1 90); do + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)" + if [ "$status" = 'healthy' ]; then + echo "GeoIntel container is healthy after attempt $attempt." + return 0 + fi + if [ "$status" = 'unhealthy' ] || [ "$status" = 'exited' ] || [ "$status" = 'dead' ]; then + echo "GeoIntel container entered terminal state: $status" >&2 + docker logs --tail 120 geointel >&2 || true + return 1 + fi + sleep 2 + done + echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2 + docker logs --tail 120 geointel >&2 || true + return 1 +} + +# Container startup owns the migration. Waiting here prevents the validation +# smoke from racing a concurrent `alembic upgrade head` against the same DB. +wait_for_geointel_health + if [ -x scripts/live_migration_smoke.sh ]; then LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh fi diff --git a/scripts/deploy_tower.sh b/scripts/deploy_tower.sh index c4dfed00..863205b0 100755 --- a/scripts/deploy_tower.sh +++ b/scripts/deploy_tower.sh @@ -51,6 +51,30 @@ docker compose -f docker-compose.unraid.yml config >/dev/null docker build --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" -f deploy/unraid/Dockerfile.all-in-one -t geointel-all-in-one:latest . bash deploy/unraid/run-dockerman-container.sh +wait_for_geointel_health() { + local status='' + for attempt in $(seq 1 90); do + status="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' geointel 2>/dev/null || true)" + if [[ "$status" == 'healthy' ]]; then + echo "GeoIntel container is healthy after attempt $attempt." + return 0 + fi + if [[ "$status" == 'unhealthy' || "$status" == 'exited' || "$status" == 'dead' ]]; then + echo "GeoIntel container entered terminal state: $status" >&2 + docker logs --tail 120 geointel >&2 || true + return 1 + fi + sleep 2 + done + echo "GeoIntel container did not become healthy (last state: ${status:-missing})." >&2 + docker logs --tail 120 geointel >&2 || true + return 1 +} + +# Container startup owns the migration. Waiting here prevents the validation +# smoke from racing a concurrent `alembic upgrade head` against the same DB. +wait_for_geointel_health + if [[ -x scripts/live_migration_smoke.sh ]]; then LIVE_SMOKE_CONTAINER=geointel bash scripts/live_migration_smoke.sh fi