//go:build !plan9 && !js // +build !plan9,!js // Package kv provides key/value database. package kv import ( "context" "fmt" "os" "path/filepath" "strings" "sync" "time" "github.com/rclone/rclone/fs" "github.com/rclone/rclone/fs/config" "github.com/rclone/rclone/lib/encoder" "go.etcd.io/bbolt" ) const ( initTime = 24 * time.Hour // something reasonably long dbFileMode = 0600 dbDirMode = 0700 queueSize = 2 ) // DB represents a key-value database type DB struct { name string path string facility string refs int bolt *bbolt.DB mu sync.Mutex canWrite bool queue chan *request lockTime time.Duration idleTime time.Duration openTime time.Duration idleTimer *time.Timer lockTimer *time.Timer } var ( dbMap = map[string]*DB{} dbMut sync.Mutex atExit bool ) // Supported returns true on supported OSes func Supported() bool { return true } // makeName makes a store name func makeName(facility string, f fs.Fs) string { var name string if f != nil { name = f.Name() if idx := strings.Index(name, "{"); idx != -1 { name = name[:idx] } name = encoder.OS.FromStandardPath(name) name += "~" } return name + facility + ".bolt" } // Start a new key-value database func Start(ctx context.Context, facility string, f fs.Fs) (*DB, error) { dbMut.Lock() defer dbMut.Unlock() if db := lockedGet(facility, f); db != nil { return db, nil } dir := filepath.Join(config.GetCacheDir(), "kv") if err := os.MkdirAll(dir, dbDirMode); err != nil { return nil, err } name := makeName(facility, f) lockTime := fs.GetConfig(ctx).KvLockTime db := &DB{ name: name, path: filepath.Join(dir, name), facility: facility, refs: 1, lockTime: lockTime, idleTime: lockTime / 4, openTime: lockTime * 2, idleTimer: time.NewTimer(initTime), lockTimer: time.NewTimer(initTime), queue: make(chan *request, queueSize), } fi, err := os.Stat(db.path) if strings.HasSuffix(os.Args[0], ".test") || (err == nil && fi.Size() == 0) { _ = os.Remove(db.path) fs.Infof(db.name, "drop cache remaining after unit test") } if err = db.open(ctx, false); err != nil && err != ErrEmpty { return nil, fmt.Errorf("cannot open db: %s: %w", db.path, err) } dbMap[name] = db go db.loop() return db, nil } // Get returns database record for given filesystem and facility func Get(facility string, f fs.Fs) *DB { dbMut.Lock() defer dbMut.Unlock() return lockedGet(facility, f) } func lockedGet(facility string, f fs.Fs) *DB { name := makeName(facility, f) db := dbMap[name] if db != nil { db.mu.Lock() db.refs++ db.mu.Unlock() } return db } // Path returns database path func (db *DB) Path() string { return db.path } var modeNames = map[bool]string{false: "reading", true: "writing"} func (db *DB) open(ctx context.Context, forWrite bool) (err error) { if db.bolt != nil && (db.canWrite || !forWrite) { return nil } _ = db.close() db.canWrite = forWrite if !forWrite { // mitigate https://github.com/etcd-io/bbolt/issues/98 _, err = os.Stat(db.path) if os.IsNotExist(err) { return ErrEmpty } } opt := &bbolt.Options{ Timeout: db.openTime, ReadOnly: !forWrite, } openMode := modeNames[forWrite] startTime := time.Now() var bolt *bbolt.DB retry := 1 maxRetries := fs.GetConfig(ctx).LowLevelRetries for { bolt, err = bbolt.Open(db.path, dbFileMode, opt) if err == nil || retry >= maxRetries { break } fs.Debugf(db.name, "Retry #%d opening for %s: %v", retry, openMode, err) retry++ } if err != nil { return err } fs.Debugf(db.name, "Opened for %s in %v", openMode, time.Since(startTime)) _ = db.lockTimer.Reset(db.lockTime) _ = db.idleTimer.Reset(db.idleTime) db.bolt = bolt return nil } func (db *DB) close() (err error) { if db.bolt != nil { _ = db.lockTimer.Stop() _ = db.idleTimer.Stop() err = db.bolt.Close() db.bolt = nil fs.Debugf(db.name, "released") } return } // loop over database operations sequentially func (db *DB) loop() { ctx := context.Background() var req *request quit := false for !quit { select { case req = <-db.queue: if quit = req.handle(ctx, db); !quit { req.wg.Done() _ = db.idleTimer.Reset(db.idleTime) } case <-db.idleTimer.C: _ = db.close() case <-db.lockTimer.C: _ = db.close() } } db.queue = nil if !atExit { dbMut.Lock() delete(dbMap, db.name) dbMut.Unlock() } req.wg.Done() } // Do a key-value operation and return error when done func (db *DB) Do(write bool, op Op) error { if db == nil || db.queue == nil { return ErrInactive } r := &request{ op: op, wr: write, } r.wg.Add(1) db.queue <- r r.wg.Wait() return r.err } // request encapsulates a synchronous operation and its results type request struct { op Op wr bool err error wg sync.WaitGroup } // handle a key-value request with given DB // returns true as a signal to quit the loop func (r *request) handle(ctx context.Context, db *DB) bool { db.mu.Lock() defer db.mu.Unlock() if op, stop := r.op.(*opStop); stop { r.err = db.close() if op.remove { if err := os.Remove(db.path); !os.IsNotExist(err) { r.err = err } } db.refs-- return db.refs <= 0 } r.err = db.execute(ctx, r.op, r.wr) return false } // execute a key-value DB operation func (db *DB) execute(ctx context.Context, op Op, write bool) error { if err := db.open(ctx, write); err != nil { return err } if write { return db.bolt.Update(func(tx *bbolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(db.facility)) if err != nil || b == nil { return ErrEmpty } return op.Do(ctx, &bucketAdapter{b}) }) } return db.bolt.View(func(tx *bbolt.Tx) error { b := tx.Bucket([]byte(db.facility)) if b == nil { return ErrEmpty } return op.Do(ctx, &bucketAdapter{b}) }) } // bucketAdapter is a thin wrapper adapting kv.Bucket to bbolt.Bucket type bucketAdapter struct { *bbolt.Bucket } func (b *bucketAdapter) Cursor() Cursor { return b.Bucket.Cursor() } // Stop a database loop, optionally removing the file func (db *DB) Stop(remove bool) error { return db.Do(false, &opStop{remove: remove}) } // IsStopped returns true if db is already stopped func (db *DB) IsStopped() bool { return len(dbMap) == 0 } // opStop: close database and stop operation loop type opStop struct { remove bool } func (*opStop) Do(context.Context, Bucket) error { return nil } // Exit immediately stops all databases func Exit() { dbMut.Lock() atExit = true for _, s := range dbMap { s.refs = 0 _ = s.Stop(false) } dbMap = map[string]*DB{} atExit = false dbMut.Unlock() }