outpost: pull Polar's changes instead of holding a WebSocket

Draft
MA
2h ago
+4853-3672

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-ws and wsproto are 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:

InstancesReadsRead p99Pulls served by PolarCold fetches
811,7730.11 ms310
3247,1030.07 ms310

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