Files
magpie/internal/davsync/stall.go
T
yetone f1224a055e sync: a backup on a slow line is no longer cut off at 2 minutes; a server that stops moving is (#657, irx999: a 43 MB backup with many skills to a WebDAV server on a 2.4 Mbps VPS, 305 KB/s, needs 2.4 minutes and every sync failed) — a sync had a fixed deadline in all (2 minutes from the gateway's loop and magpie webdav/s3 sync, 90 s from Settings › Sync now), so a large backup on a slow line never got through, however steadily it moved. WebDAV and S3 requests now go through a watchdog instead (stall.go): the backup's GET and PUT fail once nothing has moved either way for 60 s, each byte of the body sent or read putting it back, and after a PUT's body is all handed over the answer gets 60 s on top of as long as the write took (at least 4 minutes), for what still sits in the system's buffers to leave; the other requests (PROPFIND, MKCOL, HEAD, MOVE, DELETE) have 30 s in all. A stall says so ("the server sent and took nothing for 1m0s (PUT)"). Sync now and the CLI have no deadline; the gateway's loop keeps an hour as a backstop; a wait for another magpie's sync stays 30 s. TestSlowTransferIsNotCutShort (20 MB up and down at a steady trickle, over 5 times the watchdog each way), TestStalledTransferFailsPromptly (a server stopping mid-PUT and mid-GET fails within a second) and TestMetadataRequestHasALimit fail on the old code (no watchdog: the stalled ones hang) and pass. go vet, builds (darwin, GOOS=linux, GOOS=windows) and go test -tags nogui -count=1 ./... under a temp HOME pass. Not tried against a real slow WebDAV server.
2026-10-03 16:10:02 +08:00

143 lines
3.7 KiB
Go

package davsync
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"sync"
"time"
)
// A sync is never given a time to end in: a backup of tens of MB to a
// server on a slow line takes minutes (#657, 43 MB at 305 KB/s). What ends
// a request is a server that stops: the backup's GET and PUT (and S3's)
// fail once nothing has moved, either way, for stallAfter — a body still
// coming in or going out, however slowly, keeps them going. The other
// requests (PROPFIND, MKCOL, MOVE, DELETE…) move a few KB at most and have
// metaLimit in all.
var (
stallAfter = 60 * time.Second
metaLimit = 30 * time.Second
)
// errStalled is what a request ended for having moved nothing in time.
type errStalled struct {
method string
after time.Duration
idle bool
}
func (e *errStalled) Error() string {
if e.idle {
return fmt.Sprintf("the server sent and took nothing for %s (%s): the line or the server stopped", e.after, e.method)
}
return fmt.Sprintf("the server didn't answer in %s (%s)", e.after, e.method)
}
// syncClient is the client WebDAV and S3 are reached with.
var syncClient = &http.Client{Transport: stallTransport{http.DefaultTransport}}
// stallTransport gives each request its watchdog: for a GET or a PUT one
// put back each time bytes move, for the rest one that isn't.
type stallTransport struct{ base http.RoundTripper }
func (t stallTransport) RoundTrip(req *http.Request) (*http.Response, error) {
moving := req.Method == http.MethodGet || req.Method == http.MethodPut
after := metaLimit
if moving {
after = stallAfter
}
ctx, cancel := context.WithCancelCause(req.Context())
w := &watch{after: after, moving: moving, start: time.Now()}
w.timer = time.AfterFunc(after, func() {
cancel(&errStalled{method: req.Method, after: after, idle: moving})
})
done := func() { w.timer.Stop(); cancel(nil) }
r := req.Clone(ctx)
if req.Body != nil && req.Body != http.NoBody {
r.Body = &watched{ReadCloser: req.Body, w: w, ctx: ctx}
}
res, err := t.base.RoundTrip(r)
if err != nil {
done()
return nil, cause(ctx, err)
}
w.moved() // the answer's head came
res.Body = &watched{ReadCloser: res.Body, w: w, ctx: ctx, done: done}
return res, nil
}
// cause is err, or why ctx was ended when the watchdog ended it.
func cause(ctx context.Context, err error) error {
var s *errStalled
if c := context.Cause(ctx); errors.As(c, &s) {
return s
}
return err
}
type watch struct {
mu sync.Mutex
timer *time.Timer
after time.Duration
moving bool
start time.Time
}
// allSent is the request's body all handed to the network. The last MBs of
// it may sit in the system's buffers (macOS and Linux tune them up to 4 MB,
// and a proxy between holds more) and go out on a slow line long after: the
// answer is given stallAfter on top of as long as the write took, and at
// least 4 times stallAfter, for them to leave.
func (w *watch) allSent() {
if !w.moving {
return
}
w.mu.Lock()
w.timer.Reset(w.after + max(time.Since(w.start), 4*w.after))
w.mu.Unlock()
}
// moved puts the watchdog back: bytes went one way or the other.
func (w *watch) moved() {
if !w.moving {
return
}
w.mu.Lock()
w.timer.Reset(w.after)
w.mu.Unlock()
}
// watched is a body whose reads tell the watchdog.
type watched struct {
io.ReadCloser
w *watch
ctx context.Context
done func()
once sync.Once
}
func (b *watched) Read(p []byte) (int, error) {
n, err := b.ReadCloser.Read(p)
if n > 0 {
b.w.moved()
}
if b.done == nil && err == io.EOF { // the request's body
b.w.allSent()
}
if err != nil && err != io.EOF {
err = cause(b.ctx, err)
}
return n, err
}
func (b *watched) Close() error {
err := b.ReadCloser.Close()
if b.done != nil {
b.once.Do(b.done)
}
return err
}