Document production deployment timing

58e6347eeb72 · AtlantisPleb · · parent 894bdfb85f31

Document production deployment timing

Audit production receipts, release gates, image builds, and rolling replacement behavior. Define lane selection guidance and prioritize changes that reduce direct-load and full-roll latency.

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 472 · 2026-08-27T06:40:51.960860Z

Changed files

  • added docs/2026-08-27-production-deployment-time-audit.md

Diff

1 file changed, +297 -0

docs/2026-08-27-production-deployment-time-audit.md added +297

@@ -0,0 +1,297 @@

1
# Production deployment time audit
2
3
**Date:** August 27, 2026
4
5
**Status:** Current-state audit and improvement plan
6
7
**Scope:** Production releases for `openagents.com`
8
9
## Executive summary
10
11
The deployment system has three valid lanes, but recent production work has
12
used rolling replacement most often. Of 47 live targets with deployment
13
receipts since August 20, 2026, 26 used rolling replacement, 4 used direct
14
load, and 17 predate reliable lane typing or used the relup path. Across all
15
70 targets in the period, the classifier marked 39 for rolling replacement
16
and 19 as direct-load candidates. The remaining 12 targets had no usable
17
classification.
18
19
Rolling replacement is often correct because recent candidates combine
20
assets, configuration, migrations, release files, or other structural changes
21
with application code. It is not the correct default for every release.
22
Two recent candidates that the classifier identified as direct-load candidates
23
were rolled manually. That choice spent minutes replacing containers for
24
changes that the direct-load transaction could have applied in less than one
25
second.
26
27
The measured lane differences are large:
28
29
| Measurement | Sample | Minimum | Median | Maximum |
30
| --- | ---: | ---: | ---: | ---: |
31
| Direct load, promotion to terminal state | 4 | 10.5 s | 33.6 s | 151.0 s |
32
| Direct-load fleet transaction | 4 | 0.29 s | 0.39 s | 0.88 s |
33
| Rolling replacement, promotion to live | 26 | 3.1 min | 8.6 min | 39.4 min |
34
| Forge candidate build for rolling targets | 26 | 8.4 s | 2.0 min | 2.7 min |
35
| Release gate wall time, most recent 15 receipts | 15 | 1 s | 1.7 min | 7.6 min |
36
| Production image build, successful recent builds | 26 | 4.1 min | 5.2 min | 8.9 min |
37
38
These measurements show three immediate priorities:
39
40
1. Select the lane before starting structural-release work. Use direct load
41
   for an allowlisted BEAM-only change, relup for a compatible application
42
   upgrade outside the allowlist, and rolling replacement only when the
43
   candidate changes the runtime structure.
44
2. Automate promotion with a narrowly scoped operator credential. Recent
45
   direct deployments spent 9 to 11 minutes waiting for a manual promotion
46
   even though promotion-to-live took 29 to 38 seconds.
47
3. Instrument and streamline rolling replacement. The current script rebuilds
48
   and pulls an image, restarts three nodes serially, and polls each node every
49
   20 seconds. The durable receipt does not retain the time spent in each node
50
   phase, so it cannot identify whether image pulls, container startup,
51
   clustering, or polling dominates the roll.
52
53
## Method and limitations
54
55
This audit uses the following evidence:
56
57
- Production target and deployment receipts from August 20 through August 27,
58
  2026.
59
- Local exact-SHA release gate receipts under `.git/openagents/release-gate/`.
60
- Successful Cloud Build records for the production image repository.
61
- The deployment policy in `docs/deploymodes.md` and the executable behavior in
62
  `ops/ci/gate.sh`, `ops/deploy/build-image-cloud.sh`,
63
  `ops/deploy/release-to-production.sh`, and
64
  `ops/deploy/fleet-startup.template.sh`.
65
66
The measurements have four limitations:
67
68
- `promotion-to-live` starts when an operator promotes a pushed target. It does
69
  not include the release gate, image build, or time waiting for promotion.
70
- `push-to-live` includes operator delay. It measures what a developer feels,
71
  but it does not isolate system execution time.
72
- Release gate stage durations are cached by input hash. A receipt's total wall
73
  time describes that invocation, while an individual cached stage can report
74
  the duration of an earlier execution. Do not add the stage durations to
75
  reconstruct the total.
76
- Many manually settled rolling receipts record a deployment duration of zero
77
  or a rounded assumption. This audit therefore uses target lifecycle times
78
  for rolling replacement and does not report a receipt-level rollout median.
79
80
## How the lanes should work
81
82
Use the narrowest lane that preserves the production invariants in
83
`docs/deploymodes.md`.
84
85
| Lane | Use it when | Do not use it when | Expected production work |
86
| --- | --- | --- | --- |
87
| Direct load | Every changed module is in the direct-load allowlist; the candidate changes only added or modified BEAM files; the application specification, release version, dependencies, toolchain, and structural paths are unchanged | The candidate deletes a module or changes assets, configuration, migrations, native code, dependencies, or release structure | Build the changed modules, prepare every node, deploy to a canary, and commit the same artifact set across the fleet without restarting containers |
88
| Relup | A pure Elixir application change is outside the direct allowlist, needs an OTP code-change callback, or changes supervised state compatibly; the forward and reverse release packages pass the topology preflight | The candidate changes assets, dependencies, migrations, native code, or has no safe upgrade path | Install the exact relup package one node at a time and retain the reverse package |
89
| Rolling replacement | The candidate changes `mix.exs`, `mix.lock`, runtime configuration, assets, migrations or other release `priv` content, `rel`, native code, the Dockerfile, dependencies, or the toolchain | The classifier proves the candidate is an allowlisted BEAM-only change | Build one immutable image and replace one healthy node at a time |
90
91
Published documentation under `priv/docs/**` is compiled into the allowlisted
92
documentation catalog and can use direct load when it causes no other
93
structural change. Source documentation, comments, and other uncompiled files
94
can produce a zero-module candidate and might require no production action.
95
96
The decision order is:
97
98
1. Use direct load when the classifier and preflight permit it.
99
2. Use relup when direct load is not permitted and the OTP upgrade is
100
   compatible.
101
3. Use rolling replacement for structural candidates or when either narrower
102
   lane refuses the candidate.
103
104
Never override a refusal to obtain a faster release. A refusal is evidence
105
that the candidate does not satisfy that lane's safety contract.
106
107
## Where the current time goes
108
109
### Direct load
110
111
The fleet transaction is already fast. The most recent production direct load
112
deployed one module across all three nodes in 381 ms. Its full
113
promotion-to-live interval was 28.9 seconds, including the Forge build.
114
115
The variable cost is the candidate build. In the four typed direct-load
116
receipts, the build took 10.0 to 149.4 seconds, with a median of 32.5 seconds.
117
The operator queue can cost more than the entire deployment. Three recent
118
direct releases took 9.0, 9.1, and 11.3 minutes from push to live, while their
119
promotion-to-live intervals were 151.0, 38.3, and 28.9 seconds.
120
121
Direct-load optimization should therefore target promotion latency and Forge
122
builder latency. Optimizing the subsecond fleet transaction will not improve
123
the developer's wait materially.
124
125
### Release gate
126
127
The most recent 15 gate receipts took 1 to 454 seconds, with a median of 103
128
seconds. A one-second gate means that every stage reused exact input evidence;
129
it does not mean the full test suite ran in one second. The broadest stage is
130
`mix precommit`: its cache input is the entire tree, so a new commit invalidates
131
it. A recent uncached run took about 4.2 minutes and executed 5,129 checks.
132
133
The production runbook's 30-to-60-minute gate estimate no longer describes the
134
recent measured range. Update that estimate only after the gate receipt reports
135
enough uncached samples to publish percentiles by candidate type.
136
137
### Production image build
138
139
Across 26 successful recent builds, Cloud Build took 246 to 536 seconds from
140
submission to completion, with a median of 314 seconds. The median queue delay
141
was 52 seconds, and the median build execution was 259 seconds.
142
143
The image path compiles the application and assets inside Docker, verifies the
144
packaged revision, and pushes the immutable image. A structural release also
145
causes the Forge builder to compile the candidate after promotion. Those two
146
builds currently run serially in the operator procedure and repeat some work.
147
148
### Three-node rolling replacement
149
150
After promotion, rolling targets took a median of 8.6 minutes to reach live.
151
The Forge candidate build accounts for about 2.0 minutes of that median. The
152
remaining interval includes operator coordination, three sequential node
153
replacements, and settlement.
154
155
For every node, the startup path performs the following costly operations:
156
157
1. Stop and remove the application container.
158
2. Prune unused images.
159
3. Pull the new production image.
160
4. Restart the Cloud SQL proxy and wait for it.
161
5. Start the application container.
162
6. Remove, prune, pull, and restart the builder container.
163
7. Rejoin the distributed Erlang cluster in a background loop.
164
165
The release script checks every other node, updates instance metadata, starts
166
the startup runner, and then checks health every 20 seconds. Across three
167
nodes, fixed polling alone adds up to 60 seconds of detection delay and an
168
expected delay of about 30 seconds. Because the receipt stores only `ready`
169
for each node, it cannot distinguish this delay from image transfer or startup.
170
171
## Why rolling replacement has become the default in practice
172
173
The observed behavior has four causes:
174
175
- Candidates often bundle application code with assets, configuration,
176
  migrations, or release files. One structural path makes the entire candidate
177
  a rolling replacement.
178
- Operators have not had a local, pre-promotion lane forecast that explains
179
  the classifier result before they spend time on a gate and image.
180
- The runbook presents the image pipeline as the primary short path even
181
  though direct load does not need a production image or node restart.
182
- Operators have used rolling replacement as a familiar fallback, including
183
  for at least two candidates that production classified as direct-load
184
  candidates.
185
186
The safety rules are not the problem. The workflow exposes the lane too late
187
and makes the broadest lane easier to start than the narrow lanes.
188
189
## Improvement plan
190
191
### Priority 0: Improve iteration time
192
193
1. **Add a preflight deployment plan command.** Given the current live SHA and
194
   a candidate SHA, print the selected lane, every structural reason, changed
195
   modules, deletions, version requirements, and required artifacts. Use the
196
   same classifier code as production. Make this the first runbook step.
197
2. **Automate explicit promotion.** Add a CLI command that obtains a short-lived
198
   `deployments:promote` credential, promotes one exact SHA, and waits for the
199
   target receipt. Keep promotion separate from push. This change removes the
200
   largest measured direct-load delay without weakening the approval boundary.
201
3. **Make the release command lane-aware.** For a direct candidate, run the
202
   direct preflight and promotion flow without building a container image. For
203
   a relup candidate, build and verify the exact release package. Start the
204
   image path only for a structural candidate.
205
4. **Keep independent changes in separate candidates.** Do not attach an asset,
206
   configuration, migration, or dependency change to an otherwise direct-load
207
   application change when the changes can ship independently. The structural
208
   file forces the combined candidate into rolling replacement.
209
5. **Report developer and system latency separately.** Publish push-to-live,
210
   push-to-promotion, promotion-to-build-complete, deployment transaction, and
211
   promotion-to-live. This split makes operator delay visible without hiding
212
   the developer's actual wait.
213
214
### Priority 1: Speed up full rolling replacement
215
216
1. **Persist phase and node timings.** Record gate start and finish, Cloud Build
217
   queue and execution, Forge build, authorization, and settlement. For every
218
   node, record drain, image pull, application start, health, cluster rejoin,
219
   and builder start. Replace zero-duration manual receipts with measured
220
   monotonic durations.
221
2. **Pre-pull the immutable image.** Pull and verify the target digest on all
222
   three nodes while the old containers still serve traffic. Drain and restart
223
   one node only after every node has the verified image. Retain the previous
224
   digest for rollback and do not prune it before the new node is healthy.
225
3. **Stop restarting unchanged support containers.** Keep the Cloud SQL proxy
226
   running when its image and arguments are unchanged. Restart the builder only
227
   when its image digest or configuration changed. These processes do not need
228
   to share the application container's release cadence.
229
4. **Use adaptive health polling.** Poll every 2 seconds during the first 30
230
   seconds, then back off to 5 seconds with jitter while preserving the current
231
   10-minute refusal timeout. This removes most fixed detection delay without
232
   weakening readiness checks.
233
5. **Run independent builds concurrently.** After the exact-SHA gate passes,
234
   start the immutable image build and Forge candidate build together for a
235
   forecast structural candidate. Bind both receipts to the same SHA and do not
236
   authorize replacement until both succeed. At current medians, serial builds
237
   cost about 7.2 minutes; overlap can approach the slower 5.2-minute build.
238
6. **Use one rolling coordinator.** Move the shell loop behind the existing
239
   rolling-replacement coordinator so authorization, other-node health,
240
   idempotent node skipping, timing, and settlement share one implementation.
241
   Keep the script as an operator entry point instead of a second state machine.
242
243
### Priority 2: Reduce structural-release frequency
244
245
1. **Review the direct-load allowlist with evidence.** Add a namespace only
246
   after tests prove state compatibility, rollback, and fleet convergence. Do
247
   not widen the allowlist to make the classifier quiet.
248
2. **Evaluate an immutable asset deployment lane.** Fingerprinted CSS and
249
   JavaScript could move independently if the application serves an atomic
250
   asset manifest and retains the previous bundle through rollback. Until that
251
   design exists and has executable invariants, assets remain structural.
252
3. **Reduce duplicated compilation.** Let the image build consume a verified
253
   candidate artifact or let both builders share content-addressed compilation
254
   inputs. Preserve independent revision verification at the final image.
255
4. **Split the builder lifecycle from the web release.** Pin a builder digest
256
   separately so routine web rolls do not pull and restart an unchanged builder
257
   on every node.
258
259
## Proposed targets
260
261
Use the following targets after phase timing lands and produces at least 20
262
valid samples per lane:
263
264
| Metric | Initial target |
265
| --- | ---: |
266
| Direct-load push to promotion | p50 under 15 s |
267
| Direct-load promotion to live | p50 under 45 s; p95 under 2 min |
268
| Direct-load fleet transaction | p95 under 2 s |
269
| Relup package ready to fleet live | p50 under 90 s |
270
| Rolling promotion to live | p50 under 6 min; p95 under 15 min |
271
| Production image submission to published digest | p50 under 4 min |
272
| Per-node image-ready to healthy | p50 under 45 s |
273
274
Do not set an end-to-end rolling target until the receipts include the gate,
275
image build, operator wait, and per-node phases. The current data can measure
276
those components separately, but it cannot reconstruct every historical full
277
release reliably.
278
279
## Recommended runbook shape
280
281
Replace the current one-size-fits-all sequence with this operator flow:
282
283
1. Select an exact candidate SHA and run the local deployment-plan preflight.
284
2. Run `mix precommit` and the qualification required for the selected lane.
285
3. Push the candidate to the forge.
286
4. Promote the exact SHA with the scoped command.
287
5. Follow the lane chosen by the production classifier:
288
   - Direct load: wait for the transactional receipt and verify production.
289
   - Relup: install the verified package one node at a time, then verify and
290
     settle.
291
   - Rolling replacement: build or resolve the immutable image, pre-pull it,
292
     replace one node at a time, then verify and settle.
293
6. Confirm the public revision, three-node convergence, and durable receipt.
294
295
This flow keeps push, promotion, and deployment separate. It makes the common
296
BEAM-only path fast and preserves rolling replacement as the safe path for
297
structural change.

This page updates live while a promote is in flight · changelog