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  }