|
| 1 | +--- |
| 2 | +"@objectstack/spec": major |
| 3 | +--- |
| 4 | + |
| 5 | +**Retry policy converges onto one declaration** (#4661 — the #4535 C8 dual-source cluster). |
| 6 | + |
| 7 | +`@objectstack/spec/automation` and `@objectstack/spec/system` both exported |
| 8 | +`RetryPolicySchema` / `RetryPolicy`, resolving to **different declarations** — so the |
| 9 | +shape you got depended only on which entry you imported (the #4411 trap). They were |
| 10 | +never two concepts: the `try_catch` node's `retry` region and `job.retryPolicy` both |
| 11 | +compute `delay = base * multiplier^(retry-1)`, and both executors implemented that |
| 12 | +identical formula. There is now one declaration, re-exported by both entries, carrying |
| 13 | +the union of what the two sides could express. |
| 14 | + |
| 15 | +## FROM → TO |
| 16 | + |
| 17 | +| | FROM `./automation` | FROM `./system` | TO (both entries) | |
| 18 | +|---|---|---|---| |
| 19 | +| base delay | `retryDelayMs`, min 0, default 1000 | `backoffMs`, positive, default 1000 | **`backoffMs`**, min 0, default 1000 | |
| 20 | +| `maxRetries` | 0–10, default **0** | ≥0 unbounded, default **3** | 0–**10**, default **0** | |
| 21 | +| `backoffMultiplier` | ≥**1**, default **1** | positive, default **2** | ≥**1**, default **1** | |
| 22 | +| `maxRetryDelayMs` | default 30000 | *(absent)* | default 30000 | |
| 23 | +| `jitter` | default false | *(absent)* | default false | |
| 24 | +| `RetryPolicy` type | `z.input` | `z.infer` | `z.input` (+ new `RetryPolicyParsed` for `z.infer`) | |
| 25 | + |
| 26 | +## What you must change |
| 27 | + |
| 28 | +**1. Rename `retryDelayMs` → `backoffMs`** in any `try_catch` node's `retry` block. |
| 29 | +The value (milliseconds before the first retry) is unchanged. The old spelling is |
| 30 | +**tombstoned**, not deleted — it rejects with the rename prescription instead of being |
| 31 | +silently swallowed, because neither owning schema is `.strict()`. Automated: |
| 32 | + |
| 33 | +``` |
| 34 | +os migrate meta --from 16 |
| 35 | +``` |
| 36 | + |
| 37 | +**2. Nothing for existing jobs — but read this if you author new ones.** `maxRetries` |
| 38 | +now defaults to **0** and `backoffMultiplier` to **1**, where `job.retryPolicy` |
| 39 | +previously defaulted to 3 and 2. Left alone that would silently stop deployed jobs from |
| 40 | +retrying, so the `retry-policy-converged` conversion **writes the pre-17 numbers |
| 41 | +explicitly into every existing `job.retryPolicy`** that omitted them: |
| 42 | + |
| 43 | +```jsonc |
| 44 | +// before // after `os migrate meta` |
| 45 | +{ "backoffMs": 5000 } { "backoffMs": 5000, "maxRetries": 3, "backoffMultiplier": 2 } |
| 46 | +``` |
| 47 | + |
| 48 | +Deployed stacks therefore keep their exact behaviour. What changes is what a **newly |
| 49 | +authored** omission means: declaring a retry block without `maxRetries` now means *no |
| 50 | +retry*. Retry is opt-in because a retry replays whatever the attempt already did — a job |
| 51 | +handler's writes and callouts, a `try` region's side effects — and an implicit replay is |
| 52 | +the failure mode hardest to catch in tests and most expensive in production. (The same |
| 53 | +reading is already recorded for flow-level retry in `flow-retry-max-retries-required`, |
| 54 | +#4247.) |
| 55 | + |
| 56 | +> This defaults change is the part **no gate can see**: the authorable-surface ratchet |
| 57 | +> compares key sets, and a default is not a key. It is called out here because a |
| 58 | +> changeset is the only channel that carries it. |
| 59 | +
|
| 60 | +**3. Two bounds now apply to jobs that did not have them** — `maxRetries` is capped at |
| 61 | +**10** and `backoffMultiplier` floored at **1**. Both fail loudly at parse time rather |
| 62 | +than being silently reinterpreted; neither has a lossless rewrite, so they are recorded |
| 63 | +as the `job-retry-policy-constraints-tightened` semantic migration note. A multiplier |
| 64 | +below 1 described a delay that *shrinks* on each attempt — retrying a failing dependency |
| 65 | +ever faster, the opposite of backoff. |
| 66 | + |
| 67 | +**4. `import type { RetryPolicy } from '@objectstack/spec/system'` is now the input |
| 68 | +shape** (every key optional) rather than the post-parse shape. Use the new |
| 69 | +`RetryPolicyParsed` where you need defaults applied. |
| 70 | + |
| 71 | +## What you gain |
| 72 | + |
| 73 | +`job.retryPolicy` accepts **`maxRetryDelayMs`** (ceiling on a single backoff delay) and |
| 74 | +**`jitter`** (randomize each delay into [50%, 100%]). Both are enforced by |
| 75 | +`runWithPolicy`, not merely declared — jitter is what stops a fleet of jobs that failed |
| 76 | +on one outage from retrying in lockstep. |
0 commit comments