outpost: pull Polar's changes instead of holding a WebSocket
Summary
Outpost no longer holds a WebSocket (PLR-423), in both SDKs.
-
One pull per merchant Redis. The instance that takes a lock in the merchant's Redis pulls
/v1/outpost/changes. It applies each change, then moves the cursor stored in that Redis. A frozen instance only holds the lock until it expires, and a failed pull leaves the cursor where it was. -
Cold means absent from the shared Redis. One instance of the fleet fetches a cold customer; the others get the state through the existing Redis pub/sub.
-
When reads pull. Time counts from the last successful pull, in multiples of
next_poll_in(30 s by default):- Before 1×, the read returns the cached value and does not pull.
- Between 1× and 2×, the read returns the cached value at once and starts a pull in the background.
- Past 2×, the data is too old: the read waits for the pull before it returns.
- After a failed pull, reads neither wait nor pull until the backoff ends.
Long-running processes also pull on a timer.
metering.flush()waits for a pull in flight. -
Confirms are plain requests with their answer, so superseded replies (PLR-410) cannot happen.
-
ws,httpx-wsandwsprotoare gone.
Depends on the server stack (PLR-420 to 422) being deployed.
Local run against the server stack, with N instances on one Redis for 75 s:
| Instances | Reads | Read p99 | Pulls served by Polar | Cold fetches |
|---|---|---|---|---|
| 8 | 11,773 | 0.11 ms | 3 | 10 |
| 32 | 47,103 | 0.07 ms | 3 | 10 |
Checklist
- This PR addresses a single concern (one bug fix, one feature, one refactor)
- The diff is reasonably sized and easy to review
- New functionality is covered by tests
- Linting and type checking pass (
uv run task lint && uv run task lint_types) - No unrelated changes or drive-by fixes are included