github.com/ethersphere/bee/v2@v2.2.0/pkg/storer/internal/transaction/transaction.go (about)

     1  // Copyright 2024 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  /*
     6  Package transaction provides transaction support for localstore operations.
     7  All writes to the localstore (both indexstore and chunkstore) must be made using a transaction.
     8  The transaction must be committed for the writes to be stored on the disk.
     9  
    10  The rules of the transaction is as follows:
    11  
    12  -sharky_write 		-> write to disk, keep sharky location in memory
    13  -sharky_release		-> keep location in memory, do not release from the disk
    14  -indexstore write	-> write to batch
    15  -on commit			-> if batch_commit succeeds, release sharky_release locations from the disk
    16  					-> if batch_commit fails or is not called, release all sharky_write location from the disk, do nothing for sharky_release
    17  
    18  See the NewTransaction method for more details.
    19  */
    20  
    21  package transaction
    22  
    23  import (
    24  	"context"
    25  	"errors"
    26  	"fmt"
    27  	"time"
    28  
    29  	m "github.com/ethersphere/bee/v2/pkg/metrics"
    30  	"github.com/ethersphere/bee/v2/pkg/sharky"
    31  	"github.com/ethersphere/bee/v2/pkg/storage"
    32  	"github.com/ethersphere/bee/v2/pkg/storer/internal/chunkstore"
    33  	"github.com/ethersphere/bee/v2/pkg/swarm"
    34  	"github.com/prometheus/client_golang/prometheus"
    35  	"resenje.org/multex"
    36  )
    37  
    38  type Transaction interface {
    39  	Store
    40  	Commit() error
    41  }
    42  
    43  type Store interface {
    44  	ChunkStore() storage.ChunkStore
    45  	IndexStore() storage.IndexStore
    46  }
    47  
    48  type ReadOnlyStore interface {
    49  	IndexStore() storage.Reader
    50  	ChunkStore() storage.ReadOnlyChunkStore
    51  }
    52  
    53  type Storage interface {
    54  	ReadOnlyStore
    55  	NewTransaction(context.Context) (Transaction, func())
    56  	Run(context.Context, func(Store) error) error
    57  	Close() error
    58  }
    59  
    60  type store struct {
    61  	sharky      *sharky.Store
    62  	bstore      storage.BatchStore
    63  	metrics     metrics
    64  	chunkLocker *multex.Multex
    65  }
    66  
    67  func NewStorage(sharky *sharky.Store, bstore storage.BatchStore) Storage {
    68  	return &store{sharky, bstore, newMetrics(), multex.New()}
    69  }
    70  
    71  type transaction struct {
    72  	start      time.Time
    73  	batch      storage.Batch
    74  	indexstore storage.IndexStore
    75  	chunkStore *chunkStoreTrx
    76  	sharkyTrx  *sharkyTrx
    77  	metrics    metrics
    78  }
    79  
    80  // NewTransaction returns a new storage transaction.
    81  // Commit must be called to persist data to the disk.
    82  // The callback function must be the final call of the transaction whether or not any errors
    83  // were returned from the storage ops or commit. Safest option is to do a defer call immediately after
    84  // creating the transaction.
    85  // By design, it is best to not batch too many writes to a single transaction, including multiple chunks writes.
    86  // Calls made to the transaction are NOT thread-safe.
    87  func (s *store) NewTransaction(ctx context.Context) (Transaction, func()) {
    88  
    89  	b := s.bstore.Batch(ctx)
    90  
    91  	index := &indexTrx{s.bstore, b, s.metrics}
    92  	sharky := &sharkyTrx{s.sharky, s.metrics, nil, nil}
    93  
    94  	t := &transaction{
    95  		start:      time.Now(),
    96  		batch:      b,
    97  		indexstore: index,
    98  		chunkStore: &chunkStoreTrx{index, sharky, s.chunkLocker, make(map[string]struct{}), s.metrics, false},
    99  		sharkyTrx:  sharky,
   100  		metrics:    s.metrics,
   101  	}
   102  
   103  	return t, func() {
   104  		// for whatever reason, commit was not called
   105  		// release uncommitted but written sharky locations
   106  		// unlock the locked addresses
   107  		for _, l := range t.sharkyTrx.writtenLocs {
   108  			_ = t.sharkyTrx.sharky.Release(context.TODO(), l)
   109  		}
   110  		for addr := range t.chunkStore.lockedAddrs {
   111  			s.chunkLocker.Unlock(addr)
   112  		}
   113  		t.sharkyTrx.writtenLocs = nil
   114  		t.chunkStore.lockedAddrs = nil
   115  	}
   116  }
   117  
   118  func (s *store) IndexStore() storage.Reader {
   119  	return &indexTrx{s.bstore, nil, s.metrics}
   120  }
   121  
   122  func (s *store) ChunkStore() storage.ReadOnlyChunkStore {
   123  	indexStore := &indexTrx{s.bstore, nil, s.metrics}
   124  	sharyTrx := &sharkyTrx{s.sharky, s.metrics, nil, nil}
   125  	return &chunkStoreTrx{indexStore, sharyTrx, s.chunkLocker, nil, s.metrics, true}
   126  }
   127  
   128  // Run creates a new transaction and gives the caller access to the transaction
   129  // in the form of a callback function. After the callback returns, the transaction
   130  // is committed to the disk. See the NewTransaction method for more details on how transactions operate internally.
   131  // By design, it is best to not batch too many writes to a single transaction, including multiple chunks writes.
   132  // Calls made to the transaction are NOT thread-safe.
   133  func (s *store) Run(ctx context.Context, f func(Store) error) error {
   134  	trx, done := s.NewTransaction(ctx)
   135  	defer done()
   136  
   137  	err := f(trx)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	return trx.Commit()
   142  }
   143  
   144  // Metrics returns set of prometheus collectors.
   145  func (s *store) Metrics() []prometheus.Collector {
   146  	return m.PrometheusCollectorsFromFields(s.metrics)
   147  }
   148  
   149  func (s *store) Close() error {
   150  	return errors.Join(s.bstore.Close(), s.sharky.Close())
   151  }
   152  
   153  func (t *transaction) Commit() (err error) {
   154  
   155  	defer func() {
   156  		t.metrics.MethodDuration.WithLabelValues("transaction", "success").Observe(time.Since(t.start).Seconds())
   157  	}()
   158  
   159  	defer handleMetric("commit", t.metrics)(&err)
   160  	defer func() {
   161  		for addr := range t.chunkStore.lockedAddrs {
   162  			t.chunkStore.globalLocker.Unlock(addr)
   163  		}
   164  		t.chunkStore.lockedAddrs = nil
   165  		t.sharkyTrx.writtenLocs = nil
   166  	}()
   167  
   168  	h := handleMetric("batch_commit", t.metrics)
   169  	err = t.batch.Commit()
   170  	h(&err)
   171  	if err != nil {
   172  		// since the batch commit has failed, we must release the written chunks from sharky.
   173  		for _, l := range t.sharkyTrx.writtenLocs {
   174  			if rerr := t.sharkyTrx.sharky.Release(context.TODO(), l); rerr != nil {
   175  				err = errors.Join(err, fmt.Errorf("failed releasing location during commit rollback %s: %w", l, rerr))
   176  			}
   177  		}
   178  		return err
   179  	}
   180  
   181  	// the batch commit was successful, we can now release the accumulated locations from sharky.
   182  	for _, l := range t.sharkyTrx.releasedLocs {
   183  		h := handleMetric("sharky_release", t.metrics)
   184  		rerr := t.sharkyTrx.sharky.Release(context.TODO(), l)
   185  		h(&rerr)
   186  		if rerr != nil {
   187  			err = errors.Join(err, fmt.Errorf("failed releasing location after commit %s: %w", l, rerr))
   188  		}
   189  	}
   190  
   191  	return err
   192  }
   193  
   194  // IndexStore gives access to the index store of the transaction.
   195  // Note that no writes are persisted to the disk until the commit is called.
   196  func (t *transaction) IndexStore() storage.IndexStore {
   197  	return t.indexstore
   198  }
   199  
   200  // ChunkStore gives access to the chunkstore of the transaction.
   201  // Note that no writes are persisted to the disk until the commit is called.
   202  func (t *transaction) ChunkStore() storage.ChunkStore {
   203  	return t.chunkStore
   204  }
   205  
   206  type chunkStoreTrx struct {
   207  	indexStore   storage.IndexStore
   208  	sharkyTrx    *sharkyTrx
   209  	globalLocker *multex.Multex
   210  	lockedAddrs  map[string]struct{}
   211  	metrics      metrics
   212  	readOnly     bool
   213  }
   214  
   215  func (c *chunkStoreTrx) Get(ctx context.Context, addr swarm.Address) (ch swarm.Chunk, err error) {
   216  	defer handleMetric("chunkstore_get", c.metrics)(&err)
   217  	unlock := c.lock(addr)
   218  	defer unlock()
   219  	ch, err = chunkstore.Get(ctx, c.indexStore, c.sharkyTrx, addr)
   220  	return ch, err
   221  }
   222  func (c *chunkStoreTrx) Has(ctx context.Context, addr swarm.Address) (_ bool, err error) {
   223  	defer handleMetric("chunkstore_has", c.metrics)(&err)
   224  	unlock := c.lock(addr)
   225  	defer unlock()
   226  	return chunkstore.Has(ctx, c.indexStore, addr)
   227  }
   228  func (c *chunkStoreTrx) Put(ctx context.Context, ch swarm.Chunk) (err error) {
   229  	defer handleMetric("chunkstore_put", c.metrics)(&err)
   230  	unlock := c.lock(ch.Address())
   231  	defer unlock()
   232  	return chunkstore.Put(ctx, c.indexStore, c.sharkyTrx, ch)
   233  }
   234  func (c *chunkStoreTrx) Delete(ctx context.Context, addr swarm.Address) (err error) {
   235  	defer handleMetric("chunkstore_delete", c.metrics)(&err)
   236  	unlock := c.lock(addr)
   237  	defer unlock()
   238  	return chunkstore.Delete(ctx, c.indexStore, c.sharkyTrx, addr)
   239  }
   240  func (c *chunkStoreTrx) Iterate(ctx context.Context, fn storage.IterateChunkFn) (err error) {
   241  	defer handleMetric("chunkstore_iterate", c.metrics)(&err)
   242  	return chunkstore.Iterate(ctx, c.indexStore, c.sharkyTrx, fn)
   243  }
   244  
   245  func (c *chunkStoreTrx) Replace(ctx context.Context, ch swarm.Chunk) (err error) {
   246  	defer handleMetric("chunkstore_replace", c.metrics)(&err)
   247  	unlock := c.lock(ch.Address())
   248  	defer unlock()
   249  	return chunkstore.Replace(ctx, c.indexStore, c.sharkyTrx, ch)
   250  }
   251  
   252  func (c *chunkStoreTrx) lock(addr swarm.Address) func() {
   253  	// directly lock
   254  	if c.readOnly {
   255  		c.globalLocker.Lock(addr.ByteString())
   256  		return func() { c.globalLocker.Unlock(addr.ByteString()) }
   257  	}
   258  
   259  	// lock chunk only once in the same transaction
   260  	if _, ok := c.lockedAddrs[addr.ByteString()]; !ok {
   261  		c.globalLocker.Lock(addr.ByteString())
   262  		c.lockedAddrs[addr.ByteString()] = struct{}{}
   263  	}
   264  
   265  	return func() {} // unlocking the chunk will be done in the Commit()
   266  }
   267  
   268  type indexTrx struct {
   269  	store   storage.Reader
   270  	batch   storage.Batch
   271  	metrics metrics
   272  }
   273  
   274  func (s *indexTrx) Get(i storage.Item) error           { return s.store.Get(i) }
   275  func (s *indexTrx) Has(k storage.Key) (bool, error)    { return s.store.Has(k) }
   276  func (s *indexTrx) GetSize(k storage.Key) (int, error) { return s.store.GetSize(k) }
   277  func (s *indexTrx) Iterate(q storage.Query, f storage.IterateFn) (err error) {
   278  	defer handleMetric("iterate", s.metrics)(&err)
   279  	return s.store.Iterate(q, f)
   280  }
   281  func (s *indexTrx) Count(k storage.Key) (int, error) { return s.store.Count(k) }
   282  func (s *indexTrx) Put(i storage.Item) error         { return s.batch.Put(i) }
   283  func (s *indexTrx) Delete(i storage.Item) error      { return s.batch.Delete(i) }
   284  
   285  type sharkyTrx struct {
   286  	sharky       *sharky.Store
   287  	metrics      metrics
   288  	writtenLocs  []sharky.Location
   289  	releasedLocs []sharky.Location
   290  }
   291  
   292  func (s *sharkyTrx) Read(ctx context.Context, loc sharky.Location, buf []byte) (err error) {
   293  	defer handleMetric("sharky_read", s.metrics)(&err)
   294  	return s.sharky.Read(ctx, loc, buf)
   295  }
   296  
   297  func (s *sharkyTrx) Write(ctx context.Context, data []byte) (_ sharky.Location, err error) {
   298  	defer handleMetric("sharky_write", s.metrics)(&err)
   299  	loc, err := s.sharky.Write(ctx, data)
   300  	if err != nil {
   301  		return sharky.Location{}, err
   302  	}
   303  
   304  	s.writtenLocs = append(s.writtenLocs, loc)
   305  	return loc, nil
   306  }
   307  
   308  func (s *sharkyTrx) Release(ctx context.Context, loc sharky.Location) error {
   309  	s.releasedLocs = append(s.releasedLocs, loc)
   310  	return nil
   311  }
   312  
   313  func handleMetric(key string, m metrics) func(*error) {
   314  	t := time.Now()
   315  	return func(err *error) {
   316  		if err != nil && *err != nil {
   317  			m.MethodCalls.WithLabelValues(key, "failure").Inc()
   318  			m.MethodDuration.WithLabelValues(key, "failure").Observe(time.Since(t).Seconds())
   319  		} else {
   320  			m.MethodCalls.WithLabelValues(key, "success").Inc()
   321  			m.MethodDuration.WithLabelValues(key, "success").Observe(time.Since(t).Seconds())
   322  		}
   323  	}
   324  }