Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions internal/api/handlers/playback_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -1865,7 +1865,12 @@ func (h *PlaybackHandler) startPlaybackApplicationV3(r *http.Request, body []byt
// earlier auto pick; only an explicit pick or a forced relink re-tries
// the known-bad candidate.
allowFailedCandidate := req.FileSelection == playback.FileSelectionExplicitV3 || req.ForceRelink
resolved, resolveErr := h.resolveVirtualStartWithVersionFallback(r, requestedFile, profileID, req, allowFailedCandidate, intOrZeroHandlerV3(req.BandwidthCapKbps))
// The start path is a fresh selection: declare the unbound intent on the
// request handed to the walk, which threads it (rather than forcing it)
// so the stale-pin recovery may re-pin here while a future session-bound
// caller reusing the walk keeps its own binding intent and refuses.
walkSourceReq := r.WithContext(withVirtualSessionBindingV3(r.Context(), false))
resolved, resolveErr := h.resolveVirtualStartWithVersionFallback(walkSourceReq, requestedFile, profileID, req, allowFailedCandidate, intOrZeroHandlerV3(req.BandwidthCapKbps))
if resolveErr != nil {
// A confirmed-dead pinned release is indicted here so a retry does
// not re-resolve it; an empty provider listing is not a verdict and
Expand Down Expand Up @@ -2631,11 +2636,28 @@ func (h *PlaybackHandler) resolveVirtualStartWithVersionFallback(
// elapses rather than waiting out the full cold-start budget per version.
walkCtx, cancel := context.WithTimeout(r.Context(), virtualStartVersionFallbackDecisionBudget)
defer cancel()
// Thread the caller's session-binding intent rather than forcing it false.
// The fresh-start start path declares unbound on the request context before
// calling this walk, so the stale-pin recovery may re-pin; a future
// session-bound caller that reuses the walk keeps its declaration (or the
// conservative session-bound default) and the recovery refuses to touch a
// live session's binding.
walkReq := r.WithContext(withVirtualSessionBindingV3(walkCtx, VirtualSessionBinding(r.Context())))

// Guarded stale-pin recovery for identity-less rows: when the pin id is
// absent from a non-empty live listing and the row carries no durable
// provider identity to rematch on, re-pin in memory to the best
// fingerprint-matched live candidate and retry once. Bounded to one attempt
// inside its own sub-budget; a miss falls through to the alternate walk
// below with the terminal intact.
if recovered, recoveredOK := h.recoverStaleIdentityLessPinV3(walkReq, file, profileID); recoveredOK {
return recovered, nil
}

alternates, alternateErr := h.findAlternateFiles(walkCtx, file, alternateOrderingForClient(req.Capabilities))
if alternateErr != nil || len(alternates) == 0 {
return resolved, resolveErr
}
walkReq := r.WithContext(walkCtx)

// candidateRows keeps one entry per walkable alternate, in fallback order.
// Skipping an AltMount-failed version here (rather than inside the resolve
Expand Down
Loading
Loading