toga-ai 1.0.695 → 1.0.697
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/knowledge/2.0/apps/api2/INDEX.md +1 -1
- package/knowledge/2.0/apps/api2/architecture.md +65 -0
- package/knowledge/2.0/apps/api2/workflows/codepipeline-codeconnections-deploy.md +87 -3
- package/knowledge/2.0/apps/worker2/features/elastic-beanstalk-health-monitor.md +75 -10
- package/package.json +1 -1
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
| [V2 request retry (Prong A) — deadlock AND retriable unique-key race, route-scoped in-process replay](features/v2-deadlock-retry.md) | api2's front controller ("**Prong A**") can **detect a retriable concurrency failure and replay the whole request in-process**, so a transient collision self-re | api2/Controller/Index.php, api2/Component/Api/V2/V2.php, _underscore/Database.php, _underscore/Query.php, _underscore/Model/Compass/AdvanceShippingNotice.php |
|
|
26
26
|
| [V2 REST query contract (params, where grammar, encoding, ACL behavior)](features/v2-rest-query-contract.md) | What an **HTTP client** has to get right to query the Toga v2 REST API: which query params are recognized, the exact `where` grammar, how the query string is (n | api2/Component/Api/V2/V2.php |
|
|
27
27
|
| [V2 reverse hasMany collections must be named in the fetch fields whitelist](features/v2-reverse-hasmany-fields-whitelist.md) | In the V2 JSON engine, a **reverse hasMany** relationship — the collection of child records that foreign-key back to a parent (e.g. | api2/Component/Api/V2/V2.php |
|
|
28
|
-
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php, api2/.platform/hooks/prebuild/git.sh, api2/.ebextensions/git.php, api2/.ebextensions/git.sandbox-dev.json |
|
|
28
|
+
| [AWS CodePipeline Deployment via CodeConnections (GitHub → Elastic Beanstalk)](workflows/codepipeline-codeconnections-deploy.md) | 2.0 apps (`api2`, `_underscore`) are deployed through **AWS CodePipeline**. | api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh, api2/ebs/register_instance_to_shared_application_load_balancer.php, api2/.platform/hooks/prebuild/git.sh, api2/.platform/hooks/postdeploy/015_install_composer.sh, api2/.ebextensions/git.php, api2/.ebextensions/git.sandbox-dev.json |
|
|
29
29
|
| [New Environment Configuration & Provisioning (api2)](workflows/environment-configuration-and-provisioning.md) | What it takes for a 2.0 API environment (e.g. | api2/Config/<environment>.ini, api2/composer.json, api2/composer.lock, api2/Controller/Index.php, dbchanges2/Core/2026-06-16a - DatabaseHosts for new QA QC stage demo environments.sql, dbchanges2/Logs/, _underscore/Route.php |
|
|
@@ -197,6 +197,12 @@ always exits 0). See
|
|
|
197
197
|
[the worker2 reference implementation](../worker2/features/alb-target-group-auto-registration.md) —
|
|
198
198
|
api2's copy is the **unhardened original** and must be brought up to it.
|
|
199
199
|
|
|
200
|
+
**Deploy policy (2026-08-28):** the three production envs (`api-production-1/2/3`, 2 instances each)
|
|
201
|
+
run **Rolling / `BatchSize` 1** (was `AllAtOnce`/100%), target-group `HealthyThresholdCount` 2 — so
|
|
202
|
+
a deploy keeps ≥1 healthy backend and no longer emits the ELB-502 blip. Each CodePipeline
|
|
203
|
+
auto-deploys on a push to `_production`. See
|
|
204
|
+
[the CodePipeline deploy workflow](workflows/codepipeline-codeconnections-deploy.md).
|
|
205
|
+
|
|
200
206
|
## CI — commit message policy
|
|
201
207
|
|
|
202
208
|
`.github/workflows/true-devteam-requirements.yml` enforces **`TRUE-{ticket}: {Subject}`** —
|
|
@@ -275,6 +281,48 @@ probe was starved of a PHP-FPM worker — do **not** chase client-traffic 5xx. *
|
|
|
275
281
|
liveness from PHP-FPM worker availability so a slow-request burst can no longer flap it — see
|
|
276
282
|
[health-check endpoint](features/health-check-endpoint.md).
|
|
277
283
|
|
|
284
|
+
**A fourth cause (deploy-triggered) — a burst of ELB `502`s when both instances restart at once.**
|
|
285
|
+
Confirmed on `api-production-1` (2026-08-28 23:51 UTC deploy: **43 × 502 in one minute**, the only
|
|
286
|
+
ELB 5xx in a 7-hour window). Under the old `AllAtOnce`/100% deploy policy, every deploy to a
|
|
287
|
+
2-instance env restarts Apache/PHP-FPM on **both** instances simultaneously, so in-flight requests
|
|
288
|
+
have their connections dropped mid-response → a burst of **ELB-generated HTTP 502**. EB flags "X%
|
|
289
|
+
of requests to the ELB are failing with HTTP 5xx" `Degraded` ~3–4 min later and recovers in ~5 min.
|
|
290
|
+
Distinguishing fingerprint vs. a real app failure: it is **502** (a dropped backend connection),
|
|
291
|
+
**not 503** (no healthy backend) or **504** (backend timeout); **`HTTPCode_Target_5XX_Count` (app
|
|
292
|
+
5xx) = 0**; **`HealthyHostCount` never drops below 2**; and there is **NOTHING in `Logs.Api`** —
|
|
293
|
+
api2 logs a request only *after* the response, and a 502 never completes one, so the request leaves
|
|
294
|
+
no log row. Diagnosis path: CloudWatch `HTTPCode_ELB_502/503/504_Count` vs
|
|
295
|
+
`HTTPCode_Target_5XX_Count`, plus EB `describe-events` (a version change) and CodePipeline
|
|
296
|
+
`list-pipeline-executions` — because a push to `_production` auto-deploys all three prod tiers, "we
|
|
297
|
+
didn't deploy" can be false (see
|
|
298
|
+
[the CodePipeline deploy workflow](workflows/codepipeline-codeconnections-deploy.md)).
|
|
299
|
+
**Rule:** an EB "% 5xx" `Degraded` with an **empty `Logs.Api` and 0 target-5xx** right after a
|
|
300
|
+
deploy is a deploy-restart 502 artifact, not an application bug — check the ELB-5xx breakdown and
|
|
301
|
+
the deploy/version timeline before hunting app bugs. **Fixed 2026-08-28:** all three prod envs moved
|
|
302
|
+
to **Rolling / batch-size 1** (≥1 healthy backend throughout a deploy), which eliminates the blip —
|
|
303
|
+
config + tradeoffs in the [deploy workflow](workflows/codepipeline-codeconnections-deploy.md).
|
|
304
|
+
|
|
305
|
+
**A fifth cause — a brief, self-healing "no-cause TargetGroup reduced health" flip on a
|
|
306
|
+
LOW-TRAFFIC, IDLE 2-instance env is EB Enhanced Health FLAPPINESS, not worker starvation.**
|
|
307
|
+
Distinct from the third cause (that one is the *busy* env, `api-production-1`, under Compass ASN
|
|
308
|
+
worker saturation). This is a single missed ALB `/health` check — a network/agent transient.
|
|
309
|
+
Fingerprint (verified `api-production-3`, eu-west-1, 2026-08-29 13:31 UTC `Severe`, static-`/health`
|
|
310
|
+
build deployed): self-heals in ~2–5 min; throughout, `HealthyHostCount` stays 2 (1-min minimum),
|
|
311
|
+
**0** target/ELB 5xx, sub-second latency, idle CPU, near-zero traffic; ALB target health already
|
|
312
|
+
reads "healthy" by the time you check. On an idle env there is no worker starvation, so **no
|
|
313
|
+
server-side app fix applies** — do not chase it. The mitigation is alert de-sensitization: the
|
|
314
|
+
worker2 EB health monitor now applies a **persistence-delay gate** (a non-5xx `Degraded`/`Severe`
|
|
315
|
+
must persist ≥ `nonFivexxPersistMinutes`, default 5, before it pages), which suppresses these flaps.
|
|
316
|
+
See [worker2 EB health monitor](../worker2/features/elastic-beanstalk-health-monitor.md).
|
|
317
|
+
|
|
318
|
+
**Triage aid — break an EB "% requests failing with HTTP 5xx" flag into ELB 502/503/504 to find
|
|
319
|
+
the cause:** **503** = no healthy backend (a deploy with no capacity); **504** = target timeout
|
|
320
|
+
(worker saturation); **502** = connection dropped mid-response (an `AllAtOnce` deploy killing
|
|
321
|
+
in-flight requests, or a PHP worker crash). Because **ELB-generated 5xx (any of these) never reach
|
|
322
|
+
PHP**, they leave **no row in `Logs.Api`** (api2 logs only *after* a completed response) — so an EB
|
|
323
|
+
"% 5xx" flag with an **empty `Logs.Api`** means an **ELB-level** 5xx, not an app 5xx. Discriminate
|
|
324
|
+
with CloudWatch `HTTPCode_ELB_502/503/504_Count` vs `HTTPCode_Target_5XX_Count`.
|
|
325
|
+
|
|
278
326
|
## Known issues / accepted risks
|
|
279
327
|
|
|
280
328
|
Open items a maintainer should know before changing this tier. None are "bugs to fix right now" —
|
|
@@ -352,6 +400,23 @@ they are the known sharp edges. Do not re-discover these from scratch.
|
|
|
352
400
|
single-caller branches in `V2.php` as unverified until exercised directly.
|
|
353
401
|
|
|
354
402
|
## Change history
|
|
403
|
+
- 2026-08-29 — Recorded a **fifth EB-`Degraded` cause**: a brief self-healing "no-cause reduced
|
|
404
|
+
health" flip on a low-traffic *idle* 2-instance env (e.g. `api-production-3`, eu-west-1) is EB
|
|
405
|
+
Enhanced Health flappiness (a single missed ALB `/health` check), distinct from the busy-env
|
|
406
|
+
PHP-`/health`-starvation cause — no server-side fix applies; mitigated by the worker2 monitor's
|
|
407
|
+
new non-5xx persistence-delay gate. Added an ELB **502/503/504 discrimination** triage aid
|
|
408
|
+
(503=no healthy backend, 504=target timeout/worker saturation, 502=dropped connection from an
|
|
409
|
+
AllAtOnce deploy or PHP crash); ELB-generated 5xx never reach PHP so they leave no `Logs.Api`
|
|
410
|
+
row — an EB "% 5xx" flag with an empty `Logs.Api` is ELB-level, not app 5xx. (jcardinal)
|
|
411
|
+
- 2026-08-28 — Recorded a **fourth EB-`Degraded` cause — the deploy-restart ELB 502**: under the old
|
|
412
|
+
`AllAtOnce`/100% policy a deploy restarts both instances of a 2-instance env at once, dropping
|
|
413
|
+
in-flight connections → an ELB-502 burst (43 in one minute on `api-production-1`, 2026-08-28) that
|
|
414
|
+
EB flags as "X% 5xx" Degraded ~3–4 min later. Fingerprint vs. an app bug: **502** (not 503/504),
|
|
415
|
+
**`HTTPCode_Target_5XX_Count`=0**, `HealthyHostCount` never < 2, and **nothing in `Logs.Api`** (a
|
|
416
|
+
502 never completes a PHP response, which api2 logs only after). Diagnose via the ELB-5xx breakdown
|
|
417
|
+
+ `describe-events`/`list-pipeline-executions` — a push to `_production` auto-deploys all three prod
|
|
418
|
+
tiers, so "we didn't deploy" can be false. Fixed by moving to Rolling/batch-1 (deploy config +
|
|
419
|
+
policy tradeoffs captured in the CodePipeline deploy workflow). (jcardinal)
|
|
355
420
|
- 2026-08-28 — Recorded the third EB-`Degraded` cause: a no-stated-cause "reduced health" flip on `api-production-1` is the **PHP-served `/health` probe starved of a PHP-FPM worker** by off-peak bursts of slow failing Compass ASN POSTs (17–63 s, ~100% 500) — invisible in ALB `RequestCount`/`5xx` and never dropping `HealthyHostCount` (flap < 75 s deregistration). Correlated a 06:00–06:08 CST ASN burst to the 11:05 UTC Severe flip. Noted the fix (static-file `/health` decouples liveness from PHP-FPM) and updated the "Don't break `/health`" rule to the static-file form. (jcardinal)
|
|
356
421
|
- 2026-08-26 — Documented and fixed a `/v2/auth/public` OOM: a present-but-null client `transactionId` (`{"transactionId": null}`) flowed into the request-logger's `_Model::load()` dup-check; because `Logs.Api.transactionId` is `FIELD_CHAR`, a null value collapses the WHERE and full-table-scans prod `Logs.Api` → memory exhaustion (the `_underscore` collapsed-WHERE mechanism). V2 only auto-uuids an *absent* transactionId (~L543), so a present null fell through (~L553 read → null). Fix (commit `36ef31f`): coerce null/empty transactionId → uuid in the auth block, and skip `load()` in the final logger dup-check (~L2277) when null/empty. Added the "don't blame the latest deploy — check first-occurrence" triage note. (jcardinal)
|
|
357
422
|
- 2026-08-25 — **Resolved the auto-generated `Api.transactionId` 1062 collision** (was the 2026-07-28
|
|
@@ -6,12 +6,13 @@ project: API
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: workflow
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-28
|
|
10
10
|
owners: ["jcardinal", "mhammontree", "bala", "apeterson"]
|
|
11
11
|
files:
|
|
12
12
|
- api2/.platform/hooks/postdeploy/060_register_instance_to_shared_application_load_balancer.sh
|
|
13
13
|
- api2/ebs/register_instance_to_shared_application_load_balancer.php
|
|
14
14
|
- api2/.platform/hooks/prebuild/git.sh
|
|
15
|
+
- api2/.platform/hooks/postdeploy/015_install_composer.sh
|
|
15
16
|
- api2/.ebextensions/git.php
|
|
16
17
|
- api2/.ebextensions/git.sandbox-dev.json
|
|
17
18
|
related:
|
|
@@ -62,8 +63,11 @@ are actually changing before you merge anything:
|
|
|
62
63
|
`_<environment>`). Consequence: **a framework-only change requires REDEPLOYING THE CONSUMING
|
|
63
64
|
TIER even when `api2` itself has zero changes** — an instance **restart will not pick it up**,
|
|
64
65
|
because the clone happens in prebuild.
|
|
65
|
-
- **`api2` has no deploy workflow
|
|
66
|
-
linter
|
|
66
|
+
- **`api2` has no GitHub *Actions* deploy workflow** — its only GitHub Action is the
|
|
67
|
+
`TRUE-#####:` commit-message linter. **The production tiers, however, auto-deploy via AWS
|
|
68
|
+
CodePipeline on every push to `_production`** (a different system from GitHub Actions — see
|
|
69
|
+
*Production deploy: auto-trigger + rolling policy* below). Non-pipeline-triggered tiers are
|
|
70
|
+
still deployed by hand; don't wait for a GitHub Action to ship a framework change.
|
|
67
71
|
|
|
68
72
|
### ⚠ The `_underscore` SOURCE ACTION has its own branch trigger — and a green pipeline hides a wrong one
|
|
69
73
|
|
|
@@ -105,6 +109,68 @@ beta env actually received and returned when Sentry isn't wired up. (The Rate AI
|
|
|
105
109
|
scans this same `Logs_Rate.Api` for failed outbound calls — see
|
|
106
110
|
`clients/rate/features/aig-contract-creation.md`.)
|
|
107
111
|
|
|
112
|
+
## Production deploy: auto-trigger + rolling policy (2026-08-28)
|
|
113
|
+
|
|
114
|
+
### Production auto-deploys on every push to `_production` — a merge IS a deployment
|
|
115
|
+
The `api-production-1` (us-east-1), `api-production-2` (us-west-2), and `api-production-3`
|
|
116
|
+
(eu-west-1) CodePipelines each **auto-deploy on every push to branch `_production`** — a
|
|
117
|
+
**WebhookV2** trigger on the CodeConnections GitHub connection, watching source repos
|
|
118
|
+
`agilantsolutions/_underscore` and `agilantsolutions/api2`. There is **no separate manual deploy
|
|
119
|
+
step for production**: pushing or merging to `_production` **is** a production deployment to all
|
|
120
|
+
three regions. (This is why deploying a fix can itself page a moment later — see the
|
|
121
|
+
deploy-restart 502 below.)
|
|
122
|
+
|
|
123
|
+
**Triage consequence:** "we didn't deploy" can be false — any push to `_production` is a deploy.
|
|
124
|
+
Correlate an incident against the actual pipeline runs, not just remembered manual actions:
|
|
125
|
+
```bash
|
|
126
|
+
aws codepipeline list-pipeline-executions --pipeline-name api-production-1 --region us-east-1
|
|
127
|
+
aws elasticbeanstalk describe-events --environment-name api-production-1 --region us-east-1 # version-change events
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### ⚠ An EB `Degraded` right after a deploy = the deploy-restart 502
|
|
131
|
+
Under the old `AllAtOnce`/100% policy every deploy restarted Apache/PHP-FPM on **both**
|
|
132
|
+
instances of a 2-instance env simultaneously, dropping in-flight connections → a burst of
|
|
133
|
+
ELB-generated **HTTP 502** that EB flags as an "X% 5xx" `Degraded` ~3–4 min later (recovers in
|
|
134
|
+
~5 min). This is a **deploy artifact, not an application failure** — the full diagnostic
|
|
135
|
+
fingerprint (502 not 503/504, app `HTTPCode_Target_5XX_Count`=0, `HealthyHostCount` never < 2,
|
|
136
|
+
**nothing in `Logs.Api`** because a 502 never completes a PHP response) lives in
|
|
137
|
+
[api2 architecture — EB "Degraded" health](../architecture.md). The rolling policy below
|
|
138
|
+
eliminates it.
|
|
139
|
+
|
|
140
|
+
### Deploy policy: Rolling, batch size 1 (changed 2026-08-28)
|
|
141
|
+
All three production envs (app **"API"**, 2 instances each) were switched from
|
|
142
|
+
`AllAtOnce`/`Percentage`/`100` to **`DeploymentPolicy=Rolling`, `BatchSizeType=Fixed`,
|
|
143
|
+
`BatchSize=1`**, and the target group **`HealthyThresholdCount` dropped 3 → 2** (health path
|
|
144
|
+
`/health`, `HealthCheckInterval` 15 s, `UnhealthyThresholdCount` 5 unchanged). Rolling batch-1 on
|
|
145
|
+
a 2-instance env deploys in **2 batches with ≥1 healthy backend throughout**, so a deploy no
|
|
146
|
+
longer drops in-flight requests → **no more deploy-restart 502 blip**; the 3→2 threshold shaves
|
|
147
|
+
one 15 s interval (~45 s → 30 s) of per-batch health settle.
|
|
148
|
+
|
|
149
|
+
- Applied via a **config-only `update-environment`** (AWS CLI, per env) — no instance
|
|
150
|
+
replacement, no downtime; **takes effect on the NEXT deploy**.
|
|
151
|
+
- Namespaces: `aws:elasticbeanstalk:command` (`DeploymentPolicy` / `BatchSizeType` /
|
|
152
|
+
`BatchSize`); `aws:elasticbeanstalk:environment:process:default` (`HealthyThresholdCount`).
|
|
153
|
+
- Trade-off: Rolling is slower end-to-end than the old ~31 s `AllAtOnce`, and capacity briefly
|
|
154
|
+
drops to 1/2 instances per batch (fine at current traffic).
|
|
155
|
+
- **Confirmed working in production (2026-08-29).** The 2026-08-29 00:49 UTC deploy ran as
|
|
156
|
+
Rolling batches ("Batch 1 / Batch 2, one instance at a time, register and wait for healthy")
|
|
157
|
+
across all three API prod envs with **no post-deploy 5xx flip** — the `AllAtOnce`
|
|
158
|
+
deploy-restart 502 blip is eliminated in practice, not just in config.
|
|
159
|
+
|
|
160
|
+
### Deploy-policy guidance (don't reach for immutable expecting speed)
|
|
161
|
+
- **Immutable does NOT reduce deploy time.** It is the **slowest** policy — it boots a fresh
|
|
162
|
+
instance batch, health-checks it, then swaps, and temporarily **doubles** the instance count.
|
|
163
|
+
It does fully eliminate deploy 5xx and gives instant rollback, but it is not "faster."
|
|
164
|
+
- **`AllAtOnce` is fast (~31 s) precisely BECAUSE both instances restart at once** — which is
|
|
165
|
+
what causes the 502s. You cannot have "as fast as AllAtOnce" **and** "zero 502s."
|
|
166
|
+
- **Rolling batch-1 is the sweet spot** for a 2-instance env.
|
|
167
|
+
- **`RollingWithAdditionalBatch`** is the option only if zero capacity loss during a deploy is
|
|
168
|
+
ever required (it adds a temporary instance; slower and costlier).
|
|
169
|
+
- The real deploy-**time** levers are independent of the policy: the health-check settle
|
|
170
|
+
(`HealthCheckInterval` × `HealthyThresholdCount`) and per-instance postdeploy work — api2 runs
|
|
171
|
+
`composer install` on **every** deploy via
|
|
172
|
+
`.platform/hooks/postdeploy/015_install_composer.sh` (a caching / pre-bake candidate).
|
|
173
|
+
|
|
108
174
|
## Steps
|
|
109
175
|
1. A push to the watched branch (e.g. `_stage`) triggers the pipeline.
|
|
110
176
|
2. The Source stage uses `CodeStarSourceConnection` actions, each referencing:
|
|
@@ -233,6 +299,24 @@ aws codeconnections get-connection --connection-arn "<CONN_ARN>" --region "$REGI
|
|
|
233
299
|
```
|
|
234
300
|
|
|
235
301
|
## Change history
|
|
302
|
+
- 2026-08-29 — Confirmed the Rolling/batch-1 policy works in production: the 2026-08-29
|
|
303
|
+
00:49 UTC deploy ran as Rolling batches (one instance at a time, register + wait for healthy)
|
|
304
|
+
on all three API prod envs with **no post-deploy 5xx flip** — the AllAtOnce deploy-restart
|
|
305
|
+
502 blip is eliminated. (jcardinal)
|
|
306
|
+
- 2026-08-28 — Recorded the **production deploy trigger + policy change**: the `api-production-1/2/3`
|
|
307
|
+
CodePipelines **auto-deploy on every push to `_production`** (WebhookV2 on the CodeConnections
|
|
308
|
+
connection, repos `_underscore` + `api2`), so a merge to `_production` **is** a three-region prod
|
|
309
|
+
deploy — correlate incidents against `list-pipeline-executions`, not remembered manual actions.
|
|
310
|
+
Switched all three 2-instance envs from `AllAtOnce`/100% to **Rolling / Fixed / BatchSize 1** and
|
|
311
|
+
dropped the target-group **`HealthyThresholdCount` 3 → 2** (config-only `update-environment`,
|
|
312
|
+
effective next deploy) to eliminate the **deploy-restart 502 blip** (both instances restarting at
|
|
313
|
+
once dropped in-flight requests → ELB 502 → EB "X% 5xx" Degraded). Added deploy-policy guidance:
|
|
314
|
+
immutable is the **slowest** policy (not faster) and doubles instances; `AllAtOnce` is fast only
|
|
315
|
+
because it restarts both at once (the 502 cause); Rolling batch-1 is the sweet spot;
|
|
316
|
+
`RollingWithAdditionalBatch` only if zero capacity loss is needed; the real deploy-time levers are
|
|
317
|
+
the health-check settle and the per-deploy `015_install_composer.sh` `composer install`. Corrected
|
|
318
|
+
the "EB deploys are manual" note — that is only true of the absent GitHub *Actions* workflow; prod
|
|
319
|
+
deploys via CodePipeline are automatic. (jcardinal)
|
|
236
320
|
- 2026-08-25 — Documented that the Source stage's **`_underscore` action has its own AWS-side branch
|
|
237
321
|
trigger**, independent of `git.<env>.json` and invisible in the repo — a second way to silently run
|
|
238
322
|
the wrong framework branch. `ApiSandboxClient` ran `_production` from a mis-set source action while
|
|
@@ -6,7 +6,7 @@ project: Worker
|
|
|
6
6
|
client: shared
|
|
7
7
|
type: feature
|
|
8
8
|
status: active
|
|
9
|
-
updated: 2026-08-
|
|
9
|
+
updated: 2026-08-29
|
|
10
10
|
owners: [jcardinal]
|
|
11
11
|
files:
|
|
12
12
|
- worker2/Worker/Infrastructure/CloudWatch.php
|
|
@@ -28,10 +28,13 @@ OneUptime string-matches — OneUptime cannot compare numbers on a pushed body.
|
|
|
28
28
|
|
|
29
29
|
The load-bearing design decision this doc records is the **paging (alarm) criteria**, and
|
|
30
30
|
the key **2026-08-25** change: the 5xx page is now driven by a **CloudWatch windowed
|
|
31
|
-
request-rate gate**, fully **decoupled from EB's own `HealthStatus`**.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
request-rate gate**, fully **decoupled from EB's own `HealthStatus`**. Whether a *5xx*
|
|
32
|
+
problem pages is decided by the true per-environment 5xx **rate over a trailing window**
|
|
33
|
+
(default 5 min), not by EB's ~10-second `ApplicationMetrics` snapshot. As of **2026-08-29**,
|
|
34
|
+
a non-5xx `Degraded`/`Severe` no longer pages immediately either — it must **persist** past a
|
|
35
|
+
threshold (`nonFivexxPersistMinutes`, default 5) before paging, which suppresses the brief
|
|
36
|
+
self-healing "reduced health" flips that EB Enhanced Health throws on low-traffic 2-instance
|
|
37
|
+
envs (see the persistence gate below).
|
|
35
38
|
|
|
36
39
|
**Why the change:** the old gate read EB `describeEnvironmentHealth`'s `ApplicationMetrics`,
|
|
37
40
|
a ~10-**second** window. At low traffic a single 500 in a near-empty bucket reads as 100%
|
|
@@ -52,6 +55,10 @@ itself runs non-fatal. CloudWatch read failures are caught **internally** and re
|
|
|
52
55
|
`_Worker::runTask('Infrastructure/CloudWatch/ElasticBeanstalkHealth', {awsAccountId,
|
|
53
56
|
environments, oneuptimeUrl, …thresholds})`.
|
|
54
57
|
- New private `fetchWindowedRequestCounts()` reads the CloudWatch metrics (below).
|
|
58
|
+
- New private `fetchNonOkDepartureAgeSeconds(ElasticBeanstalkClient $eb, string
|
|
59
|
+
$environmentName, int $lookbackSeconds): ?int` returns how long the current non-Ok episode
|
|
60
|
+
has persisted, from EB `describeEvents` (below). **Never throws** — catches `AwsException`
|
|
61
|
+
and returns `null`.
|
|
55
62
|
- AWS access is obtained through `_Component_Aws_Workloads::assumeCredentials()` (STS-assume
|
|
56
63
|
a read-only role per account, one assume reused across regions) — see
|
|
57
64
|
[Cross-account AWS access](./cross-account-aws-access.md), which also documents the
|
|
@@ -64,11 +71,14 @@ itself runs non-fatal. CloudWatch read failures are caught **internally** and re
|
|
|
64
71
|
|
|
65
72
|
Two independent paths now feed `alarm=HIGH`:
|
|
66
73
|
|
|
67
|
-
1. **EB-health path (non-5xx)
|
|
68
|
-
attributable solely to 5xx (latency, instances
|
|
69
|
-
no stated cause)
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
1. **EB-health path (non-5xx) — now gated on PERSISTENCE (2026-08-29).** EB `HealthStatus`
|
|
75
|
+
`Degraded` or `Severe` that is **not** attributable solely to 5xx (latency, instances
|
|
76
|
+
down, a failed deploy, a mixed cause, or no stated cause) pages **only once the current
|
|
77
|
+
non-Ok episode has persisted ≥ `nonFivexxPersistMinutes`** (new optional param, default 5)
|
|
78
|
+
— it no longer pages on the first snapshot. A sub-threshold blip is **reported but does not
|
|
79
|
+
page**. `Suspended` never pages. `causesAttributeSolelyTo5xx()` is what tells a solely-5xx
|
|
80
|
+
event apart from everything else. See the persistence-gate section below for how episode
|
|
81
|
+
age is measured and the fail-safe.
|
|
72
82
|
2. **CloudWatch windowed 5xx-rate path.** For a solely-5xx `Degraded`/`Severe`, the page is
|
|
73
83
|
decided **not** by EB but by the true windowed rate from CloudWatch. Given the window's
|
|
74
84
|
`requests` (ApplicationRequestsTotal) and `fivexx` (ApplicationRequests5xx), page when:
|
|
@@ -91,6 +101,7 @@ The four thresholds are **optional cron parameters** spread as **named arguments
|
|
|
91
101
|
| `minRequests` | `100` | Minimum windowed request count before the **share** gate applies |
|
|
92
102
|
| `min5xxPercent` | `20.0` | 5xx share (percent) at/above which a solely-5xx env pages, once `minRequests` is met |
|
|
93
103
|
| `min5xxAbsolute` | `20` | Absolute windowed 5xx count at/above which a solely-5xx env pages regardless of share |
|
|
104
|
+
| `nonFivexxPersistMinutes` | `5` | Minutes a **non-5xx** `Degraded`/`Severe` episode must persist before it pages (added 2026-08-29; a trailing optional param — existing `CronJobs.parameters` without the key still run) |
|
|
94
105
|
|
|
95
106
|
Removed this iteration: `shouldPageForHealth()` and the old
|
|
96
107
|
`HTTP_5XX_ALARM_RATIO` / `HTTP_5XX_ALARM_ABSOLUTE_COUNT` constants (superseded by the
|
|
@@ -123,6 +134,43 @@ A CloudWatch read *failure* (as opposed to empty data) sets **`probe=DEGRADED`**
|
|
|
123
134
|
gap), distinct from a measured-bad `alarm=HIGH` — same alarm-vs-probe split as the
|
|
124
135
|
multi-client monitors in [OneUptime push-metric monitors](./oneuptime-worker2-monitoring.md).
|
|
125
136
|
|
|
137
|
+
## Persistence gate on the non-5xx page (2026-08-29)
|
|
138
|
+
|
|
139
|
+
A non-5xx `Degraded`/`Severe` now pages only after the **current non-Ok episode has persisted
|
|
140
|
+
≥ `nonFivexxPersistMinutes`** (default 5). This exists because the brief "no-cause TargetGroup
|
|
141
|
+
reduced health" flips on low-traffic 2-instance envs are **EB Enhanced Health flappiness** —
|
|
142
|
+
a single missed ALB `/health` check (a network/agent transient), not a real app problem — and
|
|
143
|
+
they self-heal in ~2–5 min. A persistence delay suppresses those while a genuinely sustained
|
|
144
|
+
non-5xx degradation still pages, delayed only by the threshold. (The windowed 5xx-flood gate
|
|
145
|
+
is **unchanged** — it has its own, independent decision.)
|
|
146
|
+
|
|
147
|
+
**Fingerprint of the idle-env flap** (verified `api-production-3`, eu-west-1, 2026-08-29
|
|
148
|
+
13:31 UTC `Severe`, static-`/health` build deployed): self-heals in ~2–5 min; throughout,
|
|
149
|
+
`HealthyHostCount` stays 2 (1-min minimum), **0** target/ELB 5xx, sub-second latency, idle
|
|
150
|
+
CPU, near-zero traffic; ALB target health already reads "healthy" by the time you look. On an
|
|
151
|
+
idle env there is **no worker starvation**, so there is nothing to fix server-side — the
|
|
152
|
+
mitigation is this monitor persistence-delay gate, not more app code. **This is DISTINCT from
|
|
153
|
+
the PHP-`/health`-starvation cause** (that one is the *busy* env, `api-production-1`, under
|
|
154
|
+
ASN worker saturation, fixed by the static-`/health` file) — see
|
|
155
|
+
[api2 architecture — EB "Degraded" health](../../api2/architecture.md).
|
|
156
|
+
|
|
157
|
+
**Measuring episode age — `fetchNonOkDepartureAgeSeconds()`** reads EB `describeEvents`:
|
|
158
|
+
|
|
159
|
+
- The most recent **"transitioned from Ok to …"** event marks the current episode's start.
|
|
160
|
+
Once non-Ok, later transitions stay between non-Ok states until a to-Ok event; because
|
|
161
|
+
`describeEvents` returns **newest-first**, the first "from Ok to …" match wins.
|
|
162
|
+
- The query is **TIME-bounded**: `StartTime = now − (persistWindow +
|
|
163
|
+
HEALTH_EVENTS_LOOKBACK_BUFFER_SECONDS = 600)`, with a `MaxRecords = 100` cap — so a chatty
|
|
164
|
+
env's deploy/config events cannot push the departure event out of range.
|
|
165
|
+
- **Fail-safe:** an episode **older than the window** returns `null` → the caller **pages**.
|
|
166
|
+
That is correct: if the departure is beyond the lookback, the episode has already persisted
|
|
167
|
+
past the threshold, so paging is the safe outcome. A read failure also returns `null`
|
|
168
|
+
(the method catches `AwsException` and never throws) → same fail-safe.
|
|
169
|
+
|
|
170
|
+
**Auditable suppression:** a sub-threshold blip is reported, not paged, and the suppression is
|
|
171
|
+
made visible — `pendingPersistSeconds` is added to the OneUptime payload and a `pending=Ns`
|
|
172
|
+
annotation to the WorkerJobs summary — so a suppressed episode is never silent.
|
|
173
|
+
|
|
126
174
|
## Reading the windowed 5xx rate from CloudWatch (durable AWS reference)
|
|
127
175
|
|
|
128
176
|
- **EB application-request metrics are PER-INSTANCE ONLY.** `ApplicationRequestsTotal`,
|
|
@@ -214,6 +262,8 @@ the `probe` token; this is a OneUptime config change the developer applies.
|
|
|
214
262
|
`min5xxPercent`; absolute at `min5xxAbsolute-1` vs `min5xxAbsolute`; `requests` just below
|
|
215
263
|
`minRequests`; empty/zero window; mixed 5xx + non-5xx cause) — deferred pending a harness.
|
|
216
264
|
- **Deploy fails without degrading health** — still not caught (accepted residual).
|
|
265
|
+
- **`ElasticBeanstalkHealth()` is long** and grew with the persistence gate — php-reviewer
|
|
266
|
+
(0 critical) flagged it as a decomposition candidate; deferred pending the missing harness.
|
|
217
267
|
|
|
218
268
|
## Gotchas / known issues
|
|
219
269
|
|
|
@@ -235,6 +285,21 @@ the `probe` token; this is a OneUptime config change the developer applies.
|
|
|
235
285
|
record its value in a doc.
|
|
236
286
|
|
|
237
287
|
## Change history
|
|
288
|
+
- 2026-08-29 — Added a **persistence gate on the non-5xx page**: a non-5xx `Degraded`/`Severe`
|
|
289
|
+
no longer pages on the first snapshot — it must persist ≥ `nonFivexxPersistMinutes` (new
|
|
290
|
+
optional trailing param, default 5; existing `CronJobs.parameters` without the key still
|
|
291
|
+
run) before paging. Episode age comes from EB `describeEvents` via new never-throwing helper
|
|
292
|
+
`fetchNonOkDepartureAgeSeconds()` (most recent "from Ok to …" marks episode start; events
|
|
293
|
+
are newest-first so the first match wins; TIME-bounded `StartTime = now − (persistWindow +
|
|
294
|
+
HEALTH_EVENTS_LOOKBACK_BUFFER_SECONDS=600)`, `MaxRecords=100`, so an episode older than the
|
|
295
|
+
window returns `null` → the caller pages — correct fail-safe). Sub-threshold blips are
|
|
296
|
+
reported not paged, with `pendingPersistSeconds` (OneUptime payload) + `pending=Ns`
|
|
297
|
+
(WorkerJobs summary) so suppressions stay auditable. The windowed 5xx gate is unchanged.
|
|
298
|
+
Rationale: the brief self-healing "no-cause reduced health" flips on low-traffic 2-instance
|
|
299
|
+
envs are EB Enhanced Health flappiness (a single missed ALB `/health` check), not app bugs
|
|
300
|
+
— distinct from the busy-env PHP-`/health`-starvation cause. worker2-only; the param is
|
|
301
|
+
optional-with-default so no `_underscore`/dbchanges2 change. Not yet committed/deployed.
|
|
302
|
+
(jcardinal)
|
|
238
303
|
- 2026-08-25 — Replaced the EB `ApplicationMetrics` 5xx gate (a ~10s window that false-paged
|
|
239
304
|
"100% failing" on a lone 500 in a near-empty bucket — real worker-production incident: 16
|
|
240
305
|
500s / 516 req = 3.1% over 17 min) with a **CloudWatch windowed request-rate gate**. Reads
|
package/package.json
CHANGED