Files
LitePan/internal/playback/range_proxy_parallel_test.go
2026-07-27 19:31:43 +08:00

291 lines
7.9 KiB
Go

package playback
import (
"bytes"
"context"
"fmt"
"net/http"
"net/http/httptest"
"reflect"
"strconv"
"sync"
"testing"
"time"
"litepan/internal/domain"
)
type firstWriteBuffer struct {
b []byte
once sync.Once
first chan struct{}
}
func (w *firstWriteBuffer) Write(p []byte) (int, error) {
w.b = append(w.b, p...)
if len(p) > 0 {
w.once.Do(func() { close(w.first) })
}
return len(p), nil
}
func (w *firstWriteBuffer) Bytes() []byte { return w.b }
func TestProxyStreamUsesDriverConcurrencyAndPreservesOrder(t *testing.T) {
const partSize = 64 * 1024
data := make([]byte, 7*partSize+321)
for i := range data {
data[i] = byte(i % 251)
}
const streamStart = int64(17)
streamEnd := int64(len(data) - 29)
var mu sync.Mutex
active := 0
peak := 0
ranges := make(map[string]int)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start, end, err := parseSingleRange(r.Header.Get("Range"), int64(len(data)))
if err != nil {
http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
return
}
mu.Lock()
active++
if active > peak {
peak = active
}
ranges[r.Header.Get("Range")]++
mu.Unlock()
index := int((start - streamStart) / partSize)
delays := []time.Duration{45 * time.Millisecond, 5 * time.Millisecond, 20 * time.Millisecond}
time.Sleep(delays[index%len(delays)])
span := data[start : end+1]
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(data)))
w.Header().Set("Content-Length", strconv.Itoa(len(span)))
w.WriteHeader(http.StatusPartialContent)
_, _ = w.Write(span)
mu.Lock()
active--
mu.Unlock()
}))
t.Cleanup(server.Close)
svc := &Service{clientHTTP1: server.Client(), clientH2: server.Client()}
lh := &linkHolder{
svc: svc,
link: domain.DownloadInfo{URL: server.URL, ChunkSize: partSize, Concurrency: 3},
accountID: 7,
fileID: "file",
ua: "test",
}
var got bytes.Buffer
if err := svc.streamUpstreamBody(context.Background(), &got, lh, streamStart, streamEnd, partSize); err != nil {
t.Fatal(err)
}
if !bytes.Equal(got.Bytes(), data[streamStart:streamEnd+1]) {
t.Fatal("并行代理返回内容顺序错误")
}
wantRanges := make(map[string]int)
for start := streamStart; start <= streamEnd; start += partSize {
end := start + partSize - 1
if end > streamEnd {
end = streamEnd
}
wantRanges[fmt.Sprintf("bytes=%d-%d", start, end)] = 1
}
mu.Lock()
gotPeak := peak
gotRanges := ranges
mu.Unlock()
if gotPeak != 3 {
t.Fatalf("代理峰值并发=%d,期望 3", gotPeak)
}
if !reflect.DeepEqual(gotRanges, wantRanges) {
t.Fatalf("代理 Range=%v,期望 %v", gotRanges, wantRanges)
}
}
func TestProxyStreamWritesFirstBytesBeforePrefetchCompletes(t *testing.T) {
const partSize = 64 * 1024
data := make([]byte, 3*partSize)
for i := range data {
data[i] = byte(i % 239)
}
release := make(chan struct{})
var releaseOnce sync.Once
firstHandler := make(chan struct{})
prefixFlushed := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start, end, err := parseSingleRange(r.Header.Get("Range"), int64(len(data)))
if err != nil {
http.Error(w, err.Error(), http.StatusRequestedRangeNotSatisfiable)
return
}
span := data[start : end+1]
w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(data)))
w.Header().Set("Content-Length", strconv.Itoa(len(span)))
w.WriteHeader(http.StatusPartialContent)
if start == 0 {
close(firstHandler)
_, _ = w.Write(span[:1024])
w.(http.Flusher).Flush()
close(prefixFlushed)
<-release
_, _ = w.Write(span[1024:])
return
}
<-release
_, _ = w.Write(span)
}))
t.Cleanup(func() {
releaseOnce.Do(func() { close(release) })
server.Close()
})
svc := &Service{clientHTTP1: server.Client(), clientH2: server.Client()}
lh := &linkHolder{
svc: svc,
link: domain.DownloadInfo{URL: server.URL, ChunkSize: partSize, Concurrency: 3},
accountID: 10,
fileID: "file",
ua: "test",
}
got := &firstWriteBuffer{first: make(chan struct{})}
done := make(chan error, 1)
go func() {
done <- svc.streamUpstreamBody(context.Background(), got, lh, 0, int64(len(data)-1), partSize)
}()
select {
case <-firstHandler:
case <-time.After(2 * time.Second):
t.Fatal("首片上游请求没有启动")
}
select {
case <-prefixFlushed:
case <-time.After(2 * time.Second):
t.Fatal("测试上游没有刷出首片前缀")
}
select {
case <-got.first:
case <-time.After(2 * time.Second):
t.Fatal("代理等待完整预取后才写出首字节")
}
releaseOnce.Do(func() { close(release) })
select {
case err := <-done:
if err != nil {
t.Fatal(err)
}
case <-time.After(2 * time.Second):
t.Fatal("释放上游后代理没有完成")
}
if !bytes.Equal(got.Bytes(), data) {
t.Fatal("首片流式写出后内容顺序错误")
}
}
func TestProxyStreamCancellationStopsOutstandingRanges(t *testing.T) {
const partSize = 64 * 1024
started := make(chan struct{}, 3)
canceled := make(chan struct{}, 3)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Range", "bytes 0-65535/196608")
w.Header().Set("Content-Length", strconv.Itoa(partSize))
w.WriteHeader(http.StatusPartialContent)
w.(http.Flusher).Flush()
started <- struct{}{}
<-r.Context().Done()
canceled <- struct{}{}
}))
t.Cleanup(server.Close)
svc := &Service{clientHTTP1: server.Client(), clientH2: server.Client()}
lh := &linkHolder{
svc: svc,
link: domain.DownloadInfo{URL: server.URL, ChunkSize: partSize, Concurrency: 3},
accountID: 8,
fileID: "file",
ua: "test",
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan error, 1)
go func() {
done <- svc.streamUpstreamBody(ctx, &bytes.Buffer{}, lh, 0, 3*partSize-1, partSize)
}()
for range 3 {
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("并行代理请求未全部启动")
}
}
cancel()
select {
case err := <-done:
if err == nil {
t.Fatal("取消后代理返回 nil 错误")
}
case <-time.After(2 * time.Second):
t.Fatal("取消后代理没有及时退出")
}
for range 3 {
select {
case <-canceled:
case <-time.After(2 * time.Second):
t.Fatal("取消后仍有上游 Range 未退出")
}
}
}
func TestProxyStreamRejectsIncompleteRange(t *testing.T) {
const size = 64 * 1024
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusPartialContent)
_, _ = w.Write(make([]byte, size/2))
}))
t.Cleanup(server.Close)
svc := &Service{clientHTTP1: server.Client(), clientH2: server.Client()}
lh := &linkHolder{
svc: svc,
link: domain.DownloadInfo{URL: server.URL, ChunkSize: size, Concurrency: 1},
accountID: 9,
fileID: "file",
ua: "test",
}
if err := svc.streamUpstreamBody(context.Background(), &bytes.Buffer{}, lh, 0, size-1, size); err == nil {
t.Fatal("上游 Range 缺失一半数据时未返回错误")
}
}
func TestProxyStreamDoesNotWriteBytesPastRange(t *testing.T) {
const size = 64 * 1024
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Length", strconv.Itoa(size+1))
w.WriteHeader(http.StatusPartialContent)
_, _ = w.Write(make([]byte, size+1))
}))
t.Cleanup(server.Close)
svc := &Service{clientHTTP1: server.Client(), clientH2: server.Client()}
lh := &linkHolder{
svc: svc,
link: domain.DownloadInfo{URL: server.URL, ChunkSize: size, Concurrency: 1},
accountID: 11,
fileID: "file",
ua: "test",
}
var got bytes.Buffer
if err := svc.streamUpstreamBody(context.Background(), &got, lh, 0, size-1, size); err == nil {
t.Fatal("上游返回超出 Range 的数据时未报错")
}
if got.Len() != size {
t.Fatalf("代理向客户端写出 %d 字节,期望严格限制为 %d", got.Len(), size)
}
}