Skip to content

Commit 09d97bb

Browse files
authored
fix: prevent second batch store reset wiping postage snapshot on --resync (#5499)
1 parent fe87393 commit 09d97bb

7 files changed

Lines changed: 592 additions & 79 deletions

File tree

pkg/node/node.go

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import (
4545
"github.com/ethersphere/bee/v2/pkg/postage/batchstore"
4646
"github.com/ethersphere/bee/v2/pkg/postage/listener"
4747
"github.com/ethersphere/bee/v2/pkg/postage/postagecontract"
48+
"github.com/ethersphere/bee/v2/pkg/postage/snapshot"
49+
"github.com/ethersphere/bee/v2/pkg/postage/snapshot/archive"
4850
"github.com/ethersphere/bee/v2/pkg/pricer"
4951
"github.com/ethersphere/bee/v2/pkg/pricing"
5052
"github.com/ethersphere/bee/v2/pkg/pss"
@@ -787,11 +789,6 @@ func NewBee(
787789
eventListener = listener.New(b.syncingStopped, logger, chainBackend, postageStampContractAddress, postageStampContractABI, o.BlockTime, postageSyncingStallingTimeout, postageSyncingBackoffTimeout)
788790
b.listenerCloser = eventListener
789791

790-
batchSvc, err = batchservice.New(stateStore, batchStore, logger, eventListener, overlayEthAddress.Bytes(), post, sha3.New256, o.Resync)
791-
if err != nil {
792-
return nil, fmt.Errorf("init batch service: %w", err)
793-
}
794-
795792
// Construct protocols.
796793
pingPong := pingpong.New(p2ps, logger, tracer)
797794

@@ -892,28 +889,29 @@ func NewBee(
892889
}
893890
)
894891

892+
// When the postage snapshot applies, hand it to the batch service so it
893+
// rebuilds the store from the snapshot during construction; otherwise the
894+
// store is reset only when --resync is requested. Either way the store is
895+
// reset in a single place (batchservice.New), which avoids a second reset
896+
// wiping the snapshot that was just loaded (see #5495).
897+
var batchSnapshot *batchservice.Snapshot
895898
if !o.SkipPostageSnapshot && !batchStoreExists && (networkID == mainnetNetworkID) && beeNodeMode != api.UltraLightMode {
896-
chainBackend := NewSnapshotLogFilterer(logger, archiveSnapshotGetter{})
897-
898-
snapshotEventListener := listener.New(b.syncingStopped, logger, chainBackend, postageStampContractAddress, postageStampContractABI, o.BlockTime, postageSyncingStallingTimeout, postageSyncingBackoffTimeout)
899-
900-
snapshotBatchSvc, err := batchservice.New(stateStore, batchStore, logger, snapshotEventListener, overlayEthAddress.Bytes(), post, sha3.New256, o.Resync)
899+
batchSnapshot, err = snapshot.New(ctx, logger, archive.Getter{}, b.syncingStopped, postageStampContractAddress, postageStampContractABI, o.BlockTime, postageSyncingStallingTimeout, postageSyncingBackoffTimeout, postageSyncStart)
901900
if err != nil {
902-
logger.Error(err, "failed to initialize batch service from snapshot, continuing outside snapshot block...")
903-
} else {
904-
err = snapshotBatchSvc.Start(ctx, postageSyncStart)
905-
syncStatus.Store(true)
906-
if err != nil {
907-
syncErr.Store(err)
908-
logger.Error(err, "failed to start batch service from snapshot, continuing outside snapshot block...")
909-
} else {
910-
postageSyncStart = chainBackend.maxBlockHeight
911-
}
912-
}
913-
if errClose := snapshotEventListener.Close(); errClose != nil {
914-
logger.Error(errClose, "failed to close event listener (snapshot) failure")
901+
// A corrupt snapshot is not fatal: rebuild from the chain instead.
902+
logger.Error(err, "postage snapshot unavailable, syncing from chain instead")
915903
}
904+
}
916905

906+
var snapshotLoaded bool
907+
batchSvc, snapshotLoaded, err = batchservice.New(ctx, stateStore, batchStore, logger, eventListener, overlayEthAddress.Bytes(), post, sha3.New256, batchSnapshot, o.Resync)
908+
if err != nil {
909+
return nil, fmt.Errorf("init batch service: %w", err)
910+
}
911+
if snapshotLoaded {
912+
// The snapshot rebuilt the store up to its block height, so the node can
913+
// already serve postage requests while the remaining gap syncs live.
914+
syncStatus.Store(true)
917915
}
918916

919917
if batchSvc != nil && chainEnabled {

pkg/postage/batchservice/batchservice.go

Lines changed: 120 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,51 @@ type batchService struct {
3939
batchListener postage.BatchEventListener
4040

4141
checksum hash.Hash // checksum hasher
42-
resync bool
42+
43+
// snapshotResumeBlock is the chain block height the store was rebuilt to
44+
// from a postage snapshot. When set, live sync resumes from here.
45+
snapshotResumeBlock uint64
4346
}
4447

4548
type Interface interface {
4649
postage.EventUpdater
4750
}
4851

52+
// Snapshot carries the optional inputs needed to rebuild the batch store from a
53+
// postage snapshot. When passed to New (non-nil), the store is reset and
54+
// replayed from the snapshot before the service is returned, and live sync
55+
// later resumes from the snapshot's block height. New takes ownership of
56+
// Listener and closes it once the snapshot has been replayed.
57+
type Snapshot struct {
58+
// Listener replays the snapshot's events into the batch store.
59+
Listener postage.Listener
60+
// StartBlock is the block height from which the snapshot is replayed (the
61+
// postage contract start block).
62+
StartBlock uint64
63+
// ResumeBlock is the block height the snapshot reached, from which live sync
64+
// resumes once the replay completes.
65+
ResumeBlock uint64
66+
}
67+
4968
// New will create a new BatchService.
69+
//
70+
// The batch store is reset here, in the constructor, when a resync was requested
71+
// or a dirty shutdown was detected. A provided snapshot is then replayed onto the
72+
// (possibly reset) store; if that replay fails the store is reset again so live
73+
// sync can rebuild it from the chain. Start never resets the store. The returned
74+
// bool reports whether the snapshot was replayed successfully.
5075
func New(
76+
ctx context.Context,
5177
stateStore storage.StateStorer,
5278
storer postage.Storer,
5379
logger log.Logger,
5480
listener postage.Listener,
5581
owner []byte,
5682
batchListener postage.BatchEventListener,
5783
checksumFunc func() hash.Hash,
84+
snapshot *Snapshot,
5885
resync bool,
59-
) (Interface, error) {
86+
) (Interface, bool, error) {
6087
if checksumFunc == nil {
6188
checksumFunc = sha3.New256
6289
}
@@ -65,37 +92,113 @@ func New(
6592
sum = checksumFunc()
6693
)
6794

95+
logger = logger.WithName(loggerName).Register()
96+
6897
dirty := false
69-
err := stateStore.Get(dirtyDBKey, &dirty)
70-
if err != nil && !errors.Is(err, storage.ErrNotFound) {
71-
return nil, err
98+
if err := stateStore.Get(dirtyDBKey, &dirty); err != nil && !errors.Is(err, storage.ErrNotFound) {
99+
return nil, false, err
100+
}
101+
if dirty {
102+
logger.Warning("batch service: dirty shutdown detected, resetting batch store")
72103
}
73104

74105
if resync {
106+
logger.Warning("batch service: resync requested, resetting batch store")
75107
if err := stateStore.Delete(checksumDBKey); err != nil {
76-
return nil, err
108+
return nil, false, err
77109
}
78110
} else if !dirty {
79111
if err := stateStore.Get(checksumDBKey, &b); err != nil {
80112
if !errors.Is(err, storage.ErrNotFound) {
81-
return nil, err
113+
return nil, false, err
82114
}
83115
} else {
84116
s, err := hex.DecodeString(b)
85117
if err != nil {
86-
return nil, err
118+
return nil, false, err
87119
}
88120
n, err := sum.Write(s)
89121
if err != nil {
90-
return nil, err
122+
return nil, false, err
91123
}
92124
if n != len(s) {
93-
return nil, errors.New("batchstore checksum init")
125+
return nil, false, errors.New("batchstore checksum init")
126+
}
127+
}
128+
}
129+
130+
bs := &batchService{
131+
stateStore: stateStore,
132+
storer: storer,
133+
logger: logger,
134+
listener: listener,
135+
owner: owner,
136+
batchListener: batchListener,
137+
checksum: sum,
138+
}
139+
140+
// Reset the store once, here, when a resync was requested or a dirty shutdown
141+
// was detected (both already logged above). A snapshot is then replayed onto
142+
// the store; Start does not reset, so this is the only unconditional reset.
143+
if dirty || resync {
144+
if err := bs.reset(); err != nil {
145+
return nil, false, err
146+
}
147+
}
148+
149+
snapshotLoaded := false
150+
if snapshot != nil {
151+
if err := bs.loadSnapshot(ctx, snapshot); err != nil {
152+
logger.Error(err, "failed to start batch service from snapshot, continuing outside snapshot block...")
153+
// A partial replay may have written to (and dirtied) the store, so
154+
// reset it again to rebuild cleanly from the chain during live sync.
155+
if err := bs.reset(); err != nil {
156+
return nil, false, err
94157
}
158+
} else {
159+
snapshotLoaded = true
160+
}
161+
}
162+
163+
return bs, snapshotLoaded, nil
164+
}
165+
166+
// reset wipes the batch store so it can be rebuilt from scratch. It runs only
167+
// during New. The reason for the reset is logged by the caller at the point of
168+
// detection, so the intent is recorded even if the reset itself then fails.
169+
func (svc *batchService) reset() error {
170+
if err := svc.storer.Reset(); err != nil {
171+
return err
172+
}
173+
if err := svc.stateStore.Delete(dirtyDBKey); err != nil {
174+
return err
175+
}
176+
svc.logger.Warning("batch service: batch store has been reset. your node will now resync chain data. this might take a while...")
177+
178+
return nil
179+
}
180+
181+
// loadSnapshot rebuilds the (already reset) store from a postage snapshot by
182+
// replaying its events and records the block height to resume live sync from.
183+
func (svc *batchService) loadSnapshot(ctx context.Context, snapshot *Snapshot) error {
184+
defer func() {
185+
if err := snapshot.Listener.Close(); err != nil {
186+
svc.logger.Error(err, "failed to close event listener (snapshot) failure")
95187
}
188+
}()
189+
190+
startBlock := snapshot.StartBlock
191+
if cs := svc.storer.GetChainState(); cs.Block > startBlock {
192+
startBlock = cs.Block
96193
}
97194

98-
return &batchService{stateStore, storer, logger.WithName(loggerName).Register(), listener, owner, batchListener, sum, resync}, nil
195+
if err := <-snapshot.Listener.Listen(ctx, startBlock+1, svc); err != nil {
196+
return err
197+
}
198+
199+
svc.snapshotResumeBlock = snapshot.ResumeBlock
200+
201+
return nil
99202
}
100203

101204
// Create will create a new batch with the given ID, owner value and depth and
@@ -242,33 +345,16 @@ func (svc *batchService) TransactionEnd() error {
242345
var ErrInterruped = errors.New("postage sync interrupted")
243346

244347
func (svc *batchService) Start(ctx context.Context, startBlock uint64) (err error) {
245-
dirty := false
246-
err = svc.stateStore.Get(dirtyDBKey, &dirty)
247-
if err != nil && !errors.Is(err, storage.ErrNotFound) {
248-
return err
249-
}
250-
251-
if dirty || svc.resync {
252-
253-
if dirty {
254-
svc.logger.Warning("batch service: dirty shutdown detected, resetting batch store")
255-
} else {
256-
svc.logger.Warning("batch service: resync requested, resetting batch store")
257-
}
258-
259-
if err := svc.storer.Reset(); err != nil {
260-
return err
261-
}
262-
if err := svc.stateStore.Delete(dirtyDBKey); err != nil {
263-
return err
264-
}
265-
svc.logger.Warning("batch service: batch store has been reset. your node will now resync chain data. this might take a while...")
266-
}
267-
348+
// The store reset already happened in New, so Start only drives live sync.
268349
cs := svc.storer.GetChainState()
269350
if cs.Block > startBlock {
270351
startBlock = cs.Block
271352
}
353+
// When the store was rebuilt from a snapshot, resume live sync from the
354+
// snapshot's block height rather than the requested start block.
355+
if svc.snapshotResumeBlock > startBlock {
356+
startBlock = svc.snapshotResumeBlock
357+
}
272358

273359
syncedChan := svc.listener.Listen(ctx, startBlock+1, svc)
274360

0 commit comments

Comments
 (0)