diff --git a/database/error.go b/database/error.go index 3470c497..b2e290bc 100644 --- a/database/error.go +++ b/database/error.go @@ -62,6 +62,11 @@ const ( // the database was attempted against a read-only transaction. ErrTxNotWritable + // ErrAvailableDiskSpace indicates that the user is running out of + // disk space. The database preventively decided to not allow the + // transaction to prevent causing hard-to-detect problems. + ErrAvailableDiskSpace + // ************************************** // Errors related to metadata operations. // ************************************** @@ -143,6 +148,7 @@ var errorCodeStrings = map[ErrorCode]string{ ErrCorruption: "ErrCorruption", ErrTxClosed: "ErrTxClosed", ErrTxNotWritable: "ErrTxNotWritable", + ErrAvailableDiskSpace: "ErrAvailableDiskSpace", ErrBucketNotFound: "ErrBucketNotFound", ErrBucketExists: "ErrBucketExists", ErrBucketNameRequired: "ErrBucketNameRequired", diff --git a/database/error_test.go b/database/error_test.go index 759d26e1..e7965643 100644 --- a/database/error_test.go +++ b/database/error_test.go @@ -27,6 +27,7 @@ func TestErrorCodeStringer(t *testing.T) { {database.ErrCorruption, "ErrCorruption"}, {database.ErrTxClosed, "ErrTxClosed"}, {database.ErrTxNotWritable, "ErrTxNotWritable"}, + {database.ErrAvailableDiskSpace, "ErrAvailableDiskSpace"}, {database.ErrBucketNotFound, "ErrBucketNotFound"}, {database.ErrBucketExists, "ErrBucketExists"}, {database.ErrBucketNameRequired, "ErrBucketNameRequired"}, diff --git a/database/ffldb/db.go b/database/ffldb/db.go index 60103aaa..03ca48ed 100644 --- a/database/ffldb/db.go +++ b/database/ffldb/db.go @@ -43,6 +43,13 @@ const ( // The serialized block index row format is: // blockHdrOffset = blockLocSize + + // bytesMiB is the number of bytes in a mebibyte. + bytesMiB = 1024 * 1024 + + // minAvailableSpaceUpdate is the minimum space available (in bytes) to + // allow a write transaction. The value is 25 MiB. + minAvailableSpaceUpdate = 25 * bytesMiB ) var ( @@ -1885,6 +1892,22 @@ func (db *db) Type() string { // which is used by the managed transaction code while the database method // returns the interface. func (db *db) begin(writable bool) (*transaction, error) { + // Make sure there is enough available disk space so we can inform the + // user of the problem instead of causing a db failure. + if writable { + freeSpace, err := getAvailableDiskSpace() + if err != nil { + return nil, makeDbErr(database.ErrDriverSpecific, + "failed to inspect available disk space", err) + } + if freeSpace < minAvailableSpaceUpdate { + errMsg := fmt.Sprintf("available disk space too low: "+ + "%.1f MiB", float64(freeSpace)/float64(bytesMiB)) + return nil, makeDbErr(database.ErrAvailableDiskSpace, + errMsg, nil) + } + } + // Whenever a new writable transaction is started, grab the write lock // to ensure only a single write transaction can be active at the same // time. This lock will not be released until the transaction is diff --git a/database/ffldb/disk_posix.go b/database/ffldb/disk_posix.go new file mode 100644 index 00000000..f1cc98e7 --- /dev/null +++ b/database/ffldb/disk_posix.go @@ -0,0 +1,26 @@ +// +build !windows + +// Copyright (c) 2013-2018 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package ffldb + +import ( + "os" + "syscall" +) + +// getAvailableDiskSpace returns the number of bytes of available disk space. +func getAvailableDiskSpace() (uint64, error) { + var stat syscall.Statfs_t + + wd, err := os.Getwd() + if err != nil { + return 0, err + } + + syscall.Statfs(wd, &stat) + + return stat.Bavail * uint64(stat.Bsize), nil +} diff --git a/database/ffldb/disk_windows.go b/database/ffldb/disk_windows.go new file mode 100644 index 00000000..2b117713 --- /dev/null +++ b/database/ffldb/disk_windows.go @@ -0,0 +1,33 @@ +// +build windows + +// Copyright (c) 2013-2018 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package ffldb + +import ( + "os" + "syscall" + "unsafe" +) + +// getAvailableDiskSpace returns the number of bytes of available disk space. +func getAvailableDiskSpace() (uint64, error) { + wd, err := os.Getwd() + if err != nil { + return 0, err + } + + h := syscall.MustLoadDLL("kernel32.dll") + c := h.MustFindProc("GetDiskFreeSpaceExW") + + var freeBytes int64 + _, _, err := c.Call(uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(wd))), + uintptr(unsafe.Pointer(&freeBytes)), nil, nil) + if err != nil { + return 0, err + } + + return uint64(freeBytes), nil +}