Skip to content
This repository has been archived by the owner on Mar 9, 2019. It is now read-only.

Introduce InitialMmapSize to prevent deadlock #472

Merged
merged 1 commit into from
Dec 30, 2015
Merged

Introduce InitialMmapSize to prevent deadlock #472

merged 1 commit into from
Dec 30, 2015

Conversation

gyuho
Copy link
Contributor

@gyuho gyuho commented Dec 21, 2015

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 #432.

/cc @xiang90 @benbjohnson

I took this over from @xiang90 and confirmed that current implementation
easily causes deadlock when we pass 0 for db.mmap. You can simply reproduce
as follows:

package main

import (
    "fmt"
    "math/rand"
    "os"
    "time"

    "github.com/boltdb/bolt"
)

var (
    dbPath     = "test.db"
    bucketName = "test"

    numKeys = 100

    keyLen = 100
    valLen = 750

    keys = make([][]byte, numKeys)
    vals = make([][]byte, numKeys)
)

func init() {
    fmt.Println("Starting generating random data...")
    for i := range keys {
        keys[i] = randBytes(keyLen)
        vals[i] = randBytes(valLen)
    }
    fmt.Println("Done")
}

func main() {
    defer os.Remove(dbPath)

    initMmapSize := 1 << 31 // 2GB
    initMmapSize = 0        // comment this out to not block write

    var boltOpenOptions = &bolt.Options{InitialMmapSize: initMmapSize}

    db, err := bolt.Open(dbPath, 0600, boltOpenOptions)
    if err != nil {
        panic(err)
    }
    defer db.Close()

    fmt.Println("Creating bucket:", bucketName)
    if err := db.Update(func(tx *bolt.Tx) error {
        if _, err := tx.CreateBucket([]byte(bucketName)); err != nil {
            return err
        }
        return nil
    }); err != nil {
        panic(err)
    }
    fmt.Println("Done with creating bucket:", bucketName)

    fmt.Println("Starting reading...")
    go func() {
        _, err := db.Begin(false)
        if err != nil {
            panic(err)
        }
        fmt.Println("before select")
        select {}
        fmt.Println("after select")
    }()

    fmt.Println("Starting writing...")
    for {
        for j := range keys {
            tw := time.Now()
            if err := db.Update(func(tx *bolt.Tx) error {
                b := tx.Bucket([]byte(bucketName))
                if err := b.Put(keys[j], vals[j]); err != nil {
                    return err
                }
                return nil
            }); err != nil {
                panic(err)
            }
            fmt.Printf("#%d write took: %v\n", j, time.Since(tw))
        }
    }
}

func randBytes(n int) []byte {
    const (
        letterBytes   = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
        letterIdxBits = 6                    // 6 bits to represent a letter index
        letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
        letterIdxMax  = 63 / letterIdxBits   // # of letter indices fitting in 63 bits
    )
    src := rand.NewSource(time.Now().UnixNano())
    b := make([]byte, n)
    for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
        if remain == 0 {
            cache, remain = src.Int63(), letterIdxMax
        }
        if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
            b[i] = letterBytes[idx]
            i--
        }
        cache >>= letterIdxBits
        remain--
    }
    return b
}

And it errors with deadlock:

go run a.go 
Starting generating random data...
Done
Creating bucket: test
Done with creating bucket: test
Starting reading...
Starting writing...
before select
#0 write took: 16.612708ms
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc82005e980)
    /usr/local/go/src/runtime/sema.go:43 +0x26
sync.(*RWMutex).Lock(0xc82005e978)
    /usr/local/go/src/sync/rwmutex.go:87 +0xa1
github.com/boltdb/bolt.(*DB).mmap(0xc82005e820, 0x8000, 0x0, 0x0)
    /home/gyuho/go/src/github.com/boltdb/bolt/db.go:215 +0x55
github.com/boltdb/bolt.(*DB).allocate(0xc82005e820, 0x1, 0x0, 0x0, 0x0)
    /home/gyuho/go/src/github.com/boltdb/bolt/db.go:656 +0x149
github.com/boltdb/bolt.(*Tx).allocate(0xc8201a21c0, 0x1, 0xc820030040, 0x0, 0x0)
    /home/gyuho/go/src/github.com/boltdb/bolt/tx.go:412 +0x3f
github.com/boltdb/bolt.(*node).spill(0xc82014aee0, 0x0, 0x0)
    /home/gyuho/go/src/github.com/boltdb/bolt/node.go:363 +0x323
github.com/boltdb/bolt.(*Bucket).spill(0xc820012580, 0x0, 0x0)
    /home/gyuho/go/src/github.com/boltdb/bolt/bucket.go:541 +0x1d9
github.com/boltdb/bolt.(*Bucket).spill(0xc8201a21d8, 0x0, 0x0)
    /home/gyuho/go/src/github.com/boltdb/bolt/bucket.go:508 +0xbcc
github.com/boltdb/bolt.(*Tx).Commit(0xc8201a21c0, 0x0, 0x0)
    /home/gyuho/go/src/github.com/boltdb/bolt/tx.go:162 +0x1d4
github.com/boltdb/bolt.(*DB).Update(0xc82005e820, 0xc820041e98, 0x0, 0x0)
    /home/gyuho/go/src/github.com/boltdb/bolt/db.go:563 +0x11d
main.main()
    /home/gyuho/a.go:74 +0x6f0

goroutine 5 [select (no cases)]:
main.main.func2(0xc82005e820)
    /home/gyuho/a.go:66 +0x139
created by main.main
    /home/gyuho/a.go:68 +0x58e
exit status 2

etcd needs to set InitialMmapSize because long-running read transaction
(e.g. snapshop transaction) should never block write transaction.

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 #432.
@gyuho
Copy link
Contributor Author

gyuho commented Dec 21, 2015

Tested and it worked:

~/go/src/github.com/boltdb/bolt $ go test -run TestDB_Open_InitialMmapSize -v

seed: 92038
quick settings: count=5, items=1000, ksize=1024, vsize=1024

=== RUN   TestDB_Open_InitialMmapSize
--- PASS: TestDB_Open_InitialMmapSize (1.40s)

PASS
ok      github.com/boltdb/bolt  1.424s

@benbjohnson
Copy link
Member

Sorry for the delay. lgtm.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants