github.com/ethersphere/bee/v2@v2.2.0/pkg/sharky/store.go (about)

     1  // Copyright 2021 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package sharky
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io/fs"
    12  	"strconv"
    13  	"sync"
    14  
    15  	"github.com/hashicorp/go-multierror"
    16  )
    17  
    18  var (
    19  	// ErrTooLong returned by Write if the blob length exceeds the max blobsize.
    20  	ErrTooLong = errors.New("data too long")
    21  	// ErrQuitting returned by Write when the store is Closed before the write completes.
    22  	ErrQuitting = errors.New("quitting")
    23  )
    24  
    25  // Store models the sharded fix-length blobstore
    26  // Design provides lockless sharding:
    27  // - shard choice responding to backpressure by running operation
    28  // - read prioritisation over writing
    29  // - free slots allow write
    30  type Store struct {
    31  	maxDataSize int             // max length of blobs
    32  	writes      chan write      // shared write operations channel
    33  	shards      []*shard        // shards
    34  	wg          *sync.WaitGroup // count started operations
    35  	quit        chan struct{}   // quit channel
    36  	metrics     metrics
    37  }
    38  
    39  // New constructs a sharded blobstore
    40  // arguments:
    41  // - base directory string
    42  // - shard count - positive integer < 256 - cannot be zero or expect panic
    43  // - shard size - positive integer multiple of 8 - for others expect undefined behaviour
    44  // - maxDataSize - positive integer representing the maximum blob size to be stored
    45  func New(basedir fs.FS, shardCnt int, maxDataSize int) (*Store, error) {
    46  	store := &Store{
    47  		maxDataSize: maxDataSize,
    48  		writes:      make(chan write),
    49  		shards:      make([]*shard, shardCnt),
    50  		wg:          &sync.WaitGroup{},
    51  		quit:        make(chan struct{}),
    52  		metrics:     newMetrics(),
    53  	}
    54  	for i := range store.shards {
    55  		s, err := store.create(uint8(i), maxDataSize, basedir)
    56  		if err != nil {
    57  			return nil, err
    58  		}
    59  		store.shards[i] = s
    60  	}
    61  	store.metrics.ShardCount.Set(float64(len(store.shards)))
    62  
    63  	return store, nil
    64  }
    65  
    66  // Close closes each shard and return incidental errors from each shard
    67  func (s *Store) Close() error {
    68  	close(s.quit)
    69  	err := new(multierror.Error)
    70  	for _, sh := range s.shards {
    71  		err = multierror.Append(err, sh.close())
    72  	}
    73  
    74  	return err.ErrorOrNil()
    75  }
    76  
    77  // create creates a new shard with index, max capacity limit, file within base directory
    78  func (s *Store) create(index uint8, maxDataSize int, basedir fs.FS) (*shard, error) {
    79  	file, err := basedir.Open(fmt.Sprintf("shard_%03d", index))
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  	ffile, err := basedir.Open(fmt.Sprintf("free_%03d", index))
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	sl := newSlots(ffile.(sharkyFile), s.wg)
    88  	err = sl.load()
    89  	if err != nil {
    90  		return nil, err
    91  	}
    92  	sh := &shard{
    93  		reads:       make(chan read),
    94  		errc:        make(chan error),
    95  		writes:      s.writes,
    96  		index:       index,
    97  		maxDataSize: maxDataSize,
    98  		file:        file.(sharkyFile),
    99  		slots:       sl,
   100  		quit:        s.quit,
   101  	}
   102  	terminated := make(chan struct{})
   103  	sh.slots.wg.Add(1)
   104  	go func() {
   105  		defer sh.slots.wg.Done()
   106  		sh.process()
   107  		close(terminated)
   108  	}()
   109  	sh.slots.wg.Add(1)
   110  	go func() {
   111  		defer sh.slots.wg.Done()
   112  		sl.process(terminated)
   113  	}()
   114  	return sh, nil
   115  }
   116  
   117  // Read reads the content of the blob found at location into the byte buffer given
   118  // The location is assumed to be obtained by an earlier Write call storing the blob
   119  func (s *Store) Read(ctx context.Context, loc Location, buf []byte) (err error) {
   120  	sh := s.shards[loc.Shard]
   121  	select {
   122  	case sh.reads <- read{ctx: ctx, buf: buf[:loc.Length], slot: loc.Slot}:
   123  		s.metrics.TotalReadCalls.Inc()
   124  	case <-ctx.Done():
   125  		return ctx.Err()
   126  	case <-sh.quit:
   127  		return ErrQuitting
   128  	}
   129  
   130  	// it is important that this select would NEVER respect the context
   131  	// cancellation. this would result in a deadlock on the shard, since
   132  	// the result of the operation must be drained from errc, allowing the
   133  	// shard to be able to handle new operations (#2932).
   134  	select {
   135  	case err = <-sh.errc:
   136  		if err != nil {
   137  			s.metrics.TotalReadCallsErr.Inc()
   138  		}
   139  		return err
   140  	case <-s.quit:
   141  		// we need to make sure that the forever loop in shard.go can
   142  		// always return due to shutdown in case this goroutine goes away.
   143  		return ErrQuitting
   144  	}
   145  }
   146  
   147  // Write stores a new blob and returns its location to be used as a reference
   148  // It can be given to a Read call to return the stored blob.
   149  func (s *Store) Write(ctx context.Context, data []byte) (loc Location, err error) {
   150  	if len(data) > s.maxDataSize {
   151  		return loc, ErrTooLong
   152  	}
   153  	s.wg.Add(1)
   154  	defer s.wg.Done()
   155  
   156  	c := make(chan entry, 1) // buffer the channel to avoid blocking in shard.process on quit or context done
   157  
   158  	select {
   159  	case s.writes <- write{data, c}:
   160  		s.metrics.TotalWriteCalls.Inc()
   161  	case <-s.quit:
   162  		return loc, ErrQuitting
   163  	case <-ctx.Done():
   164  		return loc, ctx.Err()
   165  	}
   166  
   167  	select {
   168  	case e := <-c:
   169  		if e.err == nil {
   170  			shard := strconv.Itoa(int(e.loc.Shard))
   171  			s.metrics.CurrentShardSize.WithLabelValues(shard).Inc()
   172  			s.metrics.ShardFragmentation.WithLabelValues(shard).Add(float64(s.maxDataSize - int(e.loc.Length)))
   173  			s.metrics.LastAllocatedShardSlot.WithLabelValues(shard).Set(float64(e.loc.Slot))
   174  		} else {
   175  			s.metrics.TotalWriteCallsErr.Inc()
   176  		}
   177  		return e.loc, e.err
   178  	case <-s.quit:
   179  		return loc, ErrQuitting
   180  	case <-ctx.Done():
   181  		return loc, ctx.Err()
   182  	}
   183  }
   184  
   185  // Release gives back the slot to the shard
   186  // From here on the slot can be reused and overwritten
   187  // Release is meant to be called when an entry in the upstream db is removed
   188  // Note that releasing is not safe for obfuscating earlier content, since
   189  // even after reuse, the slot may be used by a very short blob and leaves the
   190  // rest of the old blob bytes untouched
   191  func (s *Store) Release(ctx context.Context, loc Location) error {
   192  	sh := s.shards[loc.Shard]
   193  	err := sh.release(ctx, loc.Slot)
   194  	s.metrics.TotalReleaseCalls.Inc()
   195  	if err == nil {
   196  		shard := strconv.Itoa(int(sh.index))
   197  		s.metrics.CurrentShardSize.WithLabelValues(shard).Dec()
   198  		s.metrics.ShardFragmentation.WithLabelValues(shard).Sub(float64(s.maxDataSize - int(loc.Length)))
   199  		s.metrics.LastReleasedShardSlot.WithLabelValues(shard).Set(float64(loc.Slot))
   200  	} else {
   201  		s.metrics.TotalReleaseCallsErr.Inc()
   202  	}
   203  	return err
   204  }