Skip to content

Commit af4612c

Browse files
authored
Merge pull request #6371 from useblacksmith/fix-history-db-corruption
Add corruption recovery to history.db
2 parents 15b43c8 + 18f1ff0 commit af4612c

File tree

3 files changed

+58
-53
lines changed

3 files changed

+58
-53
lines changed

cmd/buildkitd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ func newController(ctx context.Context, c *cli.Context, cfg *config.Config) (*co
828828
}
829829
cacheStoreForDebug = cacheStorage
830830

831-
historyDB, err := boltutil.Open(filepath.Join(cfg.Root, "history.db"), 0600, nil)
831+
historyDB, err := boltutil.SafeOpen(filepath.Join(cfg.Root, "history.db"), 0600, nil)
832832
if err != nil {
833833
return nil, err
834834
}

solver/bboltcachestorage/storage.go

Lines changed: 1 addition & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7-
"os"
87

9-
"github.com/moby/buildkit/identity"
108
"github.com/moby/buildkit/solver"
11-
"github.com/moby/buildkit/util/bklog"
129
"github.com/moby/buildkit/util/db"
1310
"github.com/moby/buildkit/util/db/boltutil"
1411
digest "github.com/opencontainers/go-digest"
@@ -28,7 +25,7 @@ type Store struct {
2825
}
2926

3027
func NewStore(dbPath string) (*Store, error) {
31-
db, err := safeOpenDB(dbPath, &bolt.Options{
28+
db, err := boltutil.SafeOpen(dbPath, 0600, &bolt.Options{
3229
NoSync: true,
3330
})
3431
if err != nil {
@@ -517,51 +514,3 @@ func isEmptyBucket(b *bolt.Bucket) bool {
517514
k, _ := b.Cursor().First()
518515
return k == nil
519516
}
520-
521-
// safeOpenDB opens a bolt database and recovers from panic that
522-
// can be caused by a corrupted database file.
523-
func safeOpenDB(dbPath string, opts *bolt.Options) (db db.DB, err error) {
524-
defer func() {
525-
if r := recover(); r != nil {
526-
err = errors.Errorf("%v", r)
527-
}
528-
529-
// If we get an error when opening the database, but we have
530-
// access to the file and the file looks like it has content,
531-
// then fallback to resetting the database since the database
532-
// may be corrupt.
533-
if err != nil && fileHasContent(dbPath) {
534-
db, err = fallbackOpenDB(dbPath, opts, err)
535-
}
536-
}()
537-
return openDB(dbPath, opts)
538-
}
539-
540-
// fallbackOpenDB performs database recovery and opens the new database
541-
// file when the database fails to open. Called after the first database
542-
// open fails.
543-
func fallbackOpenDB(dbPath string, opts *bolt.Options, openErr error) (db.DB, error) {
544-
backupPath := dbPath + "." + identity.NewID() + ".bak"
545-
bklog.L.Errorf("failed to open database file %s, resetting to empty. Old database is backed up to %s. "+
546-
"This error signifies that buildkitd likely crashed or was sigkilled abrubtly, leaving the database corrupted. "+
547-
"If you see logs from a previous panic then please report in the issue tracker at https://github.com/moby/buildkit . %+v", dbPath, backupPath, openErr)
548-
if err := os.Rename(dbPath, backupPath); err != nil {
549-
return nil, errors.Wrapf(err, "failed to rename database file %s to %s", dbPath, backupPath)
550-
}
551-
552-
// Attempt to open the database again. This should be a new database.
553-
// If this fails, it is a permanent error.
554-
return openDB(dbPath, opts)
555-
}
556-
557-
// openDB opens a bolt database in user-only read/write mode.
558-
func openDB(dbPath string, opts *bolt.Options) (db.DB, error) {
559-
return boltutil.Open(dbPath, 0600, opts)
560-
}
561-
562-
// fileHasContent checks if we have access to the file with appropriate
563-
// permissions and the file has a non-zero size.
564-
func fileHasContent(dbPath string) bool {
565-
st, err := os.Stat(dbPath)
566-
return err == nil && st.Size() > 0
567-
}

util/db/boltutil/safe_open.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package boltutil
2+
3+
import (
4+
"os"
5+
6+
"github.com/moby/buildkit/identity"
7+
"github.com/moby/buildkit/util/bklog"
8+
"github.com/moby/buildkit/util/db"
9+
"github.com/pkg/errors"
10+
bolt "go.etcd.io/bbolt"
11+
)
12+
13+
// SafeOpen opens a bolt database with automatic recovery from corruption.
14+
// If the database file is corrupted, it backs up the corrupted file and creates
15+
// a new empty database. This is useful for disposable databases like cache or
16+
// history where data loss is acceptable but startup failure is not.
17+
func SafeOpen(dbPath string, mode os.FileMode, opts *bolt.Options) (db db.DB, err error) {
18+
defer func() {
19+
if r := recover(); r != nil {
20+
err = errors.Errorf("%v", r)
21+
}
22+
23+
// If we get an error when opening the database, but we have
24+
// access to the file and the file looks like it has content,
25+
// then fallback to resetting the database since the database
26+
// may be corrupt.
27+
if err != nil && fileHasContent(dbPath) {
28+
db, err = fallbackOpen(dbPath, mode, opts, err)
29+
}
30+
}()
31+
return Open(dbPath, mode, opts)
32+
}
33+
34+
// fallbackOpen performs database recovery and opens a new database
35+
// file when the database fails to open. Called after the first database
36+
// open fails.
37+
func fallbackOpen(dbPath string, mode os.FileMode, opts *bolt.Options, openErr error) (db.DB, error) {
38+
backupPath := dbPath + "." + identity.NewID() + ".bak"
39+
bklog.L.Errorf("failed to open database file %s, resetting to empty. Old database is backed up to %s. "+
40+
"This error signifies that buildkitd likely crashed or was sigkilled abruptly, leaving the database corrupted. "+
41+
"If you see logs from a previous panic then please report in the issue tracker at https://github.com/moby/buildkit . %+v", dbPath, backupPath, openErr)
42+
if err := os.Rename(dbPath, backupPath); err != nil {
43+
return nil, errors.Wrapf(err, "failed to rename database file %s to %s", dbPath, backupPath)
44+
}
45+
46+
// Attempt to open the database again. This should be a new database.
47+
// If this fails, it is a permanent error.
48+
return Open(dbPath, mode, opts)
49+
}
50+
51+
// fileHasContent checks if we have access to the file with appropriate
52+
// permissions and the file has a non-zero size.
53+
func fileHasContent(dbPath string) bool {
54+
st, err := os.Stat(dbPath)
55+
return err == nil && st.Size() > 0
56+
}

0 commit comments

Comments
 (0)