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 }