mirror of
https://github.com/rclone/rclone.git
synced 2024-11-26 02:14:42 +01:00
vfs: fix writeback deadlocks and other bugs
- fix deadlock when cancelling upload - fix double upload and panic after cancelled upload - fix cancelation strategy of uploading files - don't cancel uploads if we don't modify the file - cancel uploads if we do modify the file - fix deadlock between Item and writeback - fix confusion about whether writeback item was being uploaded - fix cornercases in cancelling uploads and removing files
This commit is contained in:
parent
279a516c53
commit
fa472a340e
@ -558,6 +558,7 @@ func (f *File) Remove() (err error) {
|
|||||||
if wasWriting {
|
if wasWriting {
|
||||||
// Ignore error deleting file if was writing it as it may not be uploaded yet
|
// Ignore error deleting file if was writing it as it may not be uploaded yet
|
||||||
err = nil
|
err = nil
|
||||||
|
fs.Debugf(f._path(), "Ignoring File.Remove file error as uploading: %v", err)
|
||||||
} else {
|
} else {
|
||||||
fs.Debugf(f._path(), "File.Remove file error: %v", err)
|
fs.Debugf(f._path(), "File.Remove file error: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -315,9 +315,14 @@ func (c *Cache) Rename(name string, newName string, newObj fs.Object) (err error
|
|||||||
func (c *Cache) Remove(name string) (wasWriting bool) {
|
func (c *Cache) Remove(name string) (wasWriting bool) {
|
||||||
name = clean(name)
|
name = clean(name)
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
item, _ := c._get(name)
|
item := c.item[name]
|
||||||
delete(c.item, name)
|
if item != nil {
|
||||||
|
delete(c.item, name)
|
||||||
|
}
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
if item == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return item.remove("file deleted")
|
return item.remove("file deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,11 @@ import (
|
|||||||
// be taken before Item.mu. downloader may call into Item but Item may
|
// be taken before Item.mu. downloader may call into Item but Item may
|
||||||
// **not** call downloader methods with Item.mu held
|
// **not** call downloader methods with Item.mu held
|
||||||
|
|
||||||
|
// NB Item and writeback are tightly linked so it is necessary to
|
||||||
|
// have a total lock ordering between them. writeback.mu must always
|
||||||
|
// be taken before Item.mu. writeback may call into Item but Item may
|
||||||
|
// **not** call writeback methods with Item.mu held
|
||||||
|
|
||||||
// Item is stored in the item map
|
// Item is stored in the item map
|
||||||
//
|
//
|
||||||
// The Info field is written to the backing store to store status
|
// The Info field is written to the backing store to store status
|
||||||
@ -49,6 +54,7 @@ type Item struct {
|
|||||||
o fs.Object // object we are caching - may be nil
|
o fs.Object // object we are caching - may be nil
|
||||||
fd *os.File // handle we are using to read and write to the file
|
fd *os.File // handle we are using to read and write to the file
|
||||||
metaDirty bool // set if the info needs writeback
|
metaDirty bool // set if the info needs writeback
|
||||||
|
modified bool // set if the file has been modified since the last Open
|
||||||
info Info // info about the file to persist to backing store
|
info Info // info about the file to persist to backing store
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -60,7 +66,7 @@ type Info struct {
|
|||||||
Size int64 // size of the file
|
Size int64 // size of the file
|
||||||
Rs ranges.Ranges // which parts of the file are present
|
Rs ranges.Ranges // which parts of the file are present
|
||||||
Fingerprint string // fingerprint of remote object
|
Fingerprint string // fingerprint of remote object
|
||||||
Dirty bool // set if the backing file has been modifed
|
Dirty bool // set if the backing file has been modified
|
||||||
}
|
}
|
||||||
|
|
||||||
// Items are a slice of *Item ordered by ATime
|
// Items are a slice of *Item ordered by ATime
|
||||||
@ -111,7 +117,7 @@ func newItem(c *Cache, name string) (item *Item) {
|
|||||||
if os.IsNotExist(statErr) {
|
if os.IsNotExist(statErr) {
|
||||||
item._removeMeta("cache file doesn't exist")
|
item._removeMeta("cache file doesn't exist")
|
||||||
} else {
|
} else {
|
||||||
item._remove(fmt.Sprintf("failed to stat cache file: %v", statErr))
|
item.remove(fmt.Sprintf("failed to stat cache file: %v", statErr))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,7 +126,7 @@ func newItem(c *Cache, name string) (item *Item) {
|
|||||||
if !exists {
|
if !exists {
|
||||||
item._removeFile("metadata doesn't exist")
|
item._removeFile("metadata doesn't exist")
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
item._remove(fmt.Sprintf("failed to load metadata: %v", err))
|
item.remove(fmt.Sprintf("failed to load metadata: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get size estimate (which is best we can do until Open() called)
|
// Get size estimate (which is best we can do until Open() called)
|
||||||
@ -361,6 +367,10 @@ func (item *Item) _dirty() {
|
|||||||
item.info.ModTime = time.Now()
|
item.info.ModTime = time.Now()
|
||||||
item.info.ATime = item.info.ModTime
|
item.info.ATime = item.info.ModTime
|
||||||
item.metaDirty = true
|
item.metaDirty = true
|
||||||
|
if !item.modified {
|
||||||
|
item.modified = true
|
||||||
|
go item.c.writeback.cancelUpload(item)
|
||||||
|
}
|
||||||
if !item.info.Dirty {
|
if !item.info.Dirty {
|
||||||
item.info.Dirty = true
|
item.info.Dirty = true
|
||||||
err := item._save()
|
err := item._save()
|
||||||
@ -410,6 +420,7 @@ func (item *Item) Open(o fs.Object) (err error) {
|
|||||||
if item.fd != nil {
|
if item.fd != nil {
|
||||||
return errors.New("vfs cache item: internal error: didn't Close file")
|
return errors.New("vfs cache item: internal error: didn't Close file")
|
||||||
}
|
}
|
||||||
|
item.modified = false
|
||||||
|
|
||||||
fd, err := file.OpenFile(osPath, os.O_RDWR, 0600)
|
fd, err := file.OpenFile(osPath, os.O_RDWR, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -488,6 +499,7 @@ func (item *Item) _store(ctx context.Context, storeFn StoreFn) (err error) {
|
|||||||
fs.Errorf(item.name, "Failed to write metadata file: %v", err)
|
fs.Errorf(item.name, "Failed to write metadata file: %v", err)
|
||||||
}
|
}
|
||||||
if storeFn != nil && item.o != nil {
|
if storeFn != nil && item.o != nil {
|
||||||
|
fs.Debugf(item.name, "writeback object to VFS layer")
|
||||||
// Write the object back to the VFS layer as last
|
// Write the object back to the VFS layer as last
|
||||||
// thing we do with mutex unlocked
|
// thing we do with mutex unlocked
|
||||||
item.mu.Unlock()
|
item.mu.Unlock()
|
||||||
@ -595,10 +607,15 @@ func (item *Item) Close(storeFn StoreFn) (err error) {
|
|||||||
checkErr(item._store(context.Background(), storeFn))
|
checkErr(item._store(context.Background(), storeFn))
|
||||||
} else {
|
} else {
|
||||||
// asynchronous writeback
|
// asynchronous writeback
|
||||||
item.c.writeback.add(item, item.name, storeFn)
|
item.mu.Unlock()
|
||||||
|
item.c.writeback.add(item, item.name, item.modified, storeFn)
|
||||||
|
item.mu.Lock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// mark as not modified now we have uploaded or queued for upload
|
||||||
|
item.modified = false
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -711,7 +728,9 @@ func (item *Item) _removeMeta(reason string) {
|
|||||||
// call with lock held
|
// call with lock held
|
||||||
func (item *Item) _remove(reason string) (wasWriting bool) {
|
func (item *Item) _remove(reason string) (wasWriting bool) {
|
||||||
// Cancel writeback, if any
|
// Cancel writeback, if any
|
||||||
wasWriting = item.c.writeback.cancel(item)
|
item.mu.Unlock()
|
||||||
|
wasWriting = item.c.writeback.remove(item)
|
||||||
|
item.mu.Lock()
|
||||||
item.info.clean()
|
item.info.clean()
|
||||||
item.metaDirty = false
|
item.metaDirty = false
|
||||||
item._removeFile(reason)
|
item._removeFile(reason)
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rclone/rclone/fs"
|
"github.com/rclone/rclone/fs"
|
||||||
|
"github.com/rclone/rclone/fs/fserrors"
|
||||||
"github.com/rclone/rclone/vfs/vfscommon"
|
"github.com/rclone/rclone/vfs/vfscommon"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -56,7 +57,8 @@ type writeBackItem struct {
|
|||||||
index int // index into the priority queue for update
|
index int // index into the priority queue for update
|
||||||
item *Item // Item that needs writeback
|
item *Item // Item that needs writeback
|
||||||
expiry time.Time // When this expires we will write it back
|
expiry time.Time // When this expires we will write it back
|
||||||
uploading bool // If we are uploading the item
|
uploading bool // True if item is being processed by upload() method
|
||||||
|
onHeap bool // true if this item is on the items heap
|
||||||
cancel context.CancelFunc // To cancel the upload with
|
cancel context.CancelFunc // To cancel the upload with
|
||||||
done chan struct{} // closed when the cancellation completes
|
done chan struct{} // closed when the cancellation completes
|
||||||
storeFn StoreFn // To write the object back with
|
storeFn StoreFn // To write the object back with
|
||||||
@ -149,21 +151,29 @@ func (wb *writeBack) _delItem(wbItem *writeBackItem) {
|
|||||||
//
|
//
|
||||||
// call with the lock held
|
// call with the lock held
|
||||||
func (wb *writeBack) _popItem() (wbItem *writeBackItem) {
|
func (wb *writeBack) _popItem() (wbItem *writeBackItem) {
|
||||||
return heap.Pop(&wb.items).(*writeBackItem)
|
wbItem = heap.Pop(&wb.items).(*writeBackItem)
|
||||||
|
wbItem.onHeap = false
|
||||||
|
return wbItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// push a writeBackItem onto the items heap
|
// push a writeBackItem onto the items heap
|
||||||
//
|
//
|
||||||
// call with the lock held
|
// call with the lock held
|
||||||
func (wb *writeBack) _pushItem(wbItem *writeBackItem) {
|
func (wb *writeBack) _pushItem(wbItem *writeBackItem) {
|
||||||
heap.Push(&wb.items, wbItem)
|
if !wbItem.onHeap {
|
||||||
|
heap.Push(&wb.items, wbItem)
|
||||||
|
wbItem.onHeap = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove a writeBackItem from the items heap
|
// remove a writeBackItem from the items heap
|
||||||
//
|
//
|
||||||
// call with the lock held
|
// call with the lock held
|
||||||
func (wb *writeBack) _removeItem(wbItem *writeBackItem) {
|
func (wb *writeBack) _removeItem(wbItem *writeBackItem) {
|
||||||
heap.Remove(&wb.items, wbItem.index)
|
if wbItem.onHeap {
|
||||||
|
heap.Remove(&wb.items, wbItem.index)
|
||||||
|
wbItem.onHeap = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// peek the oldest writeBackItem - may be nil
|
// peek the oldest writeBackItem - may be nil
|
||||||
@ -191,8 +201,10 @@ func (wb *writeBack) _resetTimer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add adds an item to the writeback queue or resets its timer if it
|
// add adds an item to the writeback queue or resets its timer if it
|
||||||
// is already there
|
// is already there.
|
||||||
func (wb *writeBack) add(item *Item, name string, storeFn StoreFn) {
|
//
|
||||||
|
// if modified is false then it it doesn't a pending upload
|
||||||
|
func (wb *writeBack) add(item *Item, name string, modified bool, storeFn StoreFn) {
|
||||||
wb.mu.Lock()
|
wb.mu.Lock()
|
||||||
defer wb.mu.Unlock()
|
defer wb.mu.Unlock()
|
||||||
|
|
||||||
@ -200,7 +212,7 @@ func (wb *writeBack) add(item *Item, name string, storeFn StoreFn) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
wbItem = wb._newItem(item, name)
|
wbItem = wb._newItem(item, name)
|
||||||
} else {
|
} else {
|
||||||
if wbItem.uploading {
|
if wbItem.uploading && modified {
|
||||||
// We are uploading already so cancel the upload
|
// We are uploading already so cancel the upload
|
||||||
wb._cancelUpload(wbItem)
|
wb._cancelUpload(wbItem)
|
||||||
}
|
}
|
||||||
@ -211,21 +223,21 @@ func (wb *writeBack) add(item *Item, name string, storeFn StoreFn) {
|
|||||||
wb._resetTimer()
|
wb._resetTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel a writeback if there is one
|
// Call when a file is removed. This cancels a writeback if there is
|
||||||
func (wb *writeBack) cancel(item *Item) (found bool) {
|
// one and doesn't return the item to the queue.
|
||||||
|
func (wb *writeBack) remove(item *Item) (found bool) {
|
||||||
wb.mu.Lock()
|
wb.mu.Lock()
|
||||||
defer wb.mu.Unlock()
|
defer wb.mu.Unlock()
|
||||||
|
|
||||||
wbItem, found := wb.lookup[item]
|
wbItem, found := wb.lookup[item]
|
||||||
if found {
|
if found {
|
||||||
fs.Debugf(wbItem.name, "vfs cache: cancelling writeback")
|
fs.Debugf(wbItem.name, "vfs cache: cancelling writeback (uploading %v) %p item %p", wbItem.uploading, wbItem, wbItem.item)
|
||||||
if wbItem.uploading {
|
if wbItem.uploading {
|
||||||
// We are uploading already so cancel the upload
|
// We are uploading already so cancel the upload
|
||||||
wb._cancelUpload(wbItem)
|
wb._cancelUpload(wbItem)
|
||||||
} else {
|
|
||||||
// Remove the item from the heap
|
|
||||||
wb._removeItem(wbItem)
|
|
||||||
}
|
}
|
||||||
|
// Remove the item from the heap
|
||||||
|
wb._removeItem(wbItem)
|
||||||
// Remove the item from the lookup map
|
// Remove the item from the lookup map
|
||||||
wb._delItem(wbItem)
|
wb._delItem(wbItem)
|
||||||
}
|
}
|
||||||
@ -247,6 +259,8 @@ func (wb *writeBack) _kickUploader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// upload the item - called as a goroutine
|
// upload the item - called as a goroutine
|
||||||
|
//
|
||||||
|
// uploading will have been incremented here already
|
||||||
func (wb *writeBack) upload(ctx context.Context, wbItem *writeBackItem) {
|
func (wb *writeBack) upload(ctx context.Context, wbItem *writeBackItem) {
|
||||||
wb.mu.Lock()
|
wb.mu.Lock()
|
||||||
defer wb.mu.Unlock()
|
defer wb.mu.Unlock()
|
||||||
@ -258,10 +272,10 @@ func (wb *writeBack) upload(ctx context.Context, wbItem *writeBackItem) {
|
|||||||
wb.mu.Lock()
|
wb.mu.Lock()
|
||||||
|
|
||||||
wbItem.cancel() // cancel context to release resources since store done
|
wbItem.cancel() // cancel context to release resources since store done
|
||||||
if wbItem.uploading {
|
|
||||||
wbItem.uploading = false
|
fs.Debugf(wbItem.name, "uploading = false %p item %p", wbItem, wbItem.item)
|
||||||
wb.uploads--
|
wbItem.uploading = false
|
||||||
}
|
wb.uploads--
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// FIXME should this have a max number of transfer attempts?
|
// FIXME should this have a max number of transfer attempts?
|
||||||
@ -269,7 +283,13 @@ func (wb *writeBack) upload(ctx context.Context, wbItem *writeBackItem) {
|
|||||||
if wbItem.delay > maxUploadDelay {
|
if wbItem.delay > maxUploadDelay {
|
||||||
wbItem.delay = maxUploadDelay
|
wbItem.delay = maxUploadDelay
|
||||||
}
|
}
|
||||||
fs.Errorf(wbItem.name, "vfs cache: failed to upload try #%d, will retry in %v: %v", wbItem.tries, wbItem.delay, err)
|
if _, uerr := fserrors.Cause(err); uerr == context.Canceled {
|
||||||
|
fs.Infof(wbItem.name, "vfs cache: upload canceled sucessfully")
|
||||||
|
// Upload was cancelled so reset timer
|
||||||
|
wbItem.delay = wb.opt.WriteBack
|
||||||
|
} else {
|
||||||
|
fs.Errorf(wbItem.name, "vfs cache: failed to upload try #%d, will retry in %v: %v", wbItem.tries, wbItem.delay, err)
|
||||||
|
}
|
||||||
// push the item back on the queue for retry
|
// push the item back on the queue for retry
|
||||||
wb._pushItem(wbItem)
|
wb._pushItem(wbItem)
|
||||||
wb.items._update(wbItem, time.Now().Add(wbItem.delay))
|
wb.items._update(wbItem, time.Now().Add(wbItem.delay))
|
||||||
@ -282,26 +302,42 @@ func (wb *writeBack) upload(ctx context.Context, wbItem *writeBackItem) {
|
|||||||
close(wbItem.done)
|
close(wbItem.done)
|
||||||
}
|
}
|
||||||
|
|
||||||
// cancel the upload
|
// cancel the upload - the item should be on the heap after this returns
|
||||||
//
|
//
|
||||||
// call with lock held
|
// call with lock held
|
||||||
func (wb *writeBack) _cancelUpload(wbItem *writeBackItem) {
|
func (wb *writeBack) _cancelUpload(wbItem *writeBackItem) {
|
||||||
if !wbItem.uploading {
|
if !wbItem.uploading {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fs.Debugf(wbItem.name, "vfs cache: cancelling upload")
|
fs.Infof(wbItem.name, "vfs cache: cancelling upload")
|
||||||
if wbItem.cancel != nil {
|
if wbItem.cancel != nil {
|
||||||
// Cancel the upload - this may or may not be effective
|
// Cancel the upload - this may or may not be effective
|
||||||
wbItem.cancel()
|
wbItem.cancel()
|
||||||
// wait for the uploader to finish
|
// wait for the uploader to finish
|
||||||
|
//
|
||||||
|
// we need to wait without the lock otherwise the
|
||||||
|
// background part will never run.
|
||||||
|
wb.mu.Unlock()
|
||||||
<-wbItem.done
|
<-wbItem.done
|
||||||
}
|
wb.mu.Lock()
|
||||||
if wbItem.uploading {
|
|
||||||
wbItem.uploading = false
|
|
||||||
wb.uploads--
|
|
||||||
}
|
}
|
||||||
// uploading items are not on the heap so add them back
|
// uploading items are not on the heap so add them back
|
||||||
wb._pushItem(wbItem)
|
wb._pushItem(wbItem)
|
||||||
|
fs.Infof(wbItem.name, "vfs cache: cancelled upload")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancelUpload cancels the upload of the item if there is one in progress
|
||||||
|
//
|
||||||
|
// it returns true if there was an upload in progress
|
||||||
|
func (wb *writeBack) cancelUpload(item *Item) bool {
|
||||||
|
wb.mu.Lock()
|
||||||
|
defer wb.mu.Unlock()
|
||||||
|
wbItem, ok := wb.lookup[item]
|
||||||
|
if !ok || !wbItem.uploading {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
wb._cancelUpload(wbItem)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// this uploads as many items as possible
|
// this uploads as many items as possible
|
||||||
@ -320,6 +356,7 @@ func (wb *writeBack) processItems(ctx context.Context) {
|
|||||||
resetTimer = true
|
resetTimer = true
|
||||||
// Pop the item, mark as uploading and start the uploader
|
// Pop the item, mark as uploading and start the uploader
|
||||||
wbItem = wb._popItem()
|
wbItem = wb._popItem()
|
||||||
|
fs.Debugf(wbItem.name, "uploading = true %p item %p", wbItem, wbItem.item)
|
||||||
wbItem.uploading = true
|
wbItem.uploading = true
|
||||||
wb.uploads++
|
wb.uploads++
|
||||||
newCtx, cancel := context.WithCancel(ctx)
|
newCtx, cancel := context.WithCancel(ctx)
|
||||||
|
Loading…
Reference in New Issue
Block a user