feat(live-runner): session-owned payment lifecycle for streamed sessions#31
feat(live-runner): session-owned payment lifecycle for streamed sessions#31rickstaa wants to merge 3 commits into
Conversation
reserve_session now returns the LivePaymentSession built during the reserve payment challenge (previously discarded). Add run_session_payments(session), a timer-driven loop that calls LivePaymentSession.send_payment on an interval to keep a long-lived session funded — the orchestrator meters open sessions by wall-clock time and releases them when the balance runs dry. No-op offchain. Reusable across any held-open transport (trickle today, websockets next). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a session-owned payment lifecycle to LiveRunnerSession so streamed (trickle/websocket) sessions stay funded without the caller hand-managing a background task. Mirrors the lv2v Lv2vJob.start_payment_sender/close shape, but uses the transport-agnostic interval driver (run_session_payments) since the general live-runner path has no single output stream to meter and must also cover websocket. - LiveRunnerSession (still frozen) gains start_payments(), aclose(), and async context manager support; _payment_task stored via object.__setattr__. start_payments is idempotent, a no-op offchain, and warns instead of raising when called without a running loop. - run_session_payments now pays immediately before the first sleep (a cold start can leave a long gap after the reservation payment) and logs+retries per-cycle failures instead of dying. - reserve_session threads payment_interval through to the session. Callers can now do `async with await reserve_session(...) as session:` and get automatic start/stop, or use start_payments()/aclose() manually. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…o server tick Refinements from reviewing the go-livepeer orchestrator design (ai_http.go ReserveLiveRunnerSession): after the reservation 402, the orchestrator holds the session as a prepaid balance debited by a server-side ticker (-livePaymentInterval, 5s default) and silently releases it when underfunded. The client just keeps crediting out-of-band, which is what run_session_payments already does. Two corrections: - Treat HTTP 482 / SkipPaymentCycle as a healthy "balance current" gate (debug, keep looping) instead of logging it as a payment failure. The orchestrator uses it to prevent overpayment; only genuine errors warn. - Document that payment_interval must stay at or below the orchestrator's livePaymentInterval (5s), which is why the 3s default carries margin. Add a test asserting a skip cycle does not kill the loop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for this @rickstaa . I spent some time reviewing this, comparing to how Scope does it and filling in some gaps (in the runner implementation, not your code). From Claude: 1. Session liveness in the payment loopThe loop currently has no notion of whether the session it's funding still exists, and only stops if See livepeer/go-livepeer@e545b98 for a workaround on the go-livepeer-side: it adds a payment endpoint scoped to the session that returns a 404 if the session doesn't exist. The loop should point to that endpoint and, on a 404, stop sending payments (and surface it as "session released"). To more easily construct the payment URL on the client, a base control URL was added in livepeer/go-livepeer@81f24cf ... take the control_url with a 2. send_payment doesn't disable cert verification like the rest of the SDKLivePaymentSession.send_payment POSTs via a default aiohttp.ClientSession (normal TLS verification), while every async JSON helper forces TCPConnector(ssl=False) and the sync lv2v sender uses an unverified context. For consistency it might be best to match the existing behavior. The SDK already has the right primitive: _post_empty in live_runner.py does exactly this (POST empty body, raise on >= 400) with TCPConnector(ssl=False). Suggest hoisting _post_empty into http.py (next to post_json, and to avoid the live_runner ← remote_signer import cycle) and routing the payment POST through it. This also gives a single place to distinguish the 404/403 status codes needed. We can have the SDK verify TLS by default if TOFU is disabled (which it should be for the live runner) but that's a larger change outside the scope of this PR. 3. Symmetrical stop_payments() (optional)Soft suggestion: it might be nice to have a small stop_payments() that cancels only the payments loop without stopping the session, as a symmetrical complement to start_payment. This might be handy for tests and any "drain / hand off funding" flow. Session tear-down via close already stops payments so this is purely optional. |
Draft for review and comparison against Josh's
ja/live-runnerbranch.What this adds
Streamed live-runner sessions (trickle / websocket) must keep paying for as long as the session is open. After the reservation 402, go-livepeer holds the session as a prepaid balance:
ReserveLiveRunnerSession(server/ai_http.go) spawns a server-side ticker (-livePaymentInterval, 5s default) that debits the balance and silently releases the session once it runs dry. There is no further 402; the client must keep crediting out of band viaPOST /payment. Unlikecall_runner(request/response, where the orchestrator pulls payment via a 402 inline), a held-open transport has no request to attach payment to, so the client pushes on a cadence below the server tick.This makes
LiveRunnerSessionown that lifecycle so callers do not hand-manage a background task.Commits
3b60852surface payment session and addrun_session_payments(the interval payment loop).37ee769make the session own start/stop of that loop.81ee7behandle the 482 skip-payment gate and anchor the interval to the server tick.Changes
LiveRunnerSession(still@dataclass(frozen=True)) gainsstart_payments(),aclose(), and async context manager support._payment_taskis stored viaobject.__setattr__, mirroringLv2vJob.start_payment_sender/close.start_paymentsis idempotent, a no-op offchain, and warns instead of raising when called without a running loop.run_session_paymentspays immediately before the first sleep (a cold start can leave a long gap after the reservation payment), treats HTTP 482 /SkipPaymentCycleas a healthy "balance current" gate (debug, keep looping) rather than a failure, and logs and retries other per-cycle failures instead of dying.reserve_sessionthreadspayment_intervalthrough to the session (default 3s, margin under the 5s server tick).Design notes vs lv2v and scope
LivePaymentProcessoronlivePaymentInterval), so interval push aligns more directly with the server than per-segment does.send_paymentPOSTs to{orch}/payment), so one interval loop funds any held-open transport.Usage
Tests
tests/test_live_runner_payments.py(9 cases): offchain no-op, immediate first payment, survives a transient payment error, treats a skip cycle as paid-up, idempotency, no-loop skip,aclose, async context manager. Full suite: 18 passed, ruff clean.Scope and altitude (intentionally left for Josh)
This PR is faithful to the payment-loop mechanics in scope (
LivepeerClient._payment_loop) and lv2v (Lv2vJob), and to the go-livepeer server model. It is intentionally scoped to the payment lifecycle and leaves the larger object-shape decisions open:LivepeerClient.connect()owns three connection-lifetime loops together:_payment_loop,_events_loop,_ping_loop. Here only payments live on the (thin)LiveRunnerSession. If the SDK should grow a scope-styleLiveRunnerClientthat owns payments, events, and heartbeat over one connection lifecycle, these methods move into it unchanged. Open question: is payment lifecycle onLiveRunnerSessionthe intended surface, or a stepping stone to a unified client?_events_loop). Not handled here. A client true to scope would watch events and fail fast instead of discovering the drop via dead media.livePaymentInterval. The reservation challenge does not surface that value (onlypayment_params/orchestrator/manifest_id), so the client guesses. SurfacinglivePaymentIntervalin the challenge would let the client self-tune, which is an orchestrator-side change.async withsugar is an addition beyond scope and lv2v (both use explicit start/close); kept becausereserve_sessionis async-native. Drop it if you prefer to match the existing explicit pattern.Relates to ENG-130. Keeping this as a draft for Josh to take the object-shape pieces.
🤖 Generated with Claude Code