Files
silo-server/internal/proxy/remove_tracked_test.go
T
CoffeeKnyte 7adedf6429 fix(playback): close the serve-path monitoring and kill-switch gaps
Six defects on byte-serving paths, all of which made the PR's monitoring and
kill-switch claims narrower than documented.

#2/GAP-10 -- the ABS in-flight kill switch was a production no-op. accessLog
wraps every ABS route, and its statusRecorder implemented Write, WriteHeader,
Hijack and Flush but not Unwrap, so http.NewResponseController dead-ended and
SetWriteDeadline returned ErrNotSupported. A multi-GB audiobook pour survived a
RevokeUser. The existing test passed throughout because it called handlers
directly and never saw the middleware; the new test drives the mounted router
over a real socket, and both new assertions fail if Unwrap is removed again.

GAP-11 -- ebook, comic and PDF serving was invisible and un-killable: no meter,
no transfer record, no Refuse, no watcher, on a route that serves cbz/cbr/pdf
files routinely 100 MB-1 GB+. It now follows the ABS file-handler idiom. Note
guardRevocationCut is deliberately *not* reused here: it keys on a session_id
URL param and an ?st= token this route does not carry, so it would have
compiled and silently guarded nothing. Cap-exempt per decision A4 -- admission
is untouched and neither route consumes a stream slot.

#10 -- the no-proxy remote transcode hop forwarded segments through a bare
RollingDeadlineWriter, so bytes on the API hop went unaccounted for a supported
topology. Metering is scoped to media bodies; manifests are excluded so a
rewritten playlist is not counted as media, and a mid-copy failure is no longer
silently discarded.

#16 -- native, proxy and compat subtitle pours were entry-gated only. They now
carry a transport span, a meter and an in-flight watcher. Proxy subtitle bytes
are attributed only when a tracker record already exists: taking Track/Remove
lifecycle ownership per subtitle request would walk straight into the
overlapping-request defect (#1) that Batch 2 addresses. Compat subtitle
extraction is buffered and rejects bitmap formats, so a cut stops delivery but
not extraction already in progress.

M2 -- the proxy deferred tracker Remove with the request context, which is
already canceled on client disconnect, so the Redis DEL never happened and the
key lingered until TTL -- a false over-cap window that could get a legitimate
stream killed. Cleanup now uses a short bounded context.

GAP-13 -- mergeStreams took the freshest record wholesale and never merged
BytesServed, so a stream that poured 8 GiB at an edge could report 0. Merged as
a max, not a sum: the records are two observers of one pour. Fixed in
DedupeSessionInfos too, which had the same hole and feeds the admin view.

#11 needed no behaviour change -- that route was already metered, registered and
watched, and commit c24d8396 plus this Unwrap fix are what make its cut work.
Its comment claimed download-class exemption while the comment above it said the
?token= form is for iOS streaming; both facts and decision A4 are now stated.

Part of #305.
2026-07-30 06:11:43 +00:00

64 lines
1.5 KiB
Go

package proxy
import (
"context"
"net"
"testing"
"github.com/Silo-Server/silo-server/internal/nodesessions"
"github.com/redis/go-redis/v9"
)
type deleteCaptureHook struct {
called bool
contextErr error
}
func (h *deleteCaptureHook) DialHook(next redis.DialHook) redis.DialHook {
return next
}
func (h *deleteCaptureHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
if cmd.Name() == "del" {
h.called = true
h.contextErr = ctx.Err()
return nil
}
return next(ctx, cmd)
}
}
func (h *deleteCaptureHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
return next
}
func TestRemoveTrackedUsesLiveContextAfterRequestCancellation(t *testing.T) {
rdb := redis.NewClient(&redis.Options{
Dialer: func(context.Context, string, string) (net.Conn, error) {
t.Fatal("DEL should be intercepted before dialing")
return nil, nil
},
})
t.Cleanup(func() { _ = rdb.Close() })
hook := &deleteCaptureHook{}
rdb.AddHook(hook)
server := &Server{tracker: nodesessions.NewTracker(rdb, "http://node", "node", "proxy")}
requestCtx, cancel := context.WithCancel(context.Background())
cancel()
if requestCtx.Err() == nil {
t.Fatal("request context was not canceled")
}
func() {
defer server.removeTracked("session-1")
}()
if !hook.called {
t.Fatal("Redis DEL was not attempted")
}
if hook.contextErr != nil {
t.Fatalf("DEL context error = %v, want live bounded cleanup context", hook.contextErr)
}
}