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 }