taildrop: fix theoretical race condition in fileDeleter.Init (#9876)

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>
This commit is contained in:
Joe Tsai
2023-10-19 13:26:55 -07:00
committed by GitHub
parent 25b6974219
commit 6ada33db77
5 changed files with 40 additions and 29 deletions
+23 -12
View File
@@ -27,8 +27,8 @@ const deleteDelay = time.Hour
type fileDeleter struct {
logf logger.Logf
clock tstime.DefaultClock
event func(string) // called for certain events; for testing only
dir string
event func(string) // called for certain events; for testing only
mu sync.Mutex
queue list.List
@@ -46,11 +46,11 @@ type deleteFile struct {
inserted time.Time
}
func (d *fileDeleter) Init(logf logger.Logf, clock tstime.DefaultClock, event func(string), dir string) {
d.logf = logf
d.clock = clock
d.dir = dir
d.event = event
func (d *fileDeleter) Init(m *Manager, eventHook func(string)) {
d.logf = m.opts.Logf
d.clock = m.opts.Clock
d.dir = m.opts.Dir
d.event = eventHook
// From a cold-start, load the list of partial and deleted files.
d.byName = make(map[string]*list.Element)
@@ -59,19 +59,30 @@ func (d *fileDeleter) Init(logf logger.Logf, clock tstime.DefaultClock, event fu
d.group.Go(func() {
d.event("start init")
defer d.event("end init")
rangeDir(dir, func(de fs.DirEntry) bool {
rangeDir(d.dir, func(de fs.DirEntry) bool {
switch {
case d.shutdownCtx.Err() != nil:
return false // terminate early
case !de.Type().IsRegular():
return true
case strings.Contains(de.Name(), partialSuffix):
d.Insert(de.Name())
case strings.Contains(de.Name(), deletedSuffix):
case strings.HasSuffix(de.Name(), partialSuffix):
// Only enqueue the file for deletion if there is no active put.
nameID := strings.TrimSuffix(de.Name(), partialSuffix)
if i := strings.LastIndexByte(nameID, '.'); i > 0 {
key := incomingFileKey{ClientID(nameID[i+len("."):]), nameID[:i]}
m.incomingFiles.LoadFunc(key, func(_ *incomingFile, loaded bool) {
if !loaded {
d.Insert(de.Name())
}
})
} else {
d.Insert(de.Name())
}
case strings.HasSuffix(de.Name(), deletedSuffix):
// Best-effort immediate deletion of deleted files.
name := strings.TrimSuffix(de.Name(), deletedSuffix)
if os.Remove(filepath.Join(dir, name)) == nil {
if os.Remove(filepath.Join(dir, de.Name())) == nil {
if os.Remove(filepath.Join(d.dir, name)) == nil {
if os.Remove(filepath.Join(d.dir, de.Name())) == nil {
break
}
}