Skip to content

Commit da926b2

Browse files
headerfs: fail gracefully on db header writes
1 parent f55f48e commit da926b2

File tree

3 files changed

+434
-4
lines changed

3 files changed

+434
-4
lines changed

headerfs/store.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,35 @@ func (h *blockHeaderStore) WriteHeaders(hdrs ...BlockHeader) error {
538538
headerLocs[i] = header.toIndexEntry()
539539
}
540540

541-
return h.addHeaders(headerLocs)
541+
// Attempt to add the headers to the database. If this fails, we'll need
542+
// to roll back any changes to the header file to maintain consistency.
543+
// The rollback process bases on the number of header serialized. If
544+
// both the initial operation and the rollback fail, we return
545+
// a detailed error explaining both failures to aid in debugging.
546+
if err := h.addHeaders(headerLocs); err != nil {
547+
// Since the probability of failing to write to the database is
548+
// very low, it is mostly worth the cost of file sync operation
549+
// to make sure truncate headers does it correctly.
550+
if err := h.file.Sync(); err != nil {
551+
return fmt.Errorf("failed to sync block headers "+
552+
"file: %v", err)
553+
}
554+
555+
headersToRollback := len(hdrs)
556+
truncateErr := h.truncateHeaders(
557+
uint32(headersToRollback), h.indexType,
558+
)
559+
if truncateErr != nil {
560+
return fmt.Errorf("failed to rollback block headers "+
561+
"from binary file to previous valid state: "+
562+
"%v, error writing to database: %v, headers "+
563+
"to rollback: %d", truncateErr, err,
564+
headersToRollback)
565+
}
566+
return fmt.Errorf("failed to add block headers to db: %v", err)
567+
}
568+
569+
return nil
542570
}
543571

544572
// blockLocatorFromHash takes a given block hash and then creates a block
@@ -1035,7 +1063,35 @@ func (f *filterHeaderStore) WriteHeaders(hdrs ...FilterHeader) error {
10351063
// As the block headers should already be written, we only need to
10361064
// update the tip pointer for this particular header type.
10371065
newTip := hdrs[len(hdrs)-1].toIndexEntry().hash
1038-
return f.truncateIndices(&newTip, []*chainhash.Hash{}, false)
1066+
1067+
// Attempt to add the headers to the database. If this fails, we'll need
1068+
// to roll back any changes to the header file to maintain consistency.
1069+
// The rollback process bases on the number of header serialized. If
1070+
// both the initial operation and the rollback fail, we return
1071+
// a detailed error explaining both failures to aid in debugging.
1072+
if err := f.truncateIndices(&newTip, nil, false); err != nil {
1073+
// Since the probability of failing to write to the database is
1074+
// very low, it is mostly worth the cost of file sync operation
1075+
// to make sure truncate headers does it correctly.
1076+
if err := f.file.Sync(); err != nil {
1077+
return fmt.Errorf("failed to sync filter headers "+
1078+
"file: %v", err)
1079+
}
1080+
1081+
headersToRollback := len(hdrs)
1082+
truncateErr := f.truncateHeaders(
1083+
uint32(headersToRollback), f.indexType,
1084+
)
1085+
if truncateErr != nil {
1086+
return fmt.Errorf("failed to rollback filter headers "+
1087+
"binary file to previous valid state: %v, "+
1088+
"error writing to database: %v", truncateErr,
1089+
err)
1090+
}
1091+
return fmt.Errorf("failed to add filter headers to db: %v", err)
1092+
}
1093+
1094+
return nil
10391095
}
10401096

10411097
// ChainTip returns the latest filter header and height known to the

headerfs/store_mock.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package headerfs
22

33
import (
4+
"io"
5+
46
"github.com/btcsuite/btcd/blockchain"
57
"github.com/btcsuite/btcd/chaincfg/chainhash"
68
"github.com/btcsuite/btcd/wire"
9+
"github.com/btcsuite/btcwallet/walletdb"
710
"github.com/stretchr/testify/mock"
811
)
912

@@ -163,3 +166,54 @@ func (m *MockFilterHeaderStore) RollbackLastBlock(
163166
}
164167
return args.Get(0).(*BlockStamp), args.Error(1)
165168
}
169+
170+
// MockWalletDB is a mock implementation of walletdb.DB for testing.
171+
type MockWalletDB struct {
172+
mock.Mock
173+
}
174+
175+
// Update implements the walletdb.DB interface.
176+
func (m *MockWalletDB) Update(fn func(tx walletdb.ReadWriteTx) error) error {
177+
args := m.Called(fn)
178+
return args.Error(0)
179+
}
180+
181+
// View implements the walletdb.DB interface.
182+
func (m *MockWalletDB) View(fn func(tx walletdb.ReadTx) error) error {
183+
args := m.Called(fn)
184+
return args.Error(0)
185+
}
186+
187+
// BeginReadWriteTx implements the walletdb.DB interface.
188+
func (m *MockWalletDB) BeginReadWriteTx() (walletdb.ReadWriteTx, error) {
189+
args := m.Called()
190+
191+
if args.Get(0) == nil {
192+
return nil, args.Error(1)
193+
}
194+
195+
return args.Get(0).(walletdb.ReadWriteTx), args.Error(1)
196+
}
197+
198+
// BeginReadTx implements the walletdb.DB interface.
199+
func (m *MockWalletDB) BeginReadTx() (walletdb.ReadTx, error) {
200+
args := m.Called()
201+
202+
if args.Get(0) == nil {
203+
return nil, args.Error(1)
204+
}
205+
206+
return args.Get(0).(walletdb.ReadTx), args.Error(1)
207+
}
208+
209+
// Copy implements the walletdb.DB interface.
210+
func (m *MockWalletDB) Copy(w io.Writer) error {
211+
args := m.Called(w)
212+
return args.Error(0)
213+
}
214+
215+
// Close implements the walletdb.DB interface.
216+
func (m *MockWalletDB) Close() error {
217+
args := m.Called()
218+
return args.Error(0)
219+
}

0 commit comments

Comments
 (0)