github.com/creachadair/ffs@v0.17.3/storage/monitor/monitor.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 monitor implements common plumbing for implementations of the
    16  // [blob.Store] interface based on storage with a flat key space.
    17  //
    18  // # Overview
    19  //
    20  // The [M] type implements shared plumbing for the methods of a [blob.Store].
    21  // It is intended to be embedded in another type to provide the required
    22  // methods of the interface. The monitor is parameterized by a storage handle
    23  // (DB) and an implementation of the [blob.KV] interface using that storage.
    24  //
    25  // Keyspaces managed by the monitor are partitioned by adding a key prefix.
    26  // The prefix is derived by hashing the path of (sub)space and keyspace names
    27  // from the root of the store (see [dbkey]). This ensures the prefix for a
    28  // given path is stable without explicitly persisting the mapping of names.
    29  //
    30  // To construct an [M], the caller must provide, at minimum:
    31  //
    32  //   - A storage handle (typically a database or storage client).
    33  //   - An implementation of the [blob.KV] interface based on that storage.
    34  //   - A constructor to create new instances of that KV implementation.
    35  //
    36  // The caller may also optionally provide a constructor to derive new substore
    37  // instances. This is not necessary unless the storage handle requires state to
    38  // track its own subspace organization, and is typically omitted.
    39  package monitor
    40  
    41  import (
    42  	"context"
    43  	"iter"
    44  	"sync"
    45  
    46  	"github.com/creachadair/ffs/blob"
    47  	"github.com/creachadair/ffs/storage/dbkey"
    48  )
    49  
    50  // Config carries settings for construction of an [M].
    51  // At minimum, the DB and NewKV fields must be populated.
    52  type Config[DB any, KV blob.KV] struct {
    53  	// DB represents the initial state of the monitor.
    54  	// It must be safe to copy DB, so if the state contains locks or other
    55  	// values that cannot be safely copied, use a pointer type.
    56  	DB DB
    57  
    58  	// Prefix gives the initial storage prefix of the root.  Empty is a valid,
    59  	// safe default.
    60  	Prefix dbkey.Prefix
    61  
    62  	// NewKV construts a KV instance from the current state, where db is the
    63  	// store state, pfx the derived prefix for the new KV, and name is the name
    64  	// passed to the KV call.
    65  	NewKV func(ctx context.Context, db DB, pfx dbkey.Prefix, name string) (KV, error)
    66  
    67  	// NewSub constructs a sub-DB state from the current state, where db is the
    68  	// store state, pfx the derived prefix for the new subspace, and name is the
    69  	// name passed to the Sub call.  If NewSub is nil, the existing state is
    70  	// copied without change.
    71  	NewSub func(ctx context.Context, db DB, pfx dbkey.Prefix, name string) (DB, error)
    72  }
    73  
    74  // A M value manages keyspace and substore allocations for the specified
    75  // database and KV implementations. The resulting value implements [blob.Store].
    76  type M[DB any, KV blob.KV] struct {
    77  	DB     DB
    78  	prefix dbkey.Prefix
    79  	newKV  func(context.Context, DB, dbkey.Prefix, string) (KV, error)
    80  	newSub func(context.Context, DB, dbkey.Prefix, string) (DB, error)
    81  
    82  	μ    sync.Mutex
    83  	subs map[string]*M[DB, KV]
    84  	kvs  map[string]KV
    85  }
    86  
    87  // New constructs a new empty store using the specified database, prefix, and
    88  // KV constructor function. New will panic if cfg.NewKV is nil.
    89  func New[DB any, KV blob.KV](cfg Config[DB, KV]) *M[DB, KV] {
    90  	if cfg.NewKV == nil {
    91  		panic("KV constructor is nil")
    92  	}
    93  	if cfg.NewSub == nil {
    94  		cfg.NewSub = func(_ context.Context, old DB, _ dbkey.Prefix, _ string) (DB, error) {
    95  			return old, nil
    96  		}
    97  	}
    98  	return &M[DB, KV]{
    99  		DB:     cfg.DB,
   100  		prefix: cfg.Prefix,
   101  		newKV:  cfg.NewKV,
   102  		newSub: cfg.NewSub,
   103  		subs:   make(map[string]*M[DB, KV]),
   104  		kvs:    make(map[string]KV),
   105  	}
   106  }
   107  
   108  // KV implements a method of [blob.Store].  A successful result has concrete
   109  // type [KV]. Any error reported by this method is from the NewKV callback
   110  // provided at construction.
   111  func (d *M[DB, KV]) KV(ctx context.Context, name string) (blob.KV, error) {
   112  	d.μ.Lock()
   113  	defer d.μ.Unlock()
   114  
   115  	kv, ok := d.kvs[name]
   116  	if !ok {
   117  		var err error
   118  		kv, err = d.newKV(ctx, d.DB, d.prefix.Keyspace(name), name)
   119  		if err != nil {
   120  			return nil, err
   121  		}
   122  		d.kvs[name] = kv
   123  	}
   124  	return kv, nil
   125  }
   126  
   127  // CAS implements a method of [blob.Store]. This implementation uses the
   128  // default [blob.CASFromKV] construction.
   129  func (d *M[DB, KV]) CAS(ctx context.Context, name string) (blob.CAS, error) {
   130  	return blob.CASFromKVError(d.KV(ctx, name))
   131  }
   132  
   133  // Sub implements a method of [blob.Store].  Any error reported by this method
   134  // is from the NewSub callback provided at construction. If no such callback
   135  // was provided, it will always succeed.
   136  func (d *M[DB, KV]) Sub(ctx context.Context, name string) (blob.Store, error) {
   137  	d.μ.Lock()
   138  	defer d.μ.Unlock()
   139  
   140  	sub, ok := d.subs[name]
   141  	if !ok {
   142  		npfx := d.prefix.Sub(name)
   143  		ndb, err := d.newSub(ctx, d.DB, npfx, name)
   144  		if err != nil {
   145  			return nil, err
   146  		}
   147  		sub = New(Config[DB, KV]{
   148  			DB:     ndb,
   149  			Prefix: npfx,
   150  			NewKV:  d.newKV,
   151  			NewSub: d.newSub,
   152  		})
   153  		d.subs[name] = sub
   154  	}
   155  	return sub, nil
   156  }
   157  
   158  // AllKV implements an iterator over all the KV values defined by d.
   159  func (d *M[DB, KV]) AllKV() iter.Seq[KV] {
   160  	return func(yield func(KV) bool) {
   161  		d.μ.Lock()
   162  		defer d.μ.Unlock()
   163  
   164  		for _, sub := range d.subs {
   165  			for kv := range sub.AllKV() {
   166  				if !yield(kv) {
   167  					return
   168  				}
   169  			}
   170  		}
   171  		for _, kv := range d.kvs {
   172  			if !yield(kv) {
   173  				return
   174  			}
   175  		}
   176  	}
   177  }