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 }