mirror of
https://github.com/tailscale/tailscale.git
synced 2024-12-01 22:15:51 +00:00
6ada33db77
It is possible that upon a cold-start, we enqueue a partial file for deletion that is resumed shortly after startup. If the file transfer happens to last longer than deleteDelay, we will delete the partial file, which is unfortunate. The client spent a long time uploading a file, only for it to be accidentally deleted. It's a very rare race, but also a frustrating one if it happens to manifest. Fix the code to only delete partial files that do not have an active puts against it. We also fix a minor bug in ResumeReader where we read b[:blockSize] instead of b[:cs.Size]. The former is the fixed size of 64KiB, while the latter is usually 64KiB, but may be less for the last block. Updates tailscale/corp#14772 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
137 lines
3.6 KiB
Go
137 lines
3.6 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package taildrop
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"tailscale.com/tstest"
|
|
"tailscale.com/tstime"
|
|
"tailscale.com/util/must"
|
|
)
|
|
|
|
func TestDeleter(t *testing.T) {
|
|
dir := t.TempDir()
|
|
must.Do(touchFile(filepath.Join(dir, "foo.partial")))
|
|
must.Do(touchFile(filepath.Join(dir, "bar.partial")))
|
|
must.Do(touchFile(filepath.Join(dir, "fizz")))
|
|
must.Do(touchFile(filepath.Join(dir, "fizz.deleted")))
|
|
must.Do(touchFile(filepath.Join(dir, "buzz.deleted"))) // lacks a matching "buzz" file
|
|
|
|
checkDirectory := func(want ...string) {
|
|
t.Helper()
|
|
var got []string
|
|
for _, de := range must.Get(os.ReadDir(dir)) {
|
|
got = append(got, de.Name())
|
|
}
|
|
slices.Sort(got)
|
|
slices.Sort(want)
|
|
if diff := cmp.Diff(got, want); diff != "" {
|
|
t.Fatalf("directory mismatch (-got +want):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
clock := tstest.NewClock(tstest.ClockOpts{Start: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)})
|
|
advance := func(d time.Duration) {
|
|
t.Helper()
|
|
t.Logf("advance: %v", d)
|
|
clock.Advance(d)
|
|
}
|
|
|
|
eventsChan := make(chan string, 1000)
|
|
checkEvents := func(want ...string) {
|
|
t.Helper()
|
|
tm := time.NewTimer(10 * time.Second)
|
|
defer tm.Stop()
|
|
var got []string
|
|
for range want {
|
|
select {
|
|
case event := <-eventsChan:
|
|
t.Logf("event: %s", event)
|
|
got = append(got, event)
|
|
case <-tm.C:
|
|
t.Fatalf("timed out waiting for event: got %v, want %v", got, want)
|
|
}
|
|
}
|
|
slices.Sort(got)
|
|
slices.Sort(want)
|
|
if diff := cmp.Diff(got, want); diff != "" {
|
|
t.Fatalf("events mismatch (-got +want):\n%s", diff)
|
|
}
|
|
}
|
|
eventHook := func(event string) { eventsChan <- event }
|
|
|
|
var m Manager
|
|
var fd fileDeleter
|
|
m.opts.Logf = t.Logf
|
|
m.opts.Clock = tstime.DefaultClock{Clock: clock}
|
|
m.opts.Dir = dir
|
|
fd.Init(&m, eventHook)
|
|
defer fd.Shutdown()
|
|
insert := func(name string) {
|
|
t.Helper()
|
|
t.Logf("insert: %v", name)
|
|
fd.Insert(name)
|
|
}
|
|
remove := func(name string) {
|
|
t.Helper()
|
|
t.Logf("remove: %v", name)
|
|
fd.Remove(name)
|
|
}
|
|
|
|
checkEvents("start init")
|
|
checkEvents("end init", "start waitAndDelete")
|
|
checkDirectory("foo.partial", "bar.partial", "buzz.deleted")
|
|
|
|
advance(deleteDelay / 2)
|
|
checkDirectory("foo.partial", "bar.partial", "buzz.deleted")
|
|
advance(deleteDelay / 2)
|
|
checkEvents("deleted foo.partial", "deleted bar.partial", "deleted buzz.deleted")
|
|
checkEvents("end waitAndDelete")
|
|
checkDirectory()
|
|
|
|
must.Do(touchFile(filepath.Join(dir, "one.partial")))
|
|
insert("one.partial")
|
|
checkEvents("start waitAndDelete")
|
|
advance(deleteDelay / 4)
|
|
must.Do(touchFile(filepath.Join(dir, "two.partial")))
|
|
insert("two.partial")
|
|
advance(deleteDelay / 4)
|
|
must.Do(touchFile(filepath.Join(dir, "three.partial")))
|
|
insert("three.partial")
|
|
advance(deleteDelay / 4)
|
|
must.Do(touchFile(filepath.Join(dir, "four.partial")))
|
|
insert("four.partial")
|
|
|
|
advance(deleteDelay / 4)
|
|
checkEvents("deleted one.partial")
|
|
checkDirectory("two.partial", "three.partial", "four.partial")
|
|
checkEvents("end waitAndDelete", "start waitAndDelete")
|
|
|
|
advance(deleteDelay / 4)
|
|
checkEvents("deleted two.partial")
|
|
checkDirectory("three.partial", "four.partial")
|
|
checkEvents("end waitAndDelete", "start waitAndDelete")
|
|
|
|
advance(deleteDelay / 4)
|
|
checkEvents("deleted three.partial")
|
|
checkDirectory("four.partial")
|
|
checkEvents("end waitAndDelete", "start waitAndDelete")
|
|
|
|
advance(deleteDelay / 4)
|
|
checkEvents("deleted four.partial")
|
|
checkDirectory()
|
|
checkEvents("end waitAndDelete")
|
|
|
|
insert("wuzz.partial")
|
|
checkEvents("start waitAndDelete")
|
|
remove("wuzz.partial")
|
|
checkEvents("end waitAndDelete")
|
|
}
|