Skip to content

Encapsulate live-runner session payments in a stateful session/client object #30

@rickstaa

Description

@rickstaa

Problem

Streamed live-runner sessions (trickle / websocket) must keep paying for as long as the session is open, because the orchestrator meters the open session by wall-clock/segments and drops it when the balance runs dry. Today the SDK exposes this as a free function, run_session_payments(session, interval=...), which the caller must drive manually:

payment_task = asyncio.create_task(run_session_payments(session, interval=...))
...
finally:
    payment_task.cancel()
    await payment_task

reserve_session already mints and attaches the payment_session credential to the returned LiveRunnerSession, but LiveRunnerSession is a frozen dataclass with no lifecycle, so it cannot own the background task. That pushes task creation and (critically) cancellation onto every consumer. Forget the finally and you silently stop paying or leak a task. The echo example (example-apps/echo/client.py) shows the boilerplate this requires.

Prior art (already in our codebase)

Two better-encapsulated patterns already exist and prove the approach:

  • lv2v.py: Lv2vJob.start_payment_sender() runs a per-output-segment payment sender, auto-started by start_lv2v(start_payments=True) and cancelled by job.aclose(). The consumer cannot forget it.
  • daydreamlive/scope ja/runner (server/livepeer_client.py): LivepeerClient starts an interval _payment_loop right after connect, gated on payment_session is not None, and cancels it on teardown. This is effectively run_session_payments hand-rolled inside a client object.

Proposal

Make reserve_session return a stateful session/client that owns the payment lifecycle, so payments are automatic for on-chain sessions:

async with await reserve_session(app=..., signer_url=signer) as session:
    # publish/consume trickle; payments start and stop automatically
  • Auto-start the payment loop when payment_session is not None (on-chain only; no-op offchain).
  • Auto-cancel on context exit / aclose(), alongside stop_runner_session.
  • Keep run_session_payments as the internal primitive.
  • This mirrors start_lv2v(start_payments=True) + job.aclose().

Payments still need a scope anchor (the session must know when the stream ended); the context manager / aclose() provides it. The object cannot infer that media stopped on its own.

Open questions

  1. One unified LiveRunnerSession object that subsumes Lv2vJob, or keep lv2v separate and only wrap the general live-runner path? The former removes the parallel implementations; the latter is the minimal change.
  2. Cadence semantics to standardize on: lv2v pays per output segment (only while media flows); run_session_payments pays on a fixed interval (pays even when paused-but-open). These differ in over-payment behavior and should be decided before standardizing the interface.

Context

Follow-up from the echo example migration and the run_session_payments work (branch rs/live-runner-session-payments). Related: gateway PR #25 (SSE streaming).

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions