Files
tuliprox/docs/src/configuration/connection-handling-session-implementation-notes.md
T

504 lines
20 KiB
Markdown
Raw Normal View History

2026-04-13 12:26:44 +02:00
# Session Handling Internals
This page is for maintainers working on Tuliprox session code.
If you only want the operator-facing behavior, read:
- [Sessions, HLS, Catchup and Reconnects](./connection-handling-sessions-and-reconnects.md)
2026-04-21 15:21:51 +02:00
- [Connection Handling Runtime Flow](./connection-handling-runtime-flow.md)
If you want the broader runtime orchestration around admission, placeholders, provider-open, and cleanup, also read:
- [Connection Handling Runtime Internals](./connection-handling-runtime-internals.md)
2026-04-13 12:26:44 +02:00
## Why this page exists
Tuliprox uses the word "session" for several related but different mechanisms:
- user admission reuse
- logical playback identity
- socket/address tracking
- provider/account affinity
- adaptive reconnect preservation
- provider-account affinity and reservation
These mechanisms interact, but they are not the same thing.
The most important rule for future changes is:
- do not derive session-based admission from socket-binding rules
- do not derive provider/account affinity from socket-binding rules either
That exact confusion breaks VOD, series, catchup, and local reopen/seek flows.
2026-04-21 15:21:51 +02:00
This page is deliberately narrower than the runtime-internals page:
- this page focuses on session identity, session admission, socket binding, and provider affinity
- the runtime-internals page focuses on the full admission/playback activity flow
2026-04-13 12:26:44 +02:00
## Core terms
### Session token
2026-05-13 09:01:26 +02:00
The session token is the playback identity used by admission, address tracking, and provider affinity.
It is stable for follow-up requests of the same playback, but it is not always derived from the same fields:
- plain TS live uses a socket-scoped playback token
- VOD, series, catchup, and local playback use a logical token suitable for reopen/seek/range workflows
2026-09-09 21:05:52 +02:00
- HLS playlist starts use a token scoped to that initial adaptive playback start, and rewritten segment URLs carry that token forward
- DASH playlist starts use an equally scoped token, but the redirected player fetches DASH segments directly from the provider
2026-04-13 12:26:44 +02:00
It is used to answer questions like:
- is this the same playback as before?
- may this request reopen an existing playback while the user is already at the hard limit?
- should Tuliprox keep using the same provider context if possible?
### Session admission
Session admission means user-limit checks are performed with `connection_admission_for_session(...)` instead of plain `connection_admission(...)`.
This happens in `resolve_admission_with_strategies(..., use_session_admission, session_token)`.
If `use_session_admission` is `true` and the session token already exists, the request may continue even when the user is
already at the nominal connection limit.
This is required for normal player behavior such as:
- VOD size checks
- VOD seeks
- pause/resume reopen
- HLS segment fetches
- local media reopen
### Socket-bound session
Socket binding only controls how Tuliprox tracks request addresses for the same session.
It does not decide:
- whether the request should use session admission
- whether the logical playback must stay on the same provider account
Current socket-binding policy:
- `PlaylistItemType::Live`
- `PlaylistItemType::LiveUnknown`
are socket-bound.
Everything else is not socket-bound:
- `LiveHls`
- `LiveDash`
- `Video`
- `Series`
- `Catchup`
- local playback types
### Provider/account affinity
Provider/account affinity answers a different question:
- when several requests belong to the same logical playback, must they keep using the same provider account?
The intended behavior is:
- TS-style live playback is socket-bound, but not provider-account-bound across requests
- VOD/movie/series playback is not socket-bound, but is provider/account-bound for the logical stream
- HLS/DASH playback is not socket-bound, but is provider/account-bound for the logical stream
Why:
- TS-style live playback has no seek/range workflow and each new request can be treated independently
- VOD uses range and seek requests that still belong to one playback
- HLS fetches many chunk requests that still belong to one playback
2026-05-13 09:01:26 +02:00
This means the same provider-affine HLS, VOD, series, or catchup stream must keep the same provider account for
its follow-up requests, even though those requests may arrive on different sockets.
2026-04-13 12:26:44 +02:00
2026-09-09 21:05:52 +02:00
### DASH delivery boundary (redirect only)
Tuliprox treats DASH as **redirect-only**:
- DASH requests (`PlaylistItemType::LiveDash` or requests with `.mpd` extension) always resolve to an HTTP redirect directly to
the upstream provider URL.
- DASH reverse-proxying and segment caching are **not supported**. Enabling reverse-proxy mode for a user or target does not route
DASH streams through the HLS proxy handler or cache segments in Tuliprox.
- DASH maintains provider-account affinity at the logical playback level (reusing session tokens and reserving the pinned
provider account across reopen/refresh within `hls_session_ttl_secs`).
- Because segments are fetched directly between the player and the upstream provider, Tuliprox cannot count individual segment
requests or confirm first-byte delivery for DASH segments. Full segment lease guarantees apply only to streams proxied
directly through Tuliprox (TS, HLS, VOD, Series, Catchup).
2026-04-13 12:26:44 +02:00
### Adaptive preserved session
Adaptive live playback (`LiveHls`, `LiveDash`) can stay logically alive for a short TTL even after the current socket disconnects.
That logic lives in `ActiveUserManager` and uses:
- `should_preserve_session_stream(...)`
- `build_preserved_stream_expiry(...)`
- `process_due_adaptive_expiry_entries(...)`
2026-09-09 10:28:42 +02:00
### Provider slot lease
2026-04-13 12:26:44 +02:00
2026-09-09 10:28:42 +02:00
A provider reservation is now a *slot lease* owned by a playback identity, not by a socket. It is a provider-slot
affinity mechanism, not a user admission mechanism. One owner holds at most one lease, so repeated requests of the same
playback reuse the slot instead of stacking a reservation per attempt.
2026-04-13 12:26:44 +02:00
2026-09-09 10:28:42 +02:00
A lease has three states:
- `Starting`: claimed when a provider allocation is acquired. It pins the provider for its own playback and blocks
nobody else. It expires after a short startup deadline regardless of the configured TTL, so an abandoned start or a
manifest-retry loop never accumulates reservations.
- `Active`: reached only when real media delivery is confirmed — the first provider media byte forwarded to a client as
an `OK`/`206` media response. Manifest, HEAD, key, map and error fetches never confirm a lease. Only an `Active` lease
with a configured reconnect window reserves capacity against other playbacks.
- `Idle`: a confirmed, reconnect-capable playback that ended cleanly keeps its slot for the reconnect window so it can
reattach to the same provider.
TTL mapping (the reconnect window):
2026-04-13 12:26:44 +02:00
- HLS/DASH use `hls_session_ttl_secs`
- Catchup uses `catchup_session_ttl_secs`
- other item types use `0`
2026-09-09 10:28:42 +02:00
Lease end is outcome-driven: provider failure, preemption, kick and timeout release capacity immediately; a clean finish
of a reconnect-capable playback keeps the idle window.
2026-04-13 12:26:44 +02:00
Important:
- VOD/movie/series still have strict provider affinity on follow-up requests
2026-09-09 10:28:42 +02:00
- they just do not currently get an extra post-request reconnect window like HLS/Catchup do
- An HTTP 200 manifest response is not playback and does not confirm a lease; neither do HEAD, key, map or error-video
responses. Only an `OK`/`206` media response confirms media delivery, which is what previously let entry
reservations block unrelated clients behind the same reverse proxy
### Multi-user socket isolation and allocation-based provider connections
When multiple users connect through the same reverse proxy socket:
- `ActiveUserManager` tracks user sessions per user key and stream UID. `SocketRegistration` records all registered
usernames for each `SocketAddr`, ensuring concurrent users on the same socket never overwrite or prematurely evict
each other's session state.
- `ActiveProviderManager` indexes single connections by unique `AllocationId` rather than grouping them under a single
socket address key (`ClientConnectionId`). Secondary socket indexes exist only for whole-transport close and kick
signals.
- Stream details and QoS fallbacks are updated directly by `stream_uid` (`update_stream_detail_by_uid`) rather than
relying on ambiguous socket lookups.
2026-04-13 12:26:44 +02:00
2026-09-10 21:41:05 +02:00
### Request ownership and binding generations
The runtime uses separate identities for separate ownership scopes:
| Identity | Scope |
| :--- | :--- |
| session token | one logical user playback |
| `PlaybackLeaseId` | one logical provider-capacity lease |
| `PlaybackRequestId` | one provider request attached to that lease |
| `ProviderBindingTag` | one generation of the owner's provider-account binding |
| `SharedSubscriberId` | one viewer attached to a shared MPEG-TS origin |
Parallel range, segment and reconnect requests may belong to the same logical stream while retaining independent request
claims. Completing one request removes only that claim. The logical stream and provider lease remain until their own
terminal conditions are met.
A provider binding increments its generation when the same playback moves to a different provider or reactivates an
idle lease. Cleanup created for an older generation cannot clear the successor binding. This protects new playback from
delayed body drops, provider errors and asynchronous HLS cleanup belonging to the preceding request.
### Cleanup ownership and overload behavior
Connection registration reserves a cleanup permit before publishing mutable user or provider state. Ownership then
moves into the response body or shared-subscriber body. Dropping an unpolled body, cancelling an await, reaching EOF or
reporting an error all converge on request-specific, idempotent cleanup.
The regular cleanup queue is bounded by `reverse_proxy.stream.cleanup_queue_capacity`. Admission waits for a bounded
interval when all permits are held and then rejects the request instead of registering a stream without guaranteed
cleanup. HLS origin I/O uses a separate bounded control lane that is preferred over ordinary body cleanup, preventing a
saturated subscriber workload from starving release of an origin provider handle.
Graceful shutdown first closes the admission gate, then removes shared origins and active user claims, releases provider
leases, flushes stream history and cancels and joins the owned background workers. Cleanup emitted by a later body drop
is harmless because the terminal indices have already been emptied.
### Shared MPEG-TS subscriber isolation
A shared origin is keyed by stream URL, but its viewers are keyed by `SharedSubscriberId`, never by `SocketAddr`. This
allows several users behind one reverse proxy connection to consume independent response bodies without replacing one
another.
Each subscriber has both a chunk limit and a byte semaphore for queued payload. Burst replay and live delivery use a
progress deadline that is renewed only after a successful send. Timeout, cancellation and a closed receiver are distinct
terminal outcomes; an incomplete replay never advances directly to the live tail. A pending-join guard also prevents
the last existing subscriber from tearing down an origin while another subscriber is committing.
2026-04-13 12:26:44 +02:00
## Where session tokens come from
### TS live, VOD, series
2026-05-13 09:01:26 +02:00
The regular M3U and Xtream playback endpoints build playback tokens with:
- `create_playback_session_fingerprint(...)`
The helper deliberately keeps the token matrix different per playback type:
2026-04-13 12:26:44 +02:00
2026-05-13 09:01:26 +02:00
- plain TS live is socket-scoped, so two TS sockets are two independent playback attempts
- VOD and series are logical, so range/seek/reopen requests can continue the same playback
- HLS/DASH playlist starts are scoped to the initial adaptive playback start, so two players behind the same
IP/user-agent can watch the same HLS/DASH channel independently
2026-04-13 12:26:44 +02:00
### Catchup
Catchup uses a different token namespace:
- `create_catchup_session_key(fingerprint, username, virtual_id)`
### HLS segment requests
Rewritten HLS URLs can carry the session token inside the encrypted segment token.
On segment fetch:
- Tuliprox decodes the HLS token
- extracts the session token if present
2026-05-13 09:01:26 +02:00
- falls back to a logical `create_session_fingerprint(...)` only for legacy or malformed tokens without an embedded session token
2026-04-13 12:26:44 +02:00
### Local playback
2026-05-13 09:01:26 +02:00
Local playback uses a stable logical `playback_session_token` passed into `local_stream_response(...)`.
2026-04-13 12:26:44 +02:00
## The three decisions that must stay separate
### 1. Should this request use session admission?
For playback endpoints, the answer is yes.
That means these paths should call `resolve_admission_with_strategies(..., true, Some(session_token))`:
- M3U playback
- Xtream playback
- HLS session/segment handling
- local playback reopen logic
Reason:
- a second request for the same logical playback must be admitted as the same playback
- not as a brand-new connection
2026-04-21 15:21:51 +02:00
Important:
- this is separate from background metadata/probe work
- probe/resolve tasks do not use normal playback session admission
- probe-capable background work instead uses provider-side probe handles and provider preemption rules
2026-04-13 12:26:44 +02:00
### 2. Should this session be bound to exactly one current socket?
This is controlled by `PlaylistItemType::uses_socket_bound_session()`.
Reason:
- plain TS-style live playback behaves like one active transport socket
- VOD, HLS, DASH, catchup, and local playback often use multiple short-lived sockets for one logical playback
Do not replace decision 1 with decision 2.
If you do that:
- existing VOD sessions stop reopening correctly at `max_connections: 1`
- seek/size-check flows start failing with `UserConnectionsExhausted`
### 3. Should this logical playback stay on the same provider account?
This is separate from both admission and socket tracking.
This policy is controlled by `PlaylistItemType::requires_provider_affinity()`.
Intended model:
- TS live: no provider/account pinning across requests
- VOD/movie/series: provider/account pinned across the logical stream
- HLS/DASH: provider/account pinned across the logical stream
- Catchup: provider/account pinned across the logical stream
If you collapse this into socket-binding, you get the wrong matrix:
- TS would be over-pinned
2026-05-13 09:01:26 +02:00
- VOD/series/HLS would be under-pinned
2026-04-13 12:26:44 +02:00
## Current code flow
### 1. Admission
Main entry point:
- `api_utils::resolve_admission_with_strategies(...)`
If `use_session_admission` is `true`, Tuliprox checks:
- `AppState::get_connection_admission_for_session(...)`
- `ActiveUserManager::connection_admission_for_session(...)`
If `false`, it falls back to:
- `AppState::get_connection_admission(...)`
- `ActiveUserManager::connection_admission(...)`
### 2. Session creation or refresh
Main entry point:
- `ActiveUserManager::create_user_session(...)`
This stores:
- session token
- virtual id
- provider
- stream url
- current address
- `socket_bound`
- `active_addrs`
- connection permission and kind
### 3. Stream registration
Main entry point:
- `ActiveUserManager::update_connection(...)`
2026-09-10 21:41:05 +02:00
This creates or reuses the tracked logical stream entry and attaches a request claim.
2026-04-13 12:26:44 +02:00
Important behavior:
- same `session_token` reuses the logical stream
2026-09-10 21:41:05 +02:00
- every response receives its own request UID even when it reuses that stream
- ending one response detaches only its request claim; the stream is removed when its final claim ends
2026-04-13 12:26:44 +02:00
- stream metrics and duration stay tied to the logical session
- for non-socket-bound sessions, the session remembers multiple active addresses
### 4. Lightweight HTTP activity
Main entry point:
- `ActiveUserManager::touch_http_activity(...)`
This is used for session continuity without creating a new logical stream, especially for HLS follow-up requests.
### 5. Seek / forced provider reopen
Main entry point:
- `api_utils::force_provider_stream_response(...)`
This path:
- releases the current provider-side connection for the session
- reacquires a provider connection
- moves the session to the request address with `update_session_addr(...)`
For non-socket-bound sessions, `update_session_addr(...)` does not forget the older active session addresses.
That is what allows a VOD session to survive:
- overlapping size-check requests
- seek requests
- overlapping teardown/start windows
The address move itself is not the provider-affinity rule.
2026-05-13 09:01:26 +02:00
Provider-affinity is the separate invariant that follow-up requests for the same VOD/series/HLS playback should
continue on the same provider account.
2026-04-13 12:26:44 +02:00
2026-04-21 15:21:51 +02:00
This also explains why seek/reopen behavior must not be confused with:
- ordinary new activation
- socket-bound TS admission
- background probe/resolve tasks
2026-04-13 12:26:44 +02:00
### 6. Disconnect handling
Main entry points:
- `ActiveUserManager::release_stream(...)`
- `ActiveUserManager::release_connection(...)`
For socket-bound sessions, closing the current address removes the old address from the session immediately.
For non-socket-bound sessions:
- the closed address is removed from `active_addrs`
- if another active address still belongs to that session, the session and logical stream fall back to that address
This is the key behavior that keeps VOD and local playback stable across multiple sockets.
2026-09-10 21:41:05 +02:00
Queued provider cleanup carries the `PlaybackRequestId` and owner captured during acquire, while provider handles retain
their `ProviderBindingTag`. The lease table validates both against the current binding, so cleanup does not need to
rediscover ownership from a possibly reused socket address.
2026-04-13 12:26:44 +02:00
## URL persistence rules
Session URL handling is intentionally different by stream type.
For:
- `Video`
- `Series`
- `Catchup`
- local VOD/series playback
Tuliprox stores the canonical request URL in the session, not an ephemeral redirect target.
Reason:
- provider-side redirects can resolve a request to a non-canonical final URL
- reusing that final URL later can change or break VOD seek/reopen behavior
For live playback, following the redirected provider URL is still acceptable and often desirable.
## File map for future changes
Read these files together before changing session logic:
2026-09-10 21:41:05 +02:00
- `backend/core/src/model/playback.rs`
- `backend/session/src/provider_leases.rs`
- `backend/session/src/active_user_manager/mod.rs`
- `backend/session/src/active_provider_manager.rs`
- `backend/session/src/connection_manager.rs`
- `backend/session/src/streams/shared_stream_manager.rs`
- `backend/app/src/api/api_utils/mod.rs`
- `backend/app/src/api/model/streams/active_client_stream.rs`
- `backend/app/src/api/endpoints/hls_api/session.rs`
- `backend/hls/src/origin.rs`
2026-04-13 12:26:44 +02:00
## Tests that should stay green
These tests cover the most fragile parts of the current logic:
- `resolve_admission_with_strategies_allows_existing_session_even_when_user_is_at_limit`
- `local_stream_response_reuses_stable_playback_session_token_across_reopens`
- `vod_session_survives_overlapping_and_seek_sockets`
2026-05-13 09:01:26 +02:00
- `playback_session_fingerprint_keeps_ts_socket_bound_but_vod_logical`
- `adaptive_playback_session_fingerprint_is_unique_per_initial_socket`
- `unlimited_user_can_open_same_and_different_live_streams_from_same_ip`
- `recently_evicted_vod_uses_session_reentry_guard`
2026-04-13 12:26:44 +02:00
- `update_session_addr_prunes_previous_registration_for_socket_bound_session`
- `test_adaptive_session_release_connection_preserves_logical_stream_and_start_time`
2026-09-10 21:41:05 +02:00
- `stale_direct_cleanup_after_rebind_preserves_successor_request`
- `subscribers_on_same_socket_do_not_replace_each_other`
- `send_client_chunk_respects_byte_budget`
- `control_cleanup_lane_precedes_a_ready_body_cleanup_backlog`
- `shutdown_with_all_cleanup_permits_held_completes`
- `http_provider_lease_downstream_smoke_test`
2026-04-13 12:26:44 +02:00
If you change session code and one of these assumptions no longer holds, update the documentation and the tests in the same change.
2026-04-21 15:21:51 +02:00
## Related pages
- [Connection Handling Runtime Flow](./connection-handling-runtime-flow.md)
- [Connection Handling Runtime Internals](./connection-handling-runtime-internals.md)
- [Sessions, HLS, Catchup and Reconnects](./connection-handling-sessions-and-reconnects.md)
- [Failures and User-Visible Behavior](./connection-handling-failures-and-user-visible-behavior.md)