Skip to content

Commit a0458a2

Browse files
WIZARD-CXYxiang90
authored andcommitted
fix rollback panic bug (#153)
1 parent 2eb7227 commit a0458a2

File tree

3 files changed

+94
-2
lines changed

3 files changed

+94
-2
lines changed

freelist.go

+22
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,28 @@ func (f *freelist) reload(p *page) {
349349
f.readIDs(a)
350350
}
351351

352+
// noSyncReload reads the freelist from pgids and filters out pending items.
353+
func (f *freelist) noSyncReload(pgids []pgid) {
354+
// Build a cache of only pending pages.
355+
pcache := make(map[pgid]bool)
356+
for _, txp := range f.pending {
357+
for _, pendingID := range txp.ids {
358+
pcache[pendingID] = true
359+
}
360+
}
361+
362+
// Check each page in the freelist and build a new available freelist
363+
// with any pages not in the pending lists.
364+
var a []pgid
365+
for _, id := range pgids {
366+
if !pcache[id] {
367+
a = append(a, id)
368+
}
369+
}
370+
371+
f.readIDs(a)
372+
}
373+
352374
// reindex rebuilds the free cache based on available and pending free lists.
353375
func (f *freelist) reindex() {
354376
ids := f.getFreePageIDs()

tx.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -254,17 +254,36 @@ func (tx *Tx) Rollback() error {
254254
if tx.db == nil {
255255
return ErrTxClosed
256256
}
257-
tx.rollback()
257+
tx.nonPhysicalRollback()
258258
return nil
259259
}
260260

261+
// nonPhysicalRollback is called when user calls Rollback directly, in this case we do not need to reload the free pages from disk.
262+
func (tx *Tx) nonPhysicalRollback() {
263+
if tx.db == nil {
264+
return
265+
}
266+
if tx.writable {
267+
tx.db.freelist.rollback(tx.meta.txid)
268+
}
269+
tx.close()
270+
}
271+
272+
// rollback needs to reload the free pages from disk in case some system error happens like fsync error.
261273
func (tx *Tx) rollback() {
262274
if tx.db == nil {
263275
return
264276
}
265277
if tx.writable {
266278
tx.db.freelist.rollback(tx.meta.txid)
267-
tx.db.freelist.reload(tx.db.page(tx.db.meta().freelist))
279+
if !tx.db.hasSyncedFreelist() {
280+
// Reconstruct free page list by scanning the DB to get the whole free page list.
281+
// Note: scaning the whole db is heavy if your db size is large in NoSyncFreeList mode.
282+
tx.db.freelist.noSyncReload(tx.db.freepages())
283+
} else {
284+
// Read free page list from freelist page.
285+
tx.db.freelist.reload(tx.db.page(tx.db.meta().freelist))
286+
}
268287
}
269288
tx.close()
270289
}

tx_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,57 @@ func TestTx_CopyFile_Error_Normal(t *testing.T) {
654654
}
655655
}
656656

657+
// TestTx_Rollback ensures there is no error when tx rollback whether we sync freelist or not.
658+
func TestTx_Rollback(t *testing.T) {
659+
for _, isSyncFreelist := range []bool{false, true} {
660+
// Open the database.
661+
db, err := bolt.Open(tempfile(), 0666, nil)
662+
if err != nil {
663+
log.Fatal(err)
664+
}
665+
defer os.Remove(db.Path())
666+
db.NoFreelistSync = isSyncFreelist
667+
668+
tx, err := db.Begin(true)
669+
if err != nil {
670+
t.Fatalf("Error starting tx: %v", err)
671+
}
672+
bucket := []byte("mybucket")
673+
if _, err := tx.CreateBucket(bucket); err != nil {
674+
t.Fatalf("Error creating bucket: %v", err)
675+
}
676+
if err := tx.Commit(); err != nil {
677+
t.Fatalf("Error on commit: %v", err)
678+
}
679+
680+
tx, err = db.Begin(true)
681+
if err != nil {
682+
t.Fatalf("Error starting tx: %v", err)
683+
}
684+
b := tx.Bucket(bucket)
685+
if err := b.Put([]byte("k"), []byte("v")); err != nil {
686+
t.Fatalf("Error on put: %v", err)
687+
}
688+
// Imagine there is an error and tx needs to be rolled-back
689+
if err := tx.Rollback(); err != nil {
690+
t.Fatalf("Error on rollback: %v", err)
691+
}
692+
693+
tx, err = db.Begin(false)
694+
if err != nil {
695+
t.Fatalf("Error starting tx: %v", err)
696+
}
697+
b = tx.Bucket(bucket)
698+
if v := b.Get([]byte("k")); v != nil {
699+
t.Fatalf("Value for k should not have been stored")
700+
}
701+
if err := tx.Rollback(); err != nil {
702+
t.Fatalf("Error on rollback: %v", err)
703+
}
704+
705+
}
706+
}
707+
657708
// TestTx_releaseRange ensures db.freePages handles page releases
658709
// correctly when there are transaction that are no longer reachable
659710
// via any read/write transactions and are "between" ongoing read

0 commit comments

Comments
 (0)