github.com/creachadair/ffs@v0.17.3/storage/wbstore/wbstore.go (about) 1 // Copyright 2019 Michael J. Fromberger. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package wbstore implements a wrapper for a [blob.Store] that caches 16 // non-replacement writes of in a buffer and pushes them to the base store 17 // concurrently in the background. 18 package wbstore 19 20 import ( 21 "context" 22 "errors" 23 24 "github.com/creachadair/ffs/blob" 25 "github.com/creachadair/ffs/storage/dbkey" 26 "github.com/creachadair/ffs/storage/monitor" 27 "github.com/creachadair/taskgroup" 28 ) 29 30 // Store implements the [blob.Store] interface by delegating to a base store. 31 // Non-replacement writes to [blob.KV] instances derived from the base store 32 // are buffered and written back to the underlying store by a background worker 33 // that runs concurrently with the store. 34 type Store struct { 35 *monitor.M[wbState, *kvWrapper] 36 37 writers *taskgroup.Group 38 stop context.CancelFunc 39 } 40 41 type wbState struct { 42 base blob.Store 43 buf blob.Store 44 } 45 46 // New constructs a [blob.Store] wrapper that delegates to base and uses buf as 47 // a local buffer store. New will panic if base == nil or buf == nil. The ctx 48 // value governs the operation of the background writer, which will run until 49 // the store is closed or ctx terminates. 50 func New(ctx context.Context, base, buf blob.Store) Store { 51 if base == nil { 52 panic("base is nil") 53 } else if buf == nil { 54 panic("buffer is nil") 55 } 56 57 wctx, cancel := context.WithCancel(ctx) 58 g := taskgroup.New(nil) 59 return Store{ 60 writers: g, 61 stop: cancel, 62 M: monitor.New(monitor.Config[wbState, *kvWrapper]{ 63 DB: wbState{base: base, buf: buf}, 64 NewKV: func(ctx context.Context, db wbState, pfx dbkey.Prefix, name string) (*kvWrapper, error) { 65 baseKV, err := db.base.KV(ctx, name) 66 if err != nil { 67 return nil, err 68 } 69 bufKV, err := db.buf.KV(ctx, name) 70 if err != nil { 71 return nil, err 72 } 73 74 // Each KV gets its own writeback worker. 75 w := &kvWrapper{base: baseKV, buf: bufKV} 76 w.nempty.Set() // prime 77 g.Run(func() { w.run(wctx) }) 78 return w, nil 79 }, 80 NewSub: func(ctx context.Context, db wbState, pfx dbkey.Prefix, name string) (wbState, error) { 81 baseSub, err := db.base.Sub(ctx, name) 82 if err != nil { 83 return wbState{}, err 84 } 85 bufSub, err := db.buf.Sub(ctx, name) 86 if err != nil { 87 return wbState{}, err 88 } 89 return wbState{base: baseSub, buf: bufSub}, nil 90 }, 91 })} 92 } 93 94 // Close implements the [blob.Closer] interface for s. 95 func (s Store) Close(ctx context.Context) error { 96 s.stop() 97 s.writers.Wait() 98 99 // N.B. Close the buffer first, since writes back depend on the base. 100 var bufErr, baseErr error 101 if c, ok := s.M.DB.buf.(blob.Closer); ok { 102 bufErr = c.Close(ctx) 103 } 104 if c, ok := s.M.DB.base.(blob.Closer); ok { 105 baseErr = c.Close(ctx) 106 } 107 return errors.Join(baseErr, bufErr) 108 } 109 110 // BufferLen reports the total number of keys buffered for writeback in the 111 // buffer storage of s. 112 func (s Store) BufferLen(ctx context.Context) (int64, error) { 113 var count int64 114 for kv := range s.M.AllKV() { 115 n, err := kv.buf.Len(ctx) 116 if err != nil { 117 return 0, err 118 } 119 count += n 120 } 121 return count, nil 122 }