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 }