github.com/creachadair/ffs@v0.17.3/storage/wbstore/wrapper.go (about)

     1  // Copyright 2024 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
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"iter"
    21  	"log"
    22  	"time"
    23  
    24  	"github.com/creachadair/ffs/blob"
    25  	"github.com/creachadair/mds/mapset"
    26  	"github.com/creachadair/msync/trigger"
    27  	"github.com/creachadair/taskgroup"
    28  )
    29  
    30  // A kvWrapper implements the [blob.KV] interface, and manages the forwarding
    31  // of cached Put requests to an underlying KV.
    32  type kvWrapper struct {
    33  	base blob.KV
    34  	buf  blob.KV
    35  
    36  	// The background writer waits on nempty when it finds no blobs to push.
    37  	nempty trigger.Cond
    38  }
    39  
    40  // run implements the backround writer. It runs until ctx terminates or until
    41  // it receives an unrecoverable error.
    42  func (w *kvWrapper) run(ctx context.Context) {
    43  	g, run := taskgroup.New(nil).Limit(32)
    44  	for {
    45  		// Check for cancellation.
    46  		select {
    47  		case <-ctx.Done():
    48  			return // normal shutdown
    49  		case <-w.nempty.Ready():
    50  		}
    51  
    52  		for key, err := range w.buf.List(ctx, "") {
    53  			if err != nil {
    54  				log.Printf("DEBUG :: error scanning buffer: %v", err)
    55  				break
    56  			}
    57  			if ctx.Err() != nil {
    58  				break
    59  			}
    60  			run(func() error {
    61  				// Read the blob and forward it to the base store, then delete it.
    62  				// Because the buffer contains only non-replacement blobs, it is
    63  				// safe to delete the blob even if another copy was written while
    64  				// we worked, since the content will be the same.  If Get or Delete
    65  				// fails, it means someone deleted the key before us. That's fine.
    66  
    67  				data, err := w.buf.Get(ctx, key)
    68  				if blob.IsKeyNotFound(err) {
    69  					return nil
    70  				} else if err != nil {
    71  					return err
    72  				}
    73  
    74  				// An individual write should not be allowed to stall for too long.
    75  				rtctx, cancel := context.WithTimeout(ctx, 20*time.Second)
    76  				defer cancel()
    77  				perr := w.base.Put(rtctx, blob.PutOptions{
    78  					Key:     key,
    79  					Data:    data,
    80  					Replace: false,
    81  				})
    82  				if perr == nil || blob.IsKeyExists(perr) {
    83  					// OK writeback succeeded, drop this blob from the buffer.
    84  					if err := w.buf.Delete(ctx, key); err != nil && !blob.IsKeyNotFound(err) {
    85  						return err
    86  					}
    87  					return nil
    88  				}
    89  				return perr
    90  			})
    91  		}
    92  		if err := g.Wait(); err != nil {
    93  			log.Printf("DEBUG :: error in writeback: %v", err)
    94  		} else if n, err := w.buf.Len(ctx); err == nil && n == 0 {
    95  			w.nempty.Reset()
    96  		}
    97  	}
    98  }
    99  
   100  // Get implements part of [blob.KV]. If key is in the write-behind store, its
   101  // value there is returned; otherwise it is fetched from the base store.
   102  func (w *kvWrapper) Get(ctx context.Context, key string) ([]byte, error) {
   103  	// Fetch from the buffer and the base store concurrently.  A hit in the base
   104  	// store takes precedence, since that reflects ground truth; but if that
   105  	// fails we will fall back to the buffered value.
   106  	ctx, cancel := context.WithCancel(ctx)
   107  	defer cancel()
   108  	buf := taskgroup.Call(func() ([]byte, error) {
   109  		return w.buf.Get(ctx, key)
   110  	})
   111  	base, err := w.base.Get(ctx, key)
   112  	if err != nil {
   113  		return buf.Wait().Get()
   114  	}
   115  	return base, nil
   116  }
   117  
   118  // Has implements part of [blob.KV].
   119  func (w *kvWrapper) Has(ctx context.Context, keys ...string) (blob.KeySet, error) {
   120  	// Look up keys in the buffer first. It is possible we may have some there
   121  	// that are not yet written back. Do this first so that if a writeback
   122  	// completes while we're checking the base store, we will still have a
   123  	// coherent value.
   124  	want := mapset.New(keys...)
   125  	have, err := w.buf.Has(ctx, want.Slice()...)
   126  	if err != nil {
   127  		return nil, fmt.Errorf("buffer stat: %w", err)
   128  	}
   129  	if have.Equals(want) {
   130  		return have, nil // we found everything
   131  	}
   132  
   133  	// Check for any keys we did not find in the buffer, in the base.
   134  	want.RemoveAll(have)
   135  	base, err := w.base.Has(ctx, want.Slice()...)
   136  	if err != nil {
   137  		return nil, fmt.Errorf("base stat: %w", err)
   138  	}
   139  	have.AddAll(base)
   140  	return have, nil
   141  }
   142  
   143  // Delete implements part of [blob.KV]. The key is deleted from both the buffer
   144  // and the base store, and succeeds as long as either of those operations
   145  // succeeds.
   146  func (w *kvWrapper) Delete(ctx context.Context, key string) error {
   147  	cerr := w.buf.Delete(ctx, key)
   148  	berr := w.base.Delete(ctx, key)
   149  	if cerr != nil && berr != nil {
   150  		return berr
   151  	}
   152  	return nil
   153  }
   154  
   155  // Put implements part of [blob.KV]. It delegates to the base store directly
   156  // for writes that request replacement; otherwise it stores the blob into the
   157  // buffer for writeback.
   158  func (w *kvWrapper) Put(ctx context.Context, opts blob.PutOptions) error {
   159  	if opts.Replace {
   160  		// Don't buffer writes that request replacement.
   161  		return w.base.Put(ctx, opts)
   162  	}
   163  
   164  	// To guarantee correct semantics we should do an occurs check here for the
   165  	// key in the base store. However, that is also expensive and reduces the
   166  	// value of the write-behind quite a bit. So we optimistically queue the
   167  	// write and report success (even though maybe we shouldn't). This will not
   168  	// write invalid data -- if the write-behind fails it means there is already
   169  	// a value in the base that we can linearize before this write. The only
   170  	// consequence is that this Put will not report that to the caller.
   171  	if err := w.buf.Put(ctx, opts); err != nil {
   172  		return err
   173  	}
   174  	w.nempty.Set()
   175  	return nil
   176  }
   177  
   178  // bufferKeysBetween iterates the keys in the buffer that are greater than or
   179  // equal to notBefore and less than notAfter.
   180  func (w *kvWrapper) bufferKeysBetween(ctx context.Context, notBefore, notAfter string) iter.Seq2[string, error] {
   181  	return func(yield func(string, error) bool) {
   182  		for key, err := range w.buf.List(ctx, notBefore) {
   183  			if err != nil {
   184  				yield("", err)
   185  				return
   186  			}
   187  			if key >= notAfter {
   188  				return
   189  			}
   190  			if !yield(key, nil) {
   191  				return
   192  			}
   193  		}
   194  	}
   195  }
   196  
   197  // Len implements part of [blob.KV]. It merges contents from the buffer that
   198  // are not listed in the underlying store, so that the reported length reflects
   199  // the total number of unique keys across both the buffer and the base store.
   200  func (w *kvWrapper) Len(ctx context.Context) (int64, error) {
   201  	var bufKeys mapset.Set[string]
   202  	for key, err := range w.buf.List(ctx, "") {
   203  		if err != nil {
   204  			return 0, err
   205  		}
   206  		bufKeys.Add(key)
   207  	}
   208  
   209  	numKeys := int64(bufKeys.Len())
   210  	for key, err := range w.base.List(ctx, "") {
   211  		if err != nil {
   212  			return 0, err
   213  		}
   214  		if !bufKeys.Has(key) {
   215  			numKeys++
   216  		}
   217  	}
   218  	return numKeys, nil
   219  }
   220  
   221  // List implements part of [blob.KV]. It merges contents from the buffer that
   222  // are not listed in the underlying store, so that the keys reported include
   223  // those that have not yet been written back.
   224  func (w *kvWrapper) List(ctx context.Context, start string) iter.Seq2[string, error] {
   225  	return func(yield func(string, error) bool) {
   226  		prev := start
   227  		for key, err := range w.base.List(ctx, start) {
   228  			if err != nil {
   229  				yield("", err)
   230  				return
   231  			}
   232  			for fill, err := range w.bufferKeysBetween(ctx, prev, key) {
   233  				if err != nil {
   234  					yield("", err)
   235  					return
   236  				}
   237  				if fill == prev {
   238  					continue
   239  				}
   240  				if !yield(fill, nil) {
   241  					return
   242  				}
   243  			}
   244  			if !yield(key, nil) {
   245  				return
   246  			}
   247  			prev = key
   248  		}
   249  
   250  		// Now ship any keys left in the buffer after the last key we sent.
   251  		for key, err := range w.buf.List(ctx, prev) {
   252  			if err != nil {
   253  				yield("", err)
   254  				return
   255  			}
   256  			if prev == key {
   257  				continue // we already shipped this key
   258  			}
   259  			if !yield(key, nil) {
   260  				return
   261  			}
   262  		}
   263  	}
   264  }