From 082efcc23e41f42d15f9e90f4e079fdb793e997f Mon Sep 17 00:00:00 2001 From: Gyu-Ho Lee Date: Mon, 21 Dec 2015 15:28:44 -0800 Subject: [PATCH] Introduce InitialMmapSize to prevent deadlock InitialMmapSize is the initial mmap size of the database in bytes. Read transaction won't block write transaction if InitialMmapSize is large enough to handle mmap size. Copied from /~https://github.com/boltdb/bolt/pull/432. --- db.go | 16 +++++++++++++++- db_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/db.go b/db.go index d5ad7151..79240563 100644 --- a/db.go +++ b/db.go @@ -196,7 +196,7 @@ func Open(path string, mode os.FileMode, options *Options) (*DB, error) { } // Memory map the data file. - if err := db.mmap(0); err != nil { + if err := db.mmap(options.InitialMmapSize); err != nil { _ = db.close() return nil, err } @@ -411,6 +411,10 @@ func (db *DB) close() error { // writer to deadlock because the database periodically needs to re-mmap itself // as it grows and it cannot do that while a read transaction is open. // +// If a long running read transaction (for example, a snapshot transaction) is +// needed, you might want to set DB.InitialMmapSize to a large enough value +// to avoid potential blocking of write transaction. +// // IMPORTANT: You must close read-only transactions after you are finished or // else the database will not reclaim old pages. func (db *DB) Begin(writable bool) (*Tx, error) { @@ -680,6 +684,16 @@ type Options struct { // Sets the DB.MmapFlags flag before memory mapping the file. MmapFlags int + + // InitialMmapSize is the initial mmap size of the database + // in bytes. Read transactions won't block write transaction + // if the InitialMmapSize is large enough to hold database mmap + // size. (See DB.Begin for more information) + // + // If <=0, the initial map size is 0. + // If initialMmapSize is smaller than the previous database size, + // it takes no effect. + InitialMmapSize int } // DefaultOptions represent the options used if nil options are passed into Open(). diff --git a/db_test.go b/db_test.go index aa8a2f26..197071bd 100644 --- a/db_test.go +++ b/db_test.go @@ -368,6 +368,51 @@ func TestOpen_ReadOnly(t *testing.T) { } } +// TestDB_Open_InitialMmapSize tests if having InitialMmapSize large enough +// to hold data from concurrent write transaction resolves the issue that +// read transaction blocks the write transaction and causes deadlock. +// This is a very hacky test since the mmap size is not exposed. +func TestDB_Open_InitialMmapSize(t *testing.T) { + path := tempfile() + defer os.Remove(path) + + initMmapSize := 1 << 31 // 2GB + testWriteSize := 1 << 27 // 134MB + + db, err := bolt.Open(path, 0666, &bolt.Options{InitialMmapSize: initMmapSize}) + assert(t, err == nil, "") + + // create a long-running read transaction + // that never gets closed while writing + rtx, err := db.Begin(false) + assert(t, err == nil, "") + defer rtx.Rollback() + + // create a write transaction + wtx, err := db.Begin(true) + assert(t, err == nil, "") + + b, err := wtx.CreateBucket([]byte("test")) + assert(t, err == nil, "") + + // and commit a large write + err = b.Put([]byte("foo"), make([]byte, testWriteSize)) + assert(t, err == nil, "") + + done := make(chan struct{}) + + go func() { + wtx.Commit() + done <- struct{}{} + }() + + select { + case <-time.After(5 * time.Second): + t.Errorf("unexpected that the reader blocks writer") + case <-done: + } +} + // TODO(benbjohnson): Test corruption at every byte of the first two pages. // Ensure that a database cannot open a transaction when it's not open.