Skip to content

Commit f74aa51

Browse files
Merge issue #102: bound compensation recovery deadlines
2 parents 49ae36c + c55fa23 commit f74aa51

9 files changed

Lines changed: 1243 additions & 163 deletions

File tree

.github/workflows/ci.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,9 @@ jobs:
963963
--timeout-seconds 180
964964
--web-stabilization-timeout-seconds 240
965965
--worker-stabilization-timeout-seconds 420
966+
--web-recovery-timeout-seconds 240
967+
--worker-recovery-timeout-seconds 420
968+
--recovery-phase-timeout-seconds 720
966969
)
967970
for subnet_id in "${subnet_ids[@]}"; do runtime_args+=(--subnet-id "$subnet_id"); done
968971
for security_group_id in "${security_group_ids[@]}"; do
@@ -1065,6 +1068,9 @@ jobs:
10651068
--timeout-seconds 180
10661069
--web-stabilization-timeout-seconds 240
10671070
--worker-stabilization-timeout-seconds 420
1071+
--web-recovery-timeout-seconds 240
1072+
--worker-recovery-timeout-seconds 420
1073+
--recovery-phase-timeout-seconds 720
10681074
)
10691075
for subnet_id in "${subnet_ids[@]}"; do runtime_args+=(--subnet-id "$subnet_id"); done
10701076
for security_group_id in "${security_group_ids[@]}"; do
@@ -1272,7 +1278,8 @@ jobs:
12721278
uv run python -m deploy.cli restore-finalization \
12731279
"${runtime_args[@]}" \
12741280
--recovery-context .tmp/deployment/recovery-context.json \
1275-
--failed-release-record .tmp/deployment/successful-release.json || restore_status=$?
1281+
--failed-release-record .tmp/deployment/successful-release.json \
1282+
--evidence-path .tmp/deployment/deployment-evidence.json || restore_status=$?
12761283
RESTORE_STATUS="$restore_status" uv run --frozen python - <<'PY'
12771284
import datetime
12781285
import json

_docs/compatibility/development-terminology-allowlist.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,15 +180,15 @@
180180
},
181181
{
182182
"path": "core/tests/test_deployment_release.py",
183-
"sha256": "155633580c16bca93c8021b06ba65e8a28760597c45eddb157d134e3f73cf533",
183+
"sha256": "ce4944bb797d2480ec14422048fc9982bf2e590384e471b7f559f65127127dfd",
184184
"class": "legacy_contract_test",
185185
"reason": "Release tests exercise exact physical task, secret, repository, and tag values.",
186186
"follow_up": "#94",
187187
"expected_count": 5
188188
},
189189
{
190190
"path": "core/tests/test_deployment_workflow.py",
191-
"sha256": "81a10ead3246687edc580538cf388fdc8f5f0d9a78468f3ed739ae041d935521",
191+
"sha256": "30226ff2be6f702b0f946681c98c92b269fcc11e8dd4c425c6ee2b1c5d21e6cf",
192192
"class": "legacy_contract_test",
193193
"reason": "Workflow tests exercise removed inputs, frozen Gate-B records, and exact physical values.",
194194
"follow_up": "#94",

_docs/runbooks/development-release.md

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -956,7 +956,7 @@ The normal automatic sequence is quality/deployment-contract, Django with Postgr
956956
one tested image, exact active-pair capture, immutable publication, migration exit `0`, stable and
957957
healthy web with exact SHA, singleton worker, read-only HTTP/browser smoke (including safe 404),
958958
terminal exact-pair verification, and artifact finalization. The deployer session is fixed at 3600
959-
seconds. The general stage, public-health, and compensation wait remains 180 seconds. A forward
959+
seconds. The general stage and public-health budgets remain 180 seconds. A forward
960960
promotion or rollback that intentionally starts/replaces web receives the explicit, code-owned
961961
240-second web-stabilization budget; 240 seconds is also its hard maximum. Only a forward promotion
962962
or rollback that starts/replaces the singleton worker receives the separate explicit, code-owned
@@ -965,7 +965,10 @@ workflow-dispatch input or an arbitrary operator override. A mutating `UpdateSer
965965
only while time remains. Read-only polling makes exactly one final ECS service observation at the
966966
monotonic deadline and never sleeps or polls again afterward. Exact completion in that observation
967967
succeeds; an incomplete or invalid observation fails. Any response returned after the deadline is
968-
discarded.
968+
discarded. Recovery has separate fixed code-owned bounds: 240 seconds for web, 420 seconds for the
969+
singleton worker, and 720 seconds for the complete recovery phase. Those values are fixed workflow
970+
arguments with matching hard maxima; they are not dispatch inputs, repository variables,
971+
environment overrides, or permission for a larger operator value.
969972

970973
Web completion still requires its expected task-definition ARN and desired count, exactly one
971974
`PRIMARY` deployment with that definition and count, exact service and primary running/pending
@@ -1002,7 +1005,7 @@ ARN. The immutable receipt target remains the requested tuple. For a positive re
10021005
may initially return that exact new PRIMARY at deployment-level desired/running/pending/failed
10031006
`0/0/0/0` and `IN_PROGRESS`, while the service-level tuple already has the requested count. That
10041007
AWS initialization shape binds identity but is poll-only. It does not rewrite `B/1` to `B/0`, prove
1005-
stabilization, start public health, or permit the worker mutation.
1008+
stabilization, start public health, or, during forward rollout, permit the worker mutation.
10061009

10071010
A structurally partial acknowledgement is reconciled with an immediate `DescribeServices` call;
10081011
there is no preliminary sleep. Every present member must be correctly typed, and the complete set
@@ -1068,14 +1071,54 @@ remained the untouched exact prior singleton. This run remains failed; ultimate
10681071
not a release success and does not substitute for a bound restorative receipt, terminal proof,
10691072
public health, smoke, or a successful-release record.
10701073

1074+
Run `31289994036` on 2026-08-09 failed at the deployed HTTP smoke because the smoke assertion for
1075+
the intentional `/courses/` production canonical was stale. The new web task was healthy and
1076+
served the exact candidate SHA; this was not an ECS rollout failure. Automatic compensation began
1077+
after that failure, spent about 160 seconds restoring and serially waiting for web, then issued the
1078+
worker restore under the unrelated 180-second general-stage budget. The controller timed out about
1079+
183 seconds into that worker wait. ECS later converged to the exact captured worker about 280
1080+
seconds after its restore began, inside the accepted 420-second singleton-worker behavior. The run
1081+
and its compensation remain failed: later convergence supplied neither a timely worker receipt
1082+
proof nor the required terminal pair and public-health proof.
1083+
10711084
If an attempted `UpdateService` response is lost or invalid, recovery uses the same absolute
1072-
180-second per-service deadline for candidate reconciliation and the restorative receipt wait.
1085+
per-workload deadline for candidate reconciliation, restorative receipt acquisition, and
1086+
stabilization: web uses exactly 240 seconds and worker exactly 420 seconds. One absolute 720-second
1087+
phase deadline starts before the first attempted-predecessor observation or recovery mutation. A
1088+
workload deadline is `min(workload recovery start + workload budget, phase deadline)` and is never
1089+
reset by acknowledgement, reconciliation, polling, error handling, terminal proof, health proof,
1090+
or evidence handling.
10731091
It polls only the captured terminal identity under the same bounded retirement rules and the
10741092
actually attempted target until that target is observed as the unique PRIMARY and its deployment ID
1075-
can be bound while time remains. The deadline is finite, cannot exceed the general-stage maximum,
1076-
and is never restarted between capture and restore. A capture that returns exactly at the deadline
1093+
can be bound while time remains. Both ambiguous attempted identities are reconciled in fixed
1094+
`web -> worker` order before restorative mutation, so a reconciliation wait cannot split the two
1095+
restorative updates. The controller then issues and binds the exact web restore followed immediately
1096+
by the exact worker restore. It performs no stabilization wait, public-health check, terminal
1097+
proof, evidence write/upload, or deliberate sleep between those two successfully bound restores;
1098+
only receipt acknowledgement and its immediate exact reconciliation belong to binding. A capture
1099+
that returns exactly at its workload deadline
10771100
cannot start the restorative mutation. A speculative workload that was never invoked is absent from
10781101
the recovery allowlist.
1102+
1103+
After all eligible restore bindings have been attempted, the controller observes the retained
1104+
receipts cooperatively in single-threaded `web -> worker` rounds. There are no threads, processes,
1105+
async calls, concurrent SDK mutations, or blind retries. Each pending workload receives one
1106+
observation per round against its own receipt, predecessor set, and absolute deadline. The
1107+
controller sleeps at most once after a round, bounded by the poll interval, the earliest pending
1108+
workload deadline, and the phase deadline. Completion or failure removes only that workload from
1109+
later rounds; it cannot consume, reset, or suppress the other workload's observation. Equality is
1110+
inclusive: an exact terminal response on the workload deadline passes. An incomplete response on
1111+
that final read expires, no later sleep/read is allowed, and any provider response returned after
1112+
the workload or phase deadline is rejected.
1113+
1114+
An isolated workload binding or observation error is retained while the other already-authorized
1115+
restore proceeds within its own deadline. The failed mutation is never retried and identity is
1116+
never inferred from later convergence. Invalid prior context, workload allowlist, budget, or a
1117+
global attribution contradiction stops further mutation. Any retained error makes total recovery
1118+
fail even if later read-only state appears converged. A contradiction or unknown error has
1119+
precedence over deadline expiry; `receipt_deadline_expired` is retained only when every recovery
1120+
error is that allowlisted reason.
1121+
10791122
An attempted candidate may be failed while its restorative receipt replaces it, but the recovery
10801123
receipt itself may not fail. Every restorative call must return a new receipt, including an
10811124
`A -> A` force-new recovery; the old `A` deployment is always a predecessor, never recovery success.
@@ -1091,16 +1134,43 @@ part of the final pair proof. Once worker mutation was actually attempted, its r
10911134
receipt-bound. Artifact-finalization recovery intentionally restores both workloads because both
10921135
belong to the failed release that had already reached terminal proof.
10931136

1137+
Only after all receipt observations finish does the controller perform one exact terminal pair
1138+
proof and, for a non-bootstrap prior release, exact prior-SHA public readiness/liveness. Success
1139+
requires one newly bound receipt for every mutated workload, each receipt as the unique exact
1140+
`PRIMARY` with `COMPLETED`, exact task definition and desired/running/pending counts, zero failed
1141+
tasks, and only absent or exact recognized zero-work predecessors. The terminal pair must bind both
1142+
exact captured task definitions/counts/receipt IDs, active task definitions, image digest, source
1143+
identity, and the singleton worker. Bootstrap `0/0` skips public health but still proves exact zero
1144+
terminal state. Missing or duplicate receipts/PRIMARYs, third or cross-paired identities, unsafe
1145+
predecessors, failed tasks/rollouts, inexact counts, SHA/readiness mismatch, phase expiry, or any
1146+
unclassified error fails closed. Ultimate ECS convergence is not a substitute.
1147+
1148+
Compensation never creates or preserves a rollback-eligible success record. An exact restored pair
1149+
does not change the original promotion or rollback failure: that release still ends red.
1150+
Artifact-finalization recovery removes the local failed-release record only after this complete
1151+
exact recovery passes and also ends red.
1152+
1153+
At worker capture, acknowledgement, every cooperative observation, and terminal proof, service
1154+
`running + pending` and the sum across all recognized deployments must each remain at most one.
1155+
Final active-task proof requires exactly the captured worker task when desired count is one and no
1156+
task for bootstrap. Any transient or final overlap is an immediate contradiction and cannot be
1157+
masked by a later singleton read.
1158+
10941159
For triage, compare redacted service/PRIMARY tuples and deployment IDs against the recorded receipt
10951160
and phase predecessors. Forward, rollback, compensation, and artifact-finalization recovery expose
1096-
each bound receipt using only its workload, deployment ID, allowlisted binding reason, and whether
1161+
the fixed plan (`240/420/720`, restore order, eligible and intentionally untouched workloads), each
1162+
bound receipt using only its workload, deployment ID, allowlisted binding reason, and whether
10971163
an exact terminal reconciliation observation was carried. The summary is recorded before waiting,
1098-
so it remains available after a post-binding failure. Evidence uses only allowlisted reason codes
1164+
but after every eligible binding has been attempted, so it remains available after a post-binding
1165+
failure without splitting the web/worker restore sequence. Per-workload evidence reports only
1166+
`passed`, `receipt_deadline_expired`, or `contract_contradiction` plus the intentionally-untouched
1167+
boolean. Terminal-pair, public-health, worker-singleton, and total results are separate safe facts.
1168+
Evidence uses only allowlisted reason codes
10991169
for complete binding, zero-count initialization, partial acknowledgement reconciliation,
11001170
contradiction, and receipt deadline expiry; it never stores the raw provider payload. Do not retry
11011171
the mutation, add an unrecognized identity, pre-sleep before
11021172
the first observation, or infer adoption from a running task, target health, logs, or an old
1103-
completed deployment. A candidate that never becomes the unique PRIMARY by the shared recovery
1173+
completed deployment. A candidate that never becomes the unique PRIMARY by its workload recovery
11041174
deadline, or any third/cross-paired identity, leaves recovery failed closed for operator review.
11051175

11061176
Restorative failure classification retains `receipt_deadline_expired` only when every observed
@@ -1109,15 +1179,22 @@ restorative error is that allowlisted deadline reason. If any workload, terminal
11091179
reason is also collapsed to `contract_contradiction`; raw exception messages and provider payloads
11101180
are never propagated into evidence or CLI output.
11111181

1182+
Recovery evidence, CLI output, exception text, and artifacts never contain raw AWS responses,
1183+
provider exception messages, request/response bodies, URLs with query strings, headers, cookies,
1184+
credentials, tokens, environment values, task logs, or recovery-context contents. Exact
1185+
task-definition ARNs, deployment IDs, desired/count tuples, booleans, and the documented reason
1186+
codes are the complete safe operator allowlist.
1187+
11121188
The conservative critical-stage recovery envelope is
11131189
`180 + 120 + 240 + 180 + 420 + 180 + 360 + 720 = 2400` seconds: migration observation,
11141190
stopped-migration terminal proof, web stabilization, public readiness/liveness, worker
11151191
stabilization, deployed browser smoke, three critical two-minute artifact uploads, and the
11161192
12-minute finalization-recovery cap. This deliberately conservative sum includes mutually
11171193
exclusive migration-stop and later recovery work. It leaves `3600 - 2400 = 1200` seconds (20
11181194
minutes) of the fixed deployer session for recovery. Automatic compensation and finalization
1119-
recovery inherit neither forward stabilization budget: their exact-pair service waits stay at 180
1120-
seconds and remain inside the separate 12-minute recovery cap. Migration observation, public
1195+
recovery use the same mutually exclusive 720-second phase cap, with the separate fixed 240-second
1196+
web and 420-second singleton-worker recovery deadlines inside it. They do not inherit or alter the
1197+
forward stabilization waits. Migration observation, public
11211198
health, browser smoke, and artifact finalization likewise retain their existing independent
11221199
bounds. Do not raise any timeout in workflow inputs or code. If either reviewed stabilization
11231200
value is insufficient, disable automatic deployment and file/groom another issue with new

0 commit comments

Comments
 (0)