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

391 lines
8.8 KiB
Go

package playback
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
"litepan/internal/domain"
)
const (
defaultRemotePartSize = 4 << 20
defaultRemoteWindowSize = 8 << 20
maximumRemoteWindowSize = 32 << 20
remoteSequentialGap = 1 << 20
)
// remoteWindowReader 用有界窗口合并 FUSE 小读,并按驱动建议并发预取顺序数据。
type remoteWindowReader struct {
svc *Service
ctx context.Context
lh *linkHolder
size int64
partSize int64
concurrency int
windowSize int64
window []byte
windowOff int64
prefetch *remoteWindowPrefetch
prefetchWG sync.WaitGroup
progressEnd int64
}
type remoteWindowPrefetch struct {
off int64
cancel context.CancelFunc
done chan remoteWindowResult
}
type remoteWindowResult struct {
data []byte
err error
}
func newRemoteWindowReader(svc *Service, ctx context.Context, lh *linkHolder, link domain.DownloadInfo, size int64) *remoteWindowReader {
partSize := link.ChunkSize
if partSize <= 0 {
partSize = defaultRemotePartSize
}
if partSize > maximumRemoteWindowSize {
partSize = maximumRemoteWindowSize
}
concurrency := link.Concurrency
if concurrency <= 0 {
concurrency = 1
}
if concurrency > maximumRangeConcurrency {
concurrency = maximumRangeConcurrency
}
windowSize := partSize * int64(concurrency)
if windowSize < defaultRemoteWindowSize {
windowSize = defaultRemoteWindowSize
}
if windowSize > maximumRemoteWindowSize {
windowSize = maximumRemoteWindowSize
}
return &remoteWindowReader{
svc: svc,
ctx: ctx,
lh: lh,
size: size,
partSize: partSize,
concurrency: concurrency,
windowSize: windowSize,
progressEnd: -1,
}
}
func (r *remoteWindowReader) readAt(p []byte, off int64) (int, error) {
if len(p) == 0 {
return 0, nil
}
if off < 0 {
return 0, fmt.Errorf("negative read offset")
}
if r.size > 0 && off >= r.size {
return 0, io.EOF
}
eofAfterRead := false
if r.size > 0 && int64(len(p)) > r.size-off {
p = p[:r.size-off]
eofAfterRead = true
}
written := 0
for written < len(p) {
curOff := off + int64(written)
if !r.contains(curOff) {
if err := r.loadWindow(curOff); err != nil {
return written, err
}
}
start := curOff - r.windowOff
if start < 0 || start >= int64(len(r.window)) {
return written, io.ErrUnexpectedEOF
}
n := copy(p[written:], r.window[start:])
if n == 0 {
return written, io.ErrNoProgress
}
r.observeRead(curOff, int64(n))
written += n
}
if eofAfterRead {
return written, io.EOF
}
return written, nil
}
func (r *remoteWindowReader) contains(off int64) bool {
return len(r.window) > 0 && off >= r.windowOff && off < r.windowOff+int64(len(r.window))
}
func (r *remoteWindowReader) loadWindow(off int64) error {
windowOff := off / r.windowSize * r.windowSize
length := r.windowSize
if r.size > 0 {
if windowOff >= r.size {
return io.EOF
}
if windowOff+length > r.size {
length = r.size - windowOff
}
}
if r.prefetch != nil && r.prefetch.off == windowOff {
prefetch := r.prefetch
r.prefetch = nil
select {
case result := <-prefetch.done:
if result.err == nil {
r.windowOff = windowOff
r.window = result.data
r.progressEnd = windowOff
return nil
}
if err := r.ctx.Err(); err != nil {
return err
}
case <-r.ctx.Done():
prefetch.cancel()
return r.ctx.Err()
}
}
if r.prefetch != nil {
r.prefetch.cancel()
r.prefetch = nil
}
data, err := r.fetchWindow(r.ctx, windowOff, length)
if err != nil {
return err
}
r.windowOff = windowOff
r.window = data
r.progressEnd = windowOff
return nil
}
func (r *remoteWindowReader) observeRead(off, length int64) {
if length <= 0 {
return
}
windowEnd := r.windowOff + int64(len(r.window))
if off < r.windowOff || off >= windowEnd {
return
}
if r.progressEnd < r.windowOff {
r.progressEnd = r.windowOff
}
// 允许 FUSE 预读交错一个 I/O 请求大小,但大跨度跳读不触发预取。
if off > r.progressEnd+remoteSequentialGap {
return
}
if end := off + length; end > r.progressEnd {
r.progressEnd = end
}
if r.progressEnd-r.windowOff >= int64(len(r.window))/2 {
r.startNextPrefetch()
}
}
func (r *remoteWindowReader) startNextPrefetch() {
if len(r.window) == 0 {
return
}
nextOff := r.windowOff + int64(len(r.window))
if r.size > 0 && nextOff >= r.size {
return
}
if r.prefetch != nil {
if r.prefetch.off == nextOff {
return
}
r.prefetch.cancel()
}
length := r.windowSize
if r.size > 0 && nextOff+length > r.size {
length = r.size - nextOff
}
if length <= 0 {
r.prefetch = nil
return
}
ctx, cancel := context.WithCancel(r.ctx)
prefetch := &remoteWindowPrefetch{
off: nextOff,
cancel: cancel,
done: make(chan remoteWindowResult, 1),
}
r.prefetch = prefetch
r.prefetchWG.Add(1)
go func() {
defer r.prefetchWG.Done()
data, err := r.fetchWindow(ctx, nextOff, length)
prefetch.done <- remoteWindowResult{data: data, err: err}
}()
}
func (r *remoteWindowReader) fetchWindow(ctx context.Context, off, length int64) ([]byte, error) {
if length <= 0 {
return []byte{}, nil
}
data := make([]byte, int(length))
parts := int((length + r.partSize - 1) / r.partSize)
workers := r.concurrency
if workers > parts {
workers = parts
}
if workers <= 1 {
if err := r.readRangeInto(ctx, off, data); err != nil {
return nil, err
}
return data, nil
}
workerCtx, cancel := context.WithCancel(ctx)
defer cancel()
jobs := make(chan int)
var wg sync.WaitGroup
var errMu sync.Mutex
var firstErr error
worker := func() {
defer wg.Done()
for {
select {
case <-workerCtx.Done():
return
case index, ok := <-jobs:
if !ok {
return
}
begin := int64(index) * r.partSize
end := begin + r.partSize
if end > length {
end = length
}
if err := r.readRangeInto(workerCtx, off+begin, data[begin:end]); err != nil {
errMu.Lock()
if firstErr == nil {
firstErr = err
cancel()
}
errMu.Unlock()
return
}
}
}
}
wg.Add(workers)
for range workers {
go worker()
}
dispatching := true
for index := 0; index < parts && dispatching; index++ {
select {
case jobs <- index:
case <-workerCtx.Done():
dispatching = false
}
}
close(jobs)
wg.Wait()
if firstErr != nil {
return nil, firstErr
}
if err := ctx.Err(); err != nil {
return nil, err
}
return data, nil
}
func (r *remoteWindowReader) readRangeInto(ctx context.Context, off int64, dest []byte) error {
written := 0
for attempt := 0; written < len(dest); attempt++ {
n, err := r.readRangeOnce(ctx, off+int64(written), dest[written:])
written += n
if written == len(dest) {
return nil
}
if err == nil {
err = io.ErrNoProgress
}
if ctxErr := ctx.Err(); ctxErr != nil {
return ctxErr
}
if attempt >= 1 {
return err
}
}
return nil
}
func (r *remoteWindowReader) readRangeOnce(ctx context.Context, off int64, dest []byte) (int, error) {
if len(dest) == 0 {
return 0, nil
}
end := off + int64(len(dest)) - 1
link := r.lh.snapshot()
for try := 0; try < 2; try++ {
resp, err := r.svc.doRangeRequest(ctx, r.lh.accountID, link, off, end)
if err != nil {
return 0, err
}
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
_ = resp.Body.Close()
newLink, refreshed, refreshErr := r.lh.refreshAfterFailure(ctx, link)
if refreshErr != nil {
return 0, refreshErr
}
if !refreshed {
return 0, domain.Errorf(domain.CodeDriverError, "上游认证失败")
}
link = newLink
continue
}
if resp.StatusCode != http.StatusPartialContent && !(resp.StatusCode == http.StatusOK && off == 0 && r.size == int64(len(dest))) {
_ = resp.Body.Close()
return 0, domain.Errorf(domain.CodeDriverError, "上游 Range 返回 %d", resp.StatusCode)
}
if resp.StatusCode == http.StatusPartialContent {
if start, ok := contentRangeStart(resp.Header.Get("Content-Range")); ok && start != off {
_ = resp.Body.Close()
return 0, domain.Errorf(domain.CodeDriverError, "上游 Range 起点不匹配")
}
}
n, readErr := io.ReadFull(resp.Body, dest)
_ = resp.Body.Close()
return n, readErr
}
return 0, domain.Errorf(domain.CodeDriverError, "上游认证失败")
}
func contentRangeStart(value string) (int64, bool) {
value = strings.TrimSpace(value)
if len(value) < len("bytes ") || !strings.EqualFold(value[:len("bytes ")], "bytes ") {
return 0, false
}
rangePart := strings.SplitN(value[len("bytes "):], "/", 2)[0]
startPart := strings.SplitN(rangePart, "-", 2)[0]
start, err := strconv.ParseInt(strings.TrimSpace(startPart), 10, 64)
return start, err == nil && start >= 0
}
func (r *remoteWindowReader) close() error {
if r == nil {
return nil
}
if r.prefetch != nil {
r.prefetch.cancel()
r.prefetch = nil
}
r.prefetchWG.Wait()
return nil
}