Files
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

69 lines
2.1 KiB
Go

package davsync
import (
"context"
"errors"
"os"
"time"
"github.com/yetone/magpie/internal/settings"
)
// ErrBusy is a wait for another magpie's sync that ran out of time.
var ErrBusy = errors.New("another magpie is syncing: try again in a moment")
// lock keeps what reads or writes sync's files — a sync, Configure, Off,
// Dismiss — to one at a time across magpies: the gateway's syncs every
// Every, and magpie webdav from a terminal meanwhile, would each save
// sync-state.json over the other's. It keeps them apart within one magpie
// too, each taking it through a descriptor of its own, and is taken before
// mu, so a wait for another magpie never holds mu. The system holds it on
// sync.lock and lets it go when the magpie holding it ends, however it
// ends, so none is ever left behind. The file stays: taken away, one
// magpie could hold the lock on it while another took one on a new file.
func lock(ctx context.Context) (unlock func(), err error) {
os.MkdirAll(settings.Dir(), 0o755)
f, err := os.OpenFile(path("sync.lock"), os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
return func() {}, nil // no lock to be had here: sync as before there was one
}
for {
got, err := tryLock(f)
if err != nil { // a file system without locks, as an NFS home with no lockd
f.Close()
return func() {}, nil
}
if got {
return func() { unlockFile(f); f.Close() }, nil
}
select {
case <-ctx.Done():
f.Close()
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
return nil, ErrBusy
}
return nil, ctx.Err()
case <-time.After(200 * time.Millisecond):
}
}
}
// wait is how long Configure, Off, Dismiss and a sync wait for a sync in
// progress, which itself has no time limit (see stallAfter).
const wait = 30 * time.Second
// locked runs fn holding the lock, then mu, after a sync in progress: it
// would save its state over what fn does.
func locked(fn func() error) error {
ctx, cancel := context.WithTimeout(context.Background(), wait)
defer cancel()
unlock, err := lock(ctx)
if err != nil {
return err
}
defer unlock()
mu.Lock()
defer mu.Unlock()
return fn()
}