github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/vfs/vfscache/writeback/writeback.go (about)

     1  // Package writeback keeps track of the files which need to be written
     2  // back to storage
     3  package writeback
     4  
     5  import (
     6  	"container/heap"
     7  	"context"
     8  	"errors"
     9  	"sync"
    10  	"sync/atomic"
    11  	"time"
    12  
    13  	"github.com/rclone/rclone/fs"
    14  	"github.com/rclone/rclone/vfs/vfscommon"
    15  )
    16  
    17  const (
    18  	maxUploadDelay = 5 * time.Minute // max delay between upload attempts
    19  )
    20  
    21  // PutFn is the interface that item provides to store the data
    22  type PutFn func(context.Context) error
    23  
    24  // Handle is returned for callers to keep track of writeback items
    25  type Handle uint64
    26  
    27  // WriteBack keeps track of the items which need to be written back to the disk at some point
    28  type WriteBack struct {
    29  	// read and written with atomic, must be 64-bit aligned
    30  	id Handle // id of the last writeBackItem created
    31  
    32  	ctx     context.Context
    33  	mu      sync.Mutex
    34  	items   writeBackItems            // priority queue of *writeBackItem - writeBackItems are in here while awaiting transfer only
    35  	lookup  map[Handle]*writeBackItem // for getting a *writeBackItem from a Handle - writeBackItems are in here until cancelled
    36  	opt     *vfscommon.Options        // VFS options
    37  	timer   *time.Timer               // next scheduled time for the uploader
    38  	expiry  time.Time                 // time the next item expires or IsZero
    39  	uploads int                       // number of uploads in progress
    40  }
    41  
    42  // New make a new WriteBack
    43  //
    44  // cancel the context to stop the background processing
    45  func New(ctx context.Context, opt *vfscommon.Options) *WriteBack {
    46  	wb := &WriteBack{
    47  		ctx:    ctx,
    48  		items:  writeBackItems{},
    49  		lookup: make(map[Handle]*writeBackItem),
    50  		opt:    opt,
    51  	}
    52  	heap.Init(&wb.items)
    53  	return wb
    54  }
    55  
    56  // writeBackItem stores an Item awaiting writeback
    57  //
    58  // These are stored on the items heap when awaiting transfer but
    59  // removed from the items heap when transferring. They remain in the
    60  // lookup map until cancelled.
    61  //
    62  // writeBack.mu must be held to manipulate this
    63  type writeBackItem struct {
    64  	name      string             // name of the item so we don't have to read it from item
    65  	id        Handle             // id of the item
    66  	index     int                // index into the priority queue for update
    67  	expiry    time.Time          // When this expires we will write it back
    68  	uploading bool               // True if item is being processed by upload() method
    69  	onHeap    bool               // true if this item is on the items heap
    70  	cancel    context.CancelFunc // To cancel the upload with
    71  	done      chan struct{}      // closed when the cancellation completes
    72  	putFn     PutFn              // To write the object data
    73  	tries     int                // number of times we have tried to upload
    74  	delay     time.Duration      // delay between upload attempts
    75  }
    76  
    77  // A writeBackItems implements a priority queue by implementing
    78  // heap.Interface and holds writeBackItems.
    79  type writeBackItems []*writeBackItem
    80  
    81  func (ws writeBackItems) Len() int { return len(ws) }
    82  
    83  func (ws writeBackItems) Less(i, j int) bool {
    84  	a, b := ws[i], ws[j]
    85  	// If times are equal then use ID to disambiguate
    86  	if a.expiry.Equal(b.expiry) {
    87  		return a.id < b.id
    88  	}
    89  	return a.expiry.Before(b.expiry)
    90  }
    91  
    92  func (ws writeBackItems) Swap(i, j int) {
    93  	ws[i], ws[j] = ws[j], ws[i]
    94  	ws[i].index = i
    95  	ws[j].index = j
    96  }
    97  
    98  func (ws *writeBackItems) Push(x interface{}) {
    99  	n := len(*ws)
   100  	item := x.(*writeBackItem)
   101  	item.index = n
   102  	*ws = append(*ws, item)
   103  }
   104  
   105  func (ws *writeBackItems) Pop() interface{} {
   106  	old := *ws
   107  	n := len(old)
   108  	item := old[n-1]
   109  	old[n-1] = nil  // avoid memory leak
   110  	item.index = -1 // for safety
   111  	*ws = old[0 : n-1]
   112  	return item
   113  }
   114  
   115  // update modifies the expiry of an Item in the queue.
   116  //
   117  // call with lock held
   118  func (ws *writeBackItems) _update(item *writeBackItem, expiry time.Time) {
   119  	item.expiry = expiry
   120  	heap.Fix(ws, item.index)
   121  }
   122  
   123  // return a new expiry time based from now until the WriteBack timeout
   124  //
   125  // call with lock held
   126  func (wb *WriteBack) _newExpiry() time.Time {
   127  	expiry := time.Now()
   128  	if wb.opt.WriteBack > 0 {
   129  		expiry = expiry.Add(wb.opt.WriteBack)
   130  	}
   131  	// expiry = expiry.Round(time.Millisecond)
   132  	return expiry
   133  }
   134  
   135  // make a new writeBackItem
   136  //
   137  // call with the lock held
   138  func (wb *WriteBack) _newItem(id Handle, name string) *writeBackItem {
   139  	wb.SetID(&id)
   140  	wbItem := &writeBackItem{
   141  		name:   name,
   142  		expiry: wb._newExpiry(),
   143  		delay:  wb.opt.WriteBack,
   144  		id:     id,
   145  	}
   146  	wb._addItem(wbItem)
   147  	wb._pushItem(wbItem)
   148  	return wbItem
   149  }
   150  
   151  // add a writeBackItem to the lookup map
   152  //
   153  // call with the lock held
   154  func (wb *WriteBack) _addItem(wbItem *writeBackItem) {
   155  	wb.lookup[wbItem.id] = wbItem
   156  }
   157  
   158  // delete a writeBackItem from the lookup map
   159  //
   160  // call with the lock held
   161  func (wb *WriteBack) _delItem(wbItem *writeBackItem) {
   162  	delete(wb.lookup, wbItem.id)
   163  }
   164  
   165  // pop a writeBackItem from the items heap
   166  //
   167  // call with the lock held
   168  func (wb *WriteBack) _popItem() (wbItem *writeBackItem) {
   169  	wbItem = heap.Pop(&wb.items).(*writeBackItem)
   170  	wbItem.onHeap = false
   171  	return wbItem
   172  }
   173  
   174  // push a writeBackItem onto the items heap
   175  //
   176  // call with the lock held
   177  func (wb *WriteBack) _pushItem(wbItem *writeBackItem) {
   178  	if !wbItem.onHeap {
   179  		heap.Push(&wb.items, wbItem)
   180  		wbItem.onHeap = true
   181  	}
   182  }
   183  
   184  // remove a writeBackItem from the items heap
   185  //
   186  // call with the lock held
   187  func (wb *WriteBack) _removeItem(wbItem *writeBackItem) {
   188  	if wbItem.onHeap {
   189  		heap.Remove(&wb.items, wbItem.index)
   190  		wbItem.onHeap = false
   191  	}
   192  }
   193  
   194  // peek the oldest writeBackItem - may be nil
   195  //
   196  // call with the lock held
   197  func (wb *WriteBack) _peekItem() (wbItem *writeBackItem) {
   198  	if len(wb.items) == 0 {
   199  		return nil
   200  	}
   201  	return wb.items[0]
   202  }
   203  
   204  // stop the timer which runs the expiries
   205  func (wb *WriteBack) _stopTimer() {
   206  	if wb.expiry.IsZero() {
   207  		return
   208  	}
   209  	wb.expiry = time.Time{}
   210  	// fs.Debugf(nil, "resetTimer STOP")
   211  	if wb.timer != nil {
   212  		wb.timer.Stop()
   213  		wb.timer = nil
   214  	}
   215  }
   216  
   217  // reset the timer which runs the expiries
   218  func (wb *WriteBack) _resetTimer() {
   219  	wbItem := wb._peekItem()
   220  	if wbItem == nil {
   221  		wb._stopTimer()
   222  	} else {
   223  		if wb.expiry.Equal(wbItem.expiry) {
   224  			return
   225  		}
   226  		wb.expiry = wbItem.expiry
   227  		dt := time.Until(wbItem.expiry)
   228  		if dt < 0 {
   229  			dt = 0
   230  		}
   231  		// fs.Debugf(nil, "resetTimer dt=%v", dt)
   232  		if wb.timer != nil {
   233  			wb.timer.Stop()
   234  		}
   235  		wb.timer = time.AfterFunc(dt, func() {
   236  			wb.processItems(wb.ctx)
   237  		})
   238  	}
   239  }
   240  
   241  // SetID sets the Handle pointed to if it is non zero to the next
   242  // handle.
   243  func (wb *WriteBack) SetID(pid *Handle) {
   244  	if *pid == 0 {
   245  		*pid = Handle(atomic.AddUint64((*uint64)(&wb.id), 1))
   246  	}
   247  }
   248  
   249  // Add adds an item to the writeback queue or resets its timer if it
   250  // is already there.
   251  //
   252  // If id is 0 then a new item will always be created and the new
   253  // Handle will be returned.
   254  //
   255  // Use SetID to create Handles in advance of calling Add.
   256  //
   257  // If modified is false then it it doesn't cancel a pending upload if
   258  // there is one as there is no need.
   259  func (wb *WriteBack) Add(id Handle, name string, modified bool, putFn PutFn) Handle {
   260  	wb.mu.Lock()
   261  	defer wb.mu.Unlock()
   262  
   263  	wbItem, ok := wb.lookup[id]
   264  	if !ok {
   265  		wbItem = wb._newItem(id, name)
   266  	} else {
   267  		if wbItem.uploading && modified {
   268  			// We are uploading already so cancel the upload
   269  			wb._cancelUpload(wbItem)
   270  		}
   271  		// Kick the timer on
   272  		wb.items._update(wbItem, wb._newExpiry())
   273  	}
   274  	wbItem.putFn = putFn
   275  	wb._resetTimer()
   276  	return wbItem.id
   277  }
   278  
   279  // _remove should be called when a file should be removed from the
   280  // writeback queue. This cancels a writeback if there is one and
   281  // doesn't return the item to the queue.
   282  //
   283  // This should be called with the lock held
   284  func (wb *WriteBack) _remove(id Handle) (found bool) {
   285  	wbItem, found := wb.lookup[id]
   286  	if found {
   287  		fs.Debugf(wbItem.name, "vfs cache: cancelling writeback (uploading %v) %p item %d", wbItem.uploading, wbItem, wbItem.id)
   288  		if wbItem.uploading {
   289  			// We are uploading already so cancel the upload
   290  			wb._cancelUpload(wbItem)
   291  		}
   292  		// Remove the item from the heap
   293  		wb._removeItem(wbItem)
   294  		// Remove the item from the lookup map
   295  		wb._delItem(wbItem)
   296  	}
   297  	wb._resetTimer()
   298  	return found
   299  }
   300  
   301  // Remove should be called when a file should be removed from the
   302  // writeback queue. This cancels a writeback if there is one and
   303  // doesn't return the item to the queue.
   304  func (wb *WriteBack) Remove(id Handle) (found bool) {
   305  	wb.mu.Lock()
   306  	defer wb.mu.Unlock()
   307  
   308  	return wb._remove(id)
   309  }
   310  
   311  // Rename should be called when a file might be uploading and it gains
   312  // a new name. This will cancel the upload and put it back in the
   313  // queue.
   314  func (wb *WriteBack) Rename(id Handle, name string) {
   315  	wb.mu.Lock()
   316  	defer wb.mu.Unlock()
   317  
   318  	wbItem, ok := wb.lookup[id]
   319  	if !ok {
   320  		return
   321  	}
   322  	if wbItem.uploading {
   323  		// We are uploading already so cancel the upload
   324  		wb._cancelUpload(wbItem)
   325  	}
   326  
   327  	// Check to see if there are any uploads with the existing
   328  	// name and remove them
   329  	for existingID, existingItem := range wb.lookup {
   330  		if existingID != id && existingItem.name == name {
   331  			wb._remove(existingID)
   332  		}
   333  	}
   334  
   335  	wbItem.name = name
   336  	// Kick the timer on
   337  	wb.items._update(wbItem, wb._newExpiry())
   338  
   339  	wb._resetTimer()
   340  }
   341  
   342  // upload the item - called as a goroutine
   343  //
   344  // uploading will have been incremented here already
   345  func (wb *WriteBack) upload(ctx context.Context, wbItem *writeBackItem) {
   346  	wb.mu.Lock()
   347  	defer wb.mu.Unlock()
   348  	putFn := wbItem.putFn
   349  	wbItem.tries++
   350  
   351  	fs.Debugf(wbItem.name, "vfs cache: starting upload")
   352  
   353  	wb.mu.Unlock()
   354  	err := putFn(ctx)
   355  	wb.mu.Lock()
   356  
   357  	wbItem.cancel() // cancel context to release resources since store done
   358  
   359  	wbItem.uploading = false
   360  	wb.uploads--
   361  
   362  	if err != nil {
   363  		// FIXME should this have a max number of transfer attempts?
   364  		wbItem.delay *= 2
   365  		if wbItem.delay > maxUploadDelay {
   366  			wbItem.delay = maxUploadDelay
   367  		}
   368  		if errors.Is(err, context.Canceled) {
   369  			fs.Infof(wbItem.name, "vfs cache: upload canceled")
   370  			// Upload was cancelled so reset timer
   371  			wbItem.delay = wb.opt.WriteBack
   372  		} else {
   373  			fs.Errorf(wbItem.name, "vfs cache: failed to upload try #%d, will retry in %v: %v", wbItem.tries, wbItem.delay, err)
   374  		}
   375  		// push the item back on the queue for retry
   376  		wb._pushItem(wbItem)
   377  		wb.items._update(wbItem, time.Now().Add(wbItem.delay))
   378  	} else {
   379  		fs.Infof(wbItem.name, "vfs cache: upload succeeded try #%d", wbItem.tries)
   380  		// show that we are done with the item
   381  		wb._delItem(wbItem)
   382  	}
   383  	wb._resetTimer()
   384  	close(wbItem.done)
   385  }
   386  
   387  // cancel the upload - the item should be on the heap after this returns
   388  //
   389  // call with lock held
   390  func (wb *WriteBack) _cancelUpload(wbItem *writeBackItem) {
   391  	if !wbItem.uploading {
   392  		return
   393  	}
   394  	fs.Debugf(wbItem.name, "vfs cache: cancelling upload")
   395  	if wbItem.cancel != nil {
   396  		// Cancel the upload - this may or may not be effective
   397  		wbItem.cancel()
   398  		// wait for the uploader to finish
   399  		//
   400  		// we need to wait without the lock otherwise the
   401  		// background part will never run.
   402  		wb.mu.Unlock()
   403  		<-wbItem.done
   404  		wb.mu.Lock()
   405  	}
   406  	// uploading items are not on the heap so add them back
   407  	wb._pushItem(wbItem)
   408  	fs.Debugf(wbItem.name, "vfs cache: cancelled upload")
   409  }
   410  
   411  // cancelUpload cancels the upload of the item if there is one in progress
   412  //
   413  // it returns true if there was an upload in progress
   414  func (wb *WriteBack) cancelUpload(id Handle) bool {
   415  	wb.mu.Lock()
   416  	defer wb.mu.Unlock()
   417  	wbItem, ok := wb.lookup[id]
   418  	if !ok || !wbItem.uploading {
   419  		return false
   420  	}
   421  	wb._cancelUpload(wbItem)
   422  	return true
   423  }
   424  
   425  // this uploads as many items as possible
   426  func (wb *WriteBack) processItems(ctx context.Context) {
   427  	wb.mu.Lock()
   428  	defer wb.mu.Unlock()
   429  
   430  	if wb.ctx.Err() != nil {
   431  		return
   432  	}
   433  
   434  	resetTimer := true
   435  	for wbItem := wb._peekItem(); wbItem != nil && time.Until(wbItem.expiry) <= 0; wbItem = wb._peekItem() {
   436  		// If reached transfer limit don't restart the timer
   437  		if wb.uploads >= fs.GetConfig(context.TODO()).Transfers {
   438  			fs.Debugf(wbItem.name, "vfs cache: delaying writeback as --transfers exceeded")
   439  			resetTimer = false
   440  			break
   441  		}
   442  		// Pop the item, mark as uploading and start the uploader
   443  		wbItem = wb._popItem()
   444  		//fs.Debugf(wbItem.name, "uploading = true %p item %p", wbItem, wbItem.item)
   445  		wbItem.uploading = true
   446  		wb.uploads++
   447  		newCtx, cancel := context.WithCancel(ctx)
   448  		wbItem.cancel = cancel
   449  		wbItem.done = make(chan struct{})
   450  		go wb.upload(newCtx, wbItem)
   451  	}
   452  
   453  	if resetTimer {
   454  		wb._resetTimer()
   455  	} else {
   456  		wb._stopTimer()
   457  	}
   458  }
   459  
   460  // Stats return the number of uploads in progress and queued
   461  func (wb *WriteBack) Stats() (uploadsInProgress, uploadsQueued int) {
   462  	wb.mu.Lock()
   463  	defer wb.mu.Unlock()
   464  	return wb.uploads, len(wb.items)
   465  }