github.com/ethersphere/bee/v2@v2.2.0/pkg/sharky/shard.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  	"encoding/binary"
    10  	"fmt"
    11  	"io"
    12  )
    13  
    14  // LocationSize is the size of the byte representation of Location
    15  const LocationSize int = 7
    16  
    17  // Location models the location <shard, slot, length> of a chunk
    18  type Location struct {
    19  	Shard  uint8
    20  	Slot   uint32
    21  	Length uint16
    22  }
    23  
    24  func (l Location) String() string {
    25  	return fmt.Sprintf("shard: %d, slot: %d, length: %d", l.Shard, l.Slot, l.Length)
    26  }
    27  
    28  // MarshalBinary returns byte representation of location
    29  func (l *Location) MarshalBinary() ([]byte, error) {
    30  	b := make([]byte, LocationSize)
    31  	b[0] = l.Shard
    32  	binary.LittleEndian.PutUint32(b[1:5], l.Slot)
    33  	binary.LittleEndian.PutUint16(b[5:], l.Length)
    34  	return b, nil
    35  }
    36  
    37  // UnmarshalBinary constructs the location from byte representation
    38  func (l *Location) UnmarshalBinary(buf []byte) error {
    39  	l.Shard = buf[0]
    40  	l.Slot = binary.LittleEndian.Uint32(buf[1:5])
    41  	l.Length = binary.LittleEndian.Uint16(buf[5:])
    42  	return nil
    43  }
    44  
    45  // LocationFromBinary is a helper to construct a Location object from byte representation
    46  func LocationFromBinary(buf []byte) (Location, error) {
    47  	l := new(Location)
    48  	err := l.UnmarshalBinary(buf)
    49  	if err != nil {
    50  		return Location{}, err
    51  	}
    52  	return *l, nil
    53  }
    54  
    55  // sharkyFile defines the minimal interface that is required for a file type for it to
    56  // be usable in sharky. This allows us to have different implementations of file types
    57  // that can continue using the sharky logic
    58  type sharkyFile interface {
    59  	io.ReadWriteCloser
    60  	io.ReaderAt
    61  	io.Seeker
    62  	io.WriterAt
    63  	Truncate(int64) error
    64  	Sync() error
    65  }
    66  
    67  // write models the input to a write operation
    68  type write struct {
    69  	buf []byte     // variable size read buffer
    70  	res chan entry // to put the result through
    71  }
    72  
    73  // entry models the output result of a write operation
    74  type entry struct {
    75  	loc Location // shard, slot, length combo
    76  	err error    // signal for end of operation
    77  }
    78  
    79  // read models the input to read operation (the output is an error)
    80  type read struct {
    81  	ctx  context.Context
    82  	buf  []byte // variable size read buffer
    83  	slot uint32 // slot to read from
    84  }
    85  
    86  // shard models a shard writing to a file with periodic offsets due to fixed maxDataSize
    87  type shard struct {
    88  	reads       chan read     // channel for reads
    89  	errc        chan error    // result for reads
    90  	writes      chan write    // channel for writes
    91  	index       uint8         // index of the shard
    92  	maxDataSize int           // max size of blobs
    93  	file        sharkyFile    // the file handle the shard is writing data to
    94  	slots       *slots        // component keeping track of freed slots
    95  	quit        chan struct{} // channel to signal quitting
    96  }
    97  
    98  // forever loop processing
    99  func (sh *shard) process() {
   100  	var writes chan write
   101  	var slot uint32
   102  	defer func() {
   103  		// this condition checks if an slot is in limbo (popped but not used for write op)
   104  		if writes != nil {
   105  			sh.slots.limboWG.Add(1)
   106  			go func() {
   107  				defer sh.slots.limboWG.Done()
   108  				sh.slots.in <- slot
   109  			}()
   110  		}
   111  	}()
   112  	free := sh.slots.out
   113  
   114  	for {
   115  		select {
   116  		case op := <-sh.reads:
   117  			select {
   118  			case sh.errc <- sh.read(op):
   119  			case <-op.ctx.Done():
   120  				// since the goroutine in the Read method can quit
   121  				// on shutdown, we need to make sure that we can actually
   122  				// write to the channel, since a shutdown is possible in
   123  				// theory between after the point that the context is cancelled
   124  				select {
   125  				case sh.errc <- op.ctx.Err():
   126  				case <-sh.quit:
   127  					// since the Read method respects the quit channel
   128  					// we can safely quit here without writing to the channel
   129  					return
   130  				}
   131  			case <-sh.quit:
   132  				return
   133  			}
   134  
   135  			// only enabled if there is a free slot previously popped
   136  		case op := <-writes:
   137  			op.res <- sh.write(op.buf, slot)
   138  			free = sh.slots.out // re-enable popping a free slot next time we can write
   139  			writes = nil        // disable popping a write operation until there is a free slot
   140  
   141  			// pop a free slot
   142  		case slot = <-free:
   143  			// only if there is one can we pop a chunk to write otherwise keep back pressure on writes
   144  			// effectively enforcing another shard to be chosen
   145  			writes = sh.writes // enable popping a write operation
   146  			free = nil         // disabling getting a new slot until a write is actually done
   147  
   148  		case <-sh.quit:
   149  			return
   150  		}
   151  	}
   152  }
   153  
   154  // close closes the shard:
   155  // wait for pending operations to finish then saves free slots and blobs on disk
   156  func (sh *shard) close() error {
   157  	sh.slots.wg.Wait()
   158  	if err := sh.slots.save(); err != nil {
   159  		return err
   160  	}
   161  	if err := sh.slots.file.Close(); err != nil {
   162  		return err
   163  	}
   164  	return sh.file.Close()
   165  }
   166  
   167  // offset calculates the offset from the slot
   168  // this is possible since all blobs are of fixed size
   169  func (sh *shard) offset(slot uint32) int64 {
   170  	return int64(slot) * int64(sh.maxDataSize)
   171  }
   172  
   173  // read reads loc.Length bytes to the buffer from the blob slot loc.Slot
   174  func (sh *shard) read(r read) error {
   175  	n, err := sh.file.ReadAt(r.buf, sh.offset(r.slot))
   176  	if err != nil {
   177  		return fmt.Errorf("read %d: %w", n, err)
   178  	}
   179  	return nil
   180  }
   181  
   182  // write writes loc.Length bytes to the buffer from the blob slot loc.Slot
   183  func (sh *shard) write(buf []byte, slot uint32) entry {
   184  	n, err := sh.file.WriteAt(buf, sh.offset(slot))
   185  	return entry{
   186  		loc: Location{
   187  			Shard:  sh.index,
   188  			Slot:   slot,
   189  			Length: uint16(n),
   190  		},
   191  		err: err,
   192  	}
   193  }
   194  
   195  // release frees the slot allowing new entry to overwrite
   196  func (sh *shard) release(ctx context.Context, slot uint32) error {
   197  	select {
   198  	case sh.slots.in <- slot:
   199  		return nil
   200  	case <-ctx.Done():
   201  		return ctx.Err()
   202  	}
   203  }