From f9d3ff664842d4d0f7d73f29c64433a415575c34 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Tue, 28 Apr 2020 20:30:23 +0000 Subject: [PATCH 01/18] Fix incorrect unsafe usage After checkptr fixes by 2fc6815c, it was discovered that new issues were hit in production systems, in particular when a single process opened and updated multiple separate databases. This indicates that some bug relating to bad unsafe usage was introduced during this commit. This commit combines several attempts at fixing this new issue. For example, slices are once again created by slicing an array of "max allocation" elements, but this time with the cap set to the intended length. This operation is espressly permitted according to the Go wiki, so it should be preferred to type converting a reflect.SliceHeader. --- freelist.go | 46 ++++++++++++++------------------------------- node.go | 25 ++++++++++--------------- node_test.go | 6 +++--- page.go | 53 +++++++++++++++++----------------------------------- tx.go | 27 ++++++++------------------ unsafe.go | 15 +++++++++++++++ 6 files changed, 67 insertions(+), 105 deletions(-) create mode 100644 unsafe.go diff --git a/freelist.go b/freelist.go index d441b6925..0e7369c85 100644 --- a/freelist.go +++ b/freelist.go @@ -2,7 +2,6 @@ package bbolt import ( "fmt" - "reflect" "sort" "unsafe" ) @@ -94,24 +93,8 @@ func (f *freelist) pending_count() int { return count } -// copyallunsafe copies a list of all free ids and all pending ids in one sorted list. +// copyall copies a list of all free ids and all pending ids in one sorted list. // f.count returns the minimum length required for dst. -func (f *freelist) copyallunsafe(dstptr unsafe.Pointer) { // dstptr is []pgid data pointer - m := make(pgids, 0, f.pending_count()) - for _, txp := range f.pending { - m = append(m, txp.ids...) - } - sort.Sort(m) - fpgids := f.getFreePageIDs() - sz := len(fpgids) + len(m) - dst := *(*[]pgid)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(dstptr), - Len: sz, - Cap: sz, - })) - mergepgids(dst, fpgids, m) -} - func (f *freelist) copyall(dst []pgid) { m := make(pgids, 0, f.pending_count()) for _, txp := range f.pending { @@ -287,18 +270,15 @@ func (f *freelist) read(p *page) { var idx, count uintptr = 0, uintptr(p.count) if count == 0xFFFF { idx = 1 - count = uintptr(*(*pgid)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p)))) + count = uintptr(*(*pgid)(unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)))) } // Copy the list of page ids from the freelist. if count == 0 { f.ids = nil } else { - ids := *(*[]pgid)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p) + idx*unsafe.Sizeof(pgid(0)), - Len: int(count), - Cap: int(count), - })) + ids := (*[maxAllocSize]pgid)(unsafeAdd( + unsafe.Pointer(p), unsafe.Sizeof(*p)))[idx : idx+count : idx+count] // copy the ids, so we don't modify on the freelist page directly idsCopy := make([]pgid, count) @@ -331,16 +311,18 @@ func (f *freelist) write(p *page) error { // The page.count can only hold up to 64k elements so if we overflow that // number then we handle it by putting the size in the first element. - lenids := f.count() - if lenids == 0 { - p.count = uint16(lenids) - } else if lenids < 0xFFFF { - p.count = uint16(lenids) - f.copyallunsafe(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p))) + l := f.count() + if l == 0 { + p.count = uint16(l) + } else if l < 0xFFFF { + p.count = uint16(l) + ids := (*[maxAllocSize]pgid)(unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)))[:l:l] + f.copyall(ids) } else { p.count = 0xFFFF - *(*pgid)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p))) = pgid(lenids) - f.copyallunsafe(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p) + unsafe.Sizeof(pgid(0)))) + ids := (*[maxAllocSize]pgid)(unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)))[: l+1 : l+1] + ids[0] = pgid(l) + f.copyall(ids[1:]) } return nil diff --git a/node.go b/node.go index 1690eef3f..73988b5c4 100644 --- a/node.go +++ b/node.go @@ -3,7 +3,6 @@ package bbolt import ( "bytes" "fmt" - "reflect" "sort" "unsafe" ) @@ -208,36 +207,32 @@ func (n *node) write(p *page) { } // Loop over each item and write it to the page. - bp := uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p) + n.pageElementSize()*uintptr(len(n.inodes)) + // off tracks the offset into p of the start of the next data. + off := unsafe.Sizeof(*p) + n.pageElementSize()*uintptr(len(n.inodes)) for i, item := range n.inodes { _assert(len(item.key) > 0, "write: zero-length inode key") + // Create a slice to write into of needed size and advance + // byte pointer for next iteration. + sz := len(item.key) + len(item.value) + b := unsafeByteSlice(unsafe.Pointer(p), off, 0, sz) + off += uintptr(sz) + // Write the page element. if n.isLeaf { elem := p.leafPageElement(uint16(i)) - elem.pos = uint32(bp - uintptr(unsafe.Pointer(elem))) + elem.pos = uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem))) elem.flags = item.flags elem.ksize = uint32(len(item.key)) elem.vsize = uint32(len(item.value)) } else { elem := p.branchPageElement(uint16(i)) - elem.pos = uint32(bp - uintptr(unsafe.Pointer(elem))) + elem.pos = uint32(uintptr(unsafe.Pointer(&b[0])) - uintptr(unsafe.Pointer(elem))) elem.ksize = uint32(len(item.key)) elem.pgid = item.pgid _assert(elem.pgid != p.id, "write: circular dependency occurred") } - // Create a slice to write into of needed size and advance - // byte pointer for next iteration. - klen, vlen := len(item.key), len(item.value) - sz := klen + vlen - b := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: bp, - Len: sz, - Cap: sz, - })) - bp += uintptr(sz) - // Write data for the element to the end of the page. l := copy(b, item.key) copy(b[l:], item.value) diff --git a/node_test.go b/node_test.go index 85a9dc218..eea4d2582 100644 --- a/node_test.go +++ b/node_test.go @@ -44,9 +44,9 @@ func TestNode_read_LeafPage(t *testing.T) { nodes[1] = leafPageElement{flags: 0, pos: 23, ksize: 10, vsize: 3} // pos = sizeof(leafPageElement) + 3 + 4 // Write data for the nodes at the end. - data := (*[4096]byte)(unsafe.Pointer(&nodes[2])) - copy(data[:], "barfooz") - copy(data[7:], "helloworldbye") + const s = "barfoozhelloworldbye" + data := unsafeByteSlice(unsafe.Pointer(&nodes[2]), 0, 0, len(s)) + copy(data, s) // Deserialize page into a leaf. n := &node{} diff --git a/page.go b/page.go index 415cd77ec..3cfd94e9d 100644 --- a/page.go +++ b/page.go @@ -3,7 +3,6 @@ package bbolt import ( "fmt" "os" - "reflect" "sort" "unsafe" ) @@ -51,13 +50,13 @@ func (p *page) typ() string { // meta returns a pointer to the metadata section of the page. func (p *page) meta() *meta { - return (*meta)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p))) + return (*meta)(unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))) } // leafPageElement retrieves the leaf node by index func (p *page) leafPageElement(index uint16) *leafPageElement { - off := uintptr(index) * leafPageElementSize - return (*leafPageElement)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p) + off)) + return (*leafPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), + leafPageElementSize, int(index))) } // leafPageElements retrieves a list of leaf nodes. @@ -65,17 +64,14 @@ func (p *page) leafPageElements() []leafPageElement { if p.count == 0 { return nil } - return *(*[]leafPageElement)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p), - Len: int(p.count), - Cap: int(p.count), - })) + return (*[maxAllocSize]leafPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), + unsafe.Sizeof(leafPageElement{}), 0))[:p.count:p.count] } // branchPageElement retrieves the branch node by index func (p *page) branchPageElement(index uint16) *branchPageElement { - off := uintptr(index) * unsafe.Sizeof(branchPageElement{}) - return (*branchPageElement)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p) + off)) + return (*branchPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), + unsafe.Sizeof(branchPageElement{}), int(index))) } // branchPageElements retrieves a list of branch nodes. @@ -83,20 +79,13 @@ func (p *page) branchPageElements() []branchPageElement { if p.count == 0 { return nil } - return *(*[]branchPageElement)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p), - Len: int(p.count), - Cap: int(p.count), - })) + return (*[maxAllocSize]branchPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), + unsafe.Sizeof(branchPageElement{}), 0))[:p.count:p.count] } // dump writes n bytes of the page to STDERR as hex output. func (p *page) hexdump(n int) { - buf := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(p)), - Len: n, - Cap: n, - })) + buf := unsafeByteSlice(unsafe.Pointer(p), 0, 0, n) fmt.Fprintf(os.Stderr, "%x\n", buf) } @@ -115,11 +104,7 @@ type branchPageElement struct { // key returns a byte slice of the node key. func (n *branchPageElement) key() []byte { - return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(n)) + uintptr(n.pos), - Len: int(n.ksize), - Cap: int(n.ksize), - })) + return unsafeByteSlice(unsafe.Pointer(n), 0, int(n.pos), int(n.pos)+int(n.ksize)) } // leafPageElement represents a node on a leaf page. @@ -132,20 +117,16 @@ type leafPageElement struct { // key returns a byte slice of the node key. func (n *leafPageElement) key() []byte { - return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(n)) + uintptr(n.pos), - Len: int(n.ksize), - Cap: int(n.ksize), - })) + i := int(n.pos) + j := i + int(n.ksize) + return unsafeByteSlice(unsafe.Pointer(n), 0, i, j) } // value returns a byte slice of the node value. func (n *leafPageElement) value() []byte { - return *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(n)) + uintptr(n.pos) + uintptr(n.ksize), - Len: int(n.vsize), - Cap: int(n.vsize), - })) + i := int(n.pos) + int(n.ksize) + j := i + int(n.vsize) + return unsafeByteSlice(unsafe.Pointer(n), 0, i, j) } // PageInfo represents human readable information about a page. diff --git a/tx.go b/tx.go index 13937cdbf..4b1a64a8b 100644 --- a/tx.go +++ b/tx.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "os" - "reflect" "sort" "strings" "time" @@ -524,24 +523,18 @@ func (tx *Tx) write() error { // Write pages to disk in order. for _, p := range pages { - size := (int(p.overflow) + 1) * tx.db.pageSize + rem := (uint64(p.overflow) + 1) * uint64(tx.db.pageSize) offset := int64(p.id) * int64(tx.db.pageSize) + var written uintptr // Write out page in "max allocation" sized chunks. - ptr := uintptr(unsafe.Pointer(p)) for { - // Limit our write to our max allocation size. - sz := size + sz := rem if sz > maxAllocSize-1 { sz = maxAllocSize - 1 } + buf := unsafeByteSlice(unsafe.Pointer(p), written, 0, int(sz)) - // Write chunk to disk. - buf := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: ptr, - Len: sz, - Cap: sz, - })) if _, err := tx.db.ops.writeAt(buf, offset); err != nil { return err } @@ -550,14 +543,14 @@ func (tx *Tx) write() error { tx.stats.Write++ // Exit inner for loop if we've written all the chunks. - size -= sz - if size == 0 { + rem -= sz + if rem == 0 { break } // Otherwise move offset forward and move pointer to next chunk. offset += int64(sz) - ptr += uintptr(sz) + written += uintptr(sz) } } @@ -576,11 +569,7 @@ func (tx *Tx) write() error { continue } - buf := *(*[]byte)(unsafe.Pointer(&reflect.SliceHeader{ - Data: uintptr(unsafe.Pointer(p)), - Len: tx.db.pageSize, - Cap: tx.db.pageSize, - })) + buf := unsafeByteSlice(unsafe.Pointer(p), 0, 0, tx.db.pageSize) // See https://go.googlesource.com/go/+/f03c9202c43e0abb130669852082117ca50aa9b1 for i := range buf { diff --git a/unsafe.go b/unsafe.go new file mode 100644 index 000000000..d6c0c049e --- /dev/null +++ b/unsafe.go @@ -0,0 +1,15 @@ +package bbolt + +import "unsafe" + +func unsafeAdd(base unsafe.Pointer, offset uintptr) unsafe.Pointer { + return unsafe.Pointer(uintptr(base) + offset) +} + +func unsafeIndex(base unsafe.Pointer, offset uintptr, elemsz uintptr, n int) unsafe.Pointer { + return unsafe.Pointer(uintptr(base) + offset + uintptr(n)*elemsz) +} + +func unsafeByteSlice(base unsafe.Pointer, offset uintptr, i, j int) []byte { + return (*[maxAllocSize]byte)(unsafeAdd(base, offset))[i:j:j] +} From f0005d4d30923ce6c583c6af2dd990134fb3d826 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Thu, 30 Apr 2020 21:08:15 +0000 Subject: [PATCH 02/18] Comment the byte slice conversion --- unsafe.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/unsafe.go b/unsafe.go index d6c0c049e..3a5fb26cf 100644 --- a/unsafe.go +++ b/unsafe.go @@ -11,5 +11,15 @@ func unsafeIndex(base unsafe.Pointer, offset uintptr, elemsz uintptr, n int) uns } func unsafeByteSlice(base unsafe.Pointer, offset uintptr, i, j int) []byte { + // See: https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices + // + // This memory is not allocated from C, but it is unmanaged by Go's + // garbage collector and should behave similarly, and the compiler + // should produce similar code. Note that this conversion allows a + // subslice to begin after the base address, with an optional offset, + // while the URL above does not cover this case and only slices from + // index 0. However, the wiki never says that the address must be to + // the beginning of a C allocation (or even that malloc was used at + // all), so this is believed to be correct. return (*[maxAllocSize]byte)(unsafeAdd(base, offset))[i:j:j] } From e04f391ee01d815889785c7b02164d0e4f9a49cb Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Thu, 30 Apr 2020 22:26:53 +0000 Subject: [PATCH 03/18] go fmt --- unsafe.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unsafe.go b/unsafe.go index 3a5fb26cf..a5e2a1a04 100644 --- a/unsafe.go +++ b/unsafe.go @@ -12,7 +12,7 @@ func unsafeIndex(base unsafe.Pointer, offset uintptr, elemsz uintptr, n int) uns func unsafeByteSlice(base unsafe.Pointer, offset uintptr, i, j int) []byte { // See: https://github.com/golang/go/wiki/cgo#turning-c-arrays-into-go-slices - // + // // This memory is not allocated from C, but it is unmanaged by Go's // garbage collector and should behave similarly, and the compiler // should produce similar code. Note that this conversion allows a From 81f25783ae43c0699147f7d8b251753ede487a5b Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Sun, 3 May 2020 17:32:57 +0000 Subject: [PATCH 04/18] Shorten max array lengths for the data type --- page.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/page.go b/page.go index 3cfd94e9d..334f0ab8c 100644 --- a/page.go +++ b/page.go @@ -64,7 +64,8 @@ func (p *page) leafPageElements() []leafPageElement { if p.count == 0 { return nil } - return (*[maxAllocSize]leafPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), + const maxArraySize = maxAllocSize / unsafe.Sizeof(leafPageElement{}) + return (*[maxArraySize]leafPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), unsafe.Sizeof(leafPageElement{}), 0))[:p.count:p.count] } @@ -79,7 +80,8 @@ func (p *page) branchPageElements() []branchPageElement { if p.count == 0 { return nil } - return (*[maxAllocSize]branchPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), + const maxArraySize = maxAllocSize / unsafe.Sizeof(branchPageElement{}) + return (*[maxArraySize]branchPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), unsafe.Sizeof(branchPageElement{}), 0))[:p.count:p.count] } From 9034717d690b8fe1bb56de1802f621b83fadf676 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Thu, 21 May 2020 18:50:41 +0000 Subject: [PATCH 05/18] Try to use reflect.SliceHeader correctly this time --- freelist.go | 21 +++++++++++++++------ page.go | 14 ++++++++------ unsafe.go | 16 +++++++++++++++- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/freelist.go b/freelist.go index 0e7369c85..697a46968 100644 --- a/freelist.go +++ b/freelist.go @@ -267,18 +267,23 @@ func (f *freelist) read(p *page) { } // If the page.count is at the max uint16 value (64k) then it's considered // an overflow and the size of the freelist is stored as the first element. - var idx, count uintptr = 0, uintptr(p.count) + var idx, count = 0, int(p.count) if count == 0xFFFF { idx = 1 - count = uintptr(*(*pgid)(unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)))) + c := *(*pgid)(unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p))) + count = int(c) + if count < 0 { + panic(fmt.Sprintf("leading element count %d overflows int", c)) + } } // Copy the list of page ids from the freelist. if count == 0 { f.ids = nil } else { - ids := (*[maxAllocSize]pgid)(unsafeAdd( - unsafe.Pointer(p), unsafe.Sizeof(*p)))[idx : idx+count : idx+count] + var ids []pgid + data := unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), unsafe.Sizeof(ids[0]), idx) + unsafeSlice(unsafe.Pointer(&ids), data, count) // copy the ids, so we don't modify on the freelist page directly idsCopy := make([]pgid, count) @@ -316,11 +321,15 @@ func (f *freelist) write(p *page) error { p.count = uint16(l) } else if l < 0xFFFF { p.count = uint16(l) - ids := (*[maxAllocSize]pgid)(unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)))[:l:l] + var ids []pgid + data := unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)) + unsafeSlice(unsafe.Pointer(&ids), data, l) f.copyall(ids) } else { p.count = 0xFFFF - ids := (*[maxAllocSize]pgid)(unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)))[: l+1 : l+1] + var ids []pgid + data := unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)) + unsafeSlice(unsafe.Pointer(&ids), data, l+1) ids[0] = pgid(l) f.copyall(ids[1:]) } diff --git a/page.go b/page.go index 334f0ab8c..c9a158fb0 100644 --- a/page.go +++ b/page.go @@ -64,9 +64,10 @@ func (p *page) leafPageElements() []leafPageElement { if p.count == 0 { return nil } - const maxArraySize = maxAllocSize / unsafe.Sizeof(leafPageElement{}) - return (*[maxArraySize]leafPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), - unsafe.Sizeof(leafPageElement{}), 0))[:p.count:p.count] + var elems []leafPageElement + data := unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)) + unsafeSlice(unsafe.Pointer(&elems), data, int(p.count)) + return elems } // branchPageElement retrieves the branch node by index @@ -80,9 +81,10 @@ func (p *page) branchPageElements() []branchPageElement { if p.count == 0 { return nil } - const maxArraySize = maxAllocSize / unsafe.Sizeof(branchPageElement{}) - return (*[maxArraySize]branchPageElement)(unsafeIndex(unsafe.Pointer(p), unsafe.Sizeof(*p), - unsafe.Sizeof(branchPageElement{}), 0))[:p.count:p.count] + var elems []branchPageElement + data := unsafeAdd(unsafe.Pointer(p), unsafe.Sizeof(*p)) + unsafeSlice(unsafe.Pointer(&elems), data, int(p.count)) + return elems } // dump writes n bytes of the page to STDERR as hex output. diff --git a/unsafe.go b/unsafe.go index a5e2a1a04..c0e503750 100644 --- a/unsafe.go +++ b/unsafe.go @@ -1,6 +1,9 @@ package bbolt -import "unsafe" +import ( + "reflect" + "unsafe" +) func unsafeAdd(base unsafe.Pointer, offset uintptr) unsafe.Pointer { return unsafe.Pointer(uintptr(base) + offset) @@ -23,3 +26,14 @@ func unsafeByteSlice(base unsafe.Pointer, offset uintptr, i, j int) []byte { // all), so this is believed to be correct. return (*[maxAllocSize]byte)(unsafeAdd(base, offset))[i:j:j] } + +// unsafeSlice modifies the data, len, and cap of a slice variable pointed to by +// the slice parameter. This helper should be used over other direct +// manipulation of reflect.SliceHeader to prevent misuse, namely, converting +// from reflect.SliceHeader to a Go slice type. +func unsafeSlice(slice, data unsafe.Pointer, len int) { + s := (*reflect.SliceHeader)(slice) + s.Data = uintptr(data) + s.Cap = len + s.Len = len +} From 044f3bd014e719214986768bae4fcbd66951fc94 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Sat, 23 May 2020 16:58:44 +0000 Subject: [PATCH 06/18] Test many DBs used concurrently --- manydbs_test.go | 67 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 manydbs_test.go diff --git a/manydbs_test.go b/manydbs_test.go new file mode 100644 index 000000000..fbbe8fe2e --- /dev/null +++ b/manydbs_test.go @@ -0,0 +1,67 @@ +package bbolt + +import ( + "fmt" + "io/ioutil" + "math/rand" + "os" + "path/filepath" + "testing" +) + +func createDb(t *testing.T) (*DB, func()) { + // First, create a temporary directory to be used for the duration of + // this test. + tempDirName, err := ioutil.TempDir("", "bboltmemtest") + if err != nil { + t.Fatalf("error creating temp dir: %v", err) + } + path := filepath.Join(tempDirName, "testdb.db") + + bdb, err := Open(path, 0600, nil) + if err != nil { + t.Fatalf("error creating bbolt db: %v", err) + } + + cleanup := func() { + bdb.Close() + os.RemoveAll(tempDirName) + } + + return bdb, cleanup +} + +func createAndPutKeys(t *testing.T) { + t.Parallel() + + db, cleanup := createDb(t) + defer cleanup() + + bucketName := []byte("bucket") + + for i := 0; i < 100; i++ { + err := db.Update(func(tx *Tx) error { + nodes, err := tx.CreateBucketIfNotExists(bucketName) + if err != nil { + return err + } + + var key [16]byte + rand.Read(key[:]) + if err := nodes.Put(key[:], nil); err != nil { + return err + } + + return nil + }) + if err != nil { + t.Fatal(err) + } + } +} + +func TestManyDBs(t *testing.T) { + for i := 0; i < 100; i++ { + t.Run(fmt.Sprintf("%d", i), createAndPutKeys) + } +} From f6be82302843a215152f5a1daf652c1ee5503f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cenk=20Alt=C4=B1?= Date: Fri, 7 Aug 2020 23:57:53 +0300 Subject: [PATCH 07/18] Add Rain torrent library to projects section in README (#232) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c9e64b1a6..6b5ed3cc4 100644 --- a/README.md +++ b/README.md @@ -941,6 +941,7 @@ Below is a list of public, open source projects that use Bolt: * [Operation Go: A Routine Mission](http://gocode.io) - An online programming game for Golang using Bolt for user accounts and a leaderboard. * [photosite/session](https://godoc.org/bitbucket.org/kardianos/photosite/session) - Sessions for a photo viewing site. * [Prometheus Annotation Server](https://github.com/oliver006/prom_annotation_server) - Annotation server for PromDash & Prometheus service monitoring system. +* [Rain](https://github.com/cenkalti/rain) - BitTorrent client and library. * [reef-pi](https://github.com/reef-pi/reef-pi) - reef-pi is an award winning, modular, DIY reef tank controller using easy to learn electronics based on a Raspberry Pi. * [Request Baskets](https://github.com/darklynx/request-baskets) - A web service to collect arbitrary HTTP requests and inspect them via REST API or simple web UI, similar to [RequestBin](http://requestb.in/) service * [Seaweed File System](https://github.com/chrislusf/seaweedfs) - Highly scalable distributed key~file system with O(1) disk read. From 74e833b57266f0ecd4deb7157be785aa134257bc Mon Sep 17 00:00:00 2001 From: Tobias Klauser Date: Thu, 24 Sep 2020 11:49:36 +0200 Subject: [PATCH 08/18] Use madvise syscall wrapper from golang.org/x/sys/unix Direct syscalls using syscall.Syscall(SYS_*, ...) should no longer be used on darwin, see [1]. Instead, use the madvise syscall wrapper provided by the golang.org/x/sys/unix package for all unix platforms. This implement the same functionality. [1] https://golang.org/doc/go1.12#darwin As suggested by @ptabor in https://github.com/etcd-io/etcd/pull/12316#issuecomment-698193671 --- .travis.yml | 1 + bolt_unix.go | 13 +++---------- go.mod | 2 +- go.sum | 4 ++-- 4 files changed, 7 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 257dfdfee..4dbe73a15 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ go: - 1.12 before_install: +- go get -v golang.org/x/sys/unix - go get -v honnef.co/go/tools/... - go get -v github.com/kisielk/errcheck diff --git a/bolt_unix.go b/bolt_unix.go index 2938fed58..34753e81f 100644 --- a/bolt_unix.go +++ b/bolt_unix.go @@ -7,6 +7,8 @@ import ( "syscall" "time" "unsafe" + + "golang.org/x/sys/unix" ) // flock acquires an advisory lock on a file descriptor. @@ -55,7 +57,7 @@ func mmap(db *DB, sz int) error { } // Advise the kernel that the mmap is accessed randomly. - err = madvise(b, syscall.MADV_RANDOM) + err = unix.Madvise(b, syscall.MADV_RANDOM) if err != nil && err != syscall.ENOSYS { // Ignore not implemented error in kernel because it still works. return fmt.Errorf("madvise: %s", err) @@ -82,12 +84,3 @@ func munmap(db *DB) error { db.datasz = 0 return err } - -// NOTE: This function is copied from stdlib because it is not available on darwin. -func madvise(b []byte, advice int) (err error) { - _, _, e1 := syscall.Syscall(syscall.SYS_MADVISE, uintptr(unsafe.Pointer(&b[0])), uintptr(len(b)), uintptr(advice)) - if e1 != 0 { - err = e1 - } - return -} diff --git a/go.mod b/go.mod index c2366daef..96355a69b 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module go.etcd.io/bbolt go 1.12 -require golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 +require golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d diff --git a/go.sum b/go.sum index 4ad15a488..c13f8f470 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 56f6a306577f7b2a029d138cfcabdd391b6929e6 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Fri, 16 Oct 2020 12:51:14 -0400 Subject: [PATCH 09/18] fix etcd-io/bbolt#247 move compact logic - add todo --- cmd/bbolt/main.go | 110 +------------------------------------------- compact.go | 114 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 109 deletions(-) create mode 100644 compact.go diff --git a/cmd/bbolt/main.go b/cmd/bbolt/main.go index 91a13bcc6..d8f182e51 100644 --- a/cmd/bbolt/main.go +++ b/cmd/bbolt/main.go @@ -1993,7 +1993,7 @@ func (cmd *CompactCommand) Run(args ...string) (err error) { defer dst.Close() // Run compaction. - if err := cmd.compact(dst, src); err != nil { + if err := bolt.Compact(dst, src, cmd.TxMaxSize); err != nil { return err } @@ -2009,114 +2009,6 @@ func (cmd *CompactCommand) Run(args ...string) (err error) { return nil } -func (cmd *CompactCommand) compact(dst, src *bolt.DB) error { - // commit regularly, or we'll run out of memory for large datasets if using one transaction. - var size int64 - tx, err := dst.Begin(true) - if err != nil { - return err - } - defer tx.Rollback() - - if err := cmd.walk(src, func(keys [][]byte, k, v []byte, seq uint64) error { - // On each key/value, check if we have exceeded tx size. - sz := int64(len(k) + len(v)) - if size+sz > cmd.TxMaxSize && cmd.TxMaxSize != 0 { - // Commit previous transaction. - if err := tx.Commit(); err != nil { - return err - } - - // Start new transaction. - tx, err = dst.Begin(true) - if err != nil { - return err - } - size = 0 - } - size += sz - - // Create bucket on the root transaction if this is the first level. - nk := len(keys) - if nk == 0 { - bkt, err := tx.CreateBucket(k) - if err != nil { - return err - } - if err := bkt.SetSequence(seq); err != nil { - return err - } - return nil - } - - // Create buckets on subsequent levels, if necessary. - b := tx.Bucket(keys[0]) - if nk > 1 { - for _, k := range keys[1:] { - b = b.Bucket(k) - } - } - - // Fill the entire page for best compaction. - b.FillPercent = 1.0 - - // If there is no value then this is a bucket call. - if v == nil { - bkt, err := b.CreateBucket(k) - if err != nil { - return err - } - if err := bkt.SetSequence(seq); err != nil { - return err - } - return nil - } - - // Otherwise treat it as a key/value pair. - return b.Put(k, v) - }); err != nil { - return err - } - - return tx.Commit() -} - -// walkFunc is the type of the function called for keys (buckets and "normal" -// values) discovered by Walk. keys is the list of keys to descend to the bucket -// owning the discovered key/value pair k/v. -type walkFunc func(keys [][]byte, k, v []byte, seq uint64) error - -// walk walks recursively the bolt database db, calling walkFn for each key it finds. -func (cmd *CompactCommand) walk(db *bolt.DB, walkFn walkFunc) error { - return db.View(func(tx *bolt.Tx) error { - return tx.ForEach(func(name []byte, b *bolt.Bucket) error { - return cmd.walkBucket(b, nil, name, nil, b.Sequence(), walkFn) - }) - }) -} - -func (cmd *CompactCommand) walkBucket(b *bolt.Bucket, keypath [][]byte, k, v []byte, seq uint64, fn walkFunc) error { - // Execute callback. - if err := fn(keypath, k, v, seq); err != nil { - return err - } - - // If this is not a bucket then stop. - if v != nil { - return nil - } - - // Iterate over each child key/value. - keypath = append(keypath, k) - return b.ForEach(func(k, v []byte) error { - if v == nil { - bkt := b.Bucket(k) - return cmd.walkBucket(bkt, keypath, k, nil, bkt.Sequence(), fn) - } - return cmd.walkBucket(b, keypath, k, v, b.Sequence(), fn) - }) -} - // Usage returns the help message. func (cmd *CompactCommand) Usage() string { return strings.TrimLeft(` diff --git a/compact.go b/compact.go new file mode 100644 index 000000000..e4fe91b04 --- /dev/null +++ b/compact.go @@ -0,0 +1,114 @@ +package bbolt + +// Compact will create a copy of the source DB and in the destination DB. This may +// reclaim space that the source database no longer has use for. txMaxSize can be +// used to limit the transactions size of this process and may trigger intermittent +// commits. A value of zero will ignore transaction sizes. +// TODO: merge with: https://github.com/etcd-io/etcd/blob/b7f0f52a16dbf83f18ca1d803f7892d750366a94/mvcc/backend/backend.go#L349 +func Compact(dst, src *DB, txMaxSize int64) error { + // commit regularly, or we'll run out of memory for large datasets if using one transaction. + var size int64 + tx, err := dst.Begin(true) + if err != nil { + return err + } + defer tx.Rollback() + + if err := walk(src, func(keys [][]byte, k, v []byte, seq uint64) error { + // On each key/value, check if we have exceeded tx size. + sz := int64(len(k) + len(v)) + if size+sz > txMaxSize && txMaxSize != 0 { + // Commit previous transaction. + if err := tx.Commit(); err != nil { + return err + } + + // Start new transaction. + tx, err = dst.Begin(true) + if err != nil { + return err + } + size = 0 + } + size += sz + + // Create bucket on the root transaction if this is the first level. + nk := len(keys) + if nk == 0 { + bkt, err := tx.CreateBucket(k) + if err != nil { + return err + } + if err := bkt.SetSequence(seq); err != nil { + return err + } + return nil + } + + // Create buckets on subsequent levels, if necessary. + b := tx.Bucket(keys[0]) + if nk > 1 { + for _, k := range keys[1:] { + b = b.Bucket(k) + } + } + + // Fill the entire page for best compaction. + b.FillPercent = 1.0 + + // If there is no value then this is a bucket call. + if v == nil { + bkt, err := b.CreateBucket(k) + if err != nil { + return err + } + if err := bkt.SetSequence(seq); err != nil { + return err + } + return nil + } + + // Otherwise treat it as a key/value pair. + return b.Put(k, v) + }); err != nil { + return err + } + + return tx.Commit() +} + +// walkFunc is the type of the function called for keys (buckets and "normal" +// values) discovered by Walk. keys is the list of keys to descend to the bucket +// owning the discovered key/value pair k/v. +type walkFunc func(keys [][]byte, k, v []byte, seq uint64) error + +// walk walks recursively the bolt database db, calling walkFn for each key it finds. +func walk(db *DB, walkFn walkFunc) error { + return db.View(func(tx *Tx) error { + return tx.ForEach(func(name []byte, b *Bucket) error { + return walkBucket(b, nil, name, nil, b.Sequence(), walkFn) + }) + }) +} + +func walkBucket(b *Bucket, keypath [][]byte, k, v []byte, seq uint64, fn walkFunc) error { + // Execute callback. + if err := fn(keypath, k, v, seq); err != nil { + return err + } + + // If this is not a bucket then stop. + if v != nil { + return nil + } + + // Iterate over each child key/value. + keypath = append(keypath, k) + return b.ForEach(func(k, v []byte) error { + if v == nil { + bkt := b.Bucket(k) + return walkBucket(bkt, keypath, k, nil, bkt.Sequence(), fn) + } + return walkBucket(b, keypath, k, v, b.Sequence(), fn) + }) +} From d4a4aebb9a8c38b22f8b88fba2fdad5ce189b096 Mon Sep 17 00:00:00 2001 From: Andrew Martinez Date: Mon, 19 Oct 2020 10:00:15 -0400 Subject: [PATCH 10/18] ignore .idea folder, go to 1.15 --- .gitignore | 1 + .travis.yml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3bcd8cbaf..15a552f00 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.swp /bin/ cover.out +/.idea diff --git a/.travis.yml b/.travis.yml index 257dfdfee..c2282b812 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ go_import_path: go.etcd.io/bbolt sudo: false go: -- 1.12 +- 1.15 before_install: - go get -v honnef.co/go/tools/... From a737c40a297224fe1d9ce35dc90850e12060a086 Mon Sep 17 00:00:00 2001 From: jrapoport Date: Wed, 13 Jan 2021 00:06:58 -0800 Subject: [PATCH 11/18] Add Chestnut to os projects in README.md added Chestnut to the list of open source projects that use bbolt. https://github.com/jrapoport/chestnut --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6b5ed3cc4..5d9187409 100644 --- a/README.md +++ b/README.md @@ -914,6 +914,7 @@ Below is a list of public, open source projects that use Bolt: simple tx and key scans. * [cayley](https://github.com/google/cayley) - Cayley is an open-source graph database using Bolt as optional backend. * [ChainStore](https://github.com/pressly/chainstore) - Simple key-value interface to a variety of storage engines organized as a chain of operations. +* [🌰 Chestnut](https://github.com/jrapoport/chestnut) - Chestnut is encrypted storage for Go. * [Consul](https://github.com/hashicorp/consul) - Consul is service discovery and configuration made easy. Distributed, highly available, and datacenter-aware. * [DVID](https://github.com/janelia-flyem/dvid) - Added Bolt as optional storage engine and testing it against Basho-tuned leveldb. * [dcrwallet](https://github.com/decred/dcrwallet) - A wallet for the Decred cryptocurrency. From cc6381f7d5543d6823b13a86f5abf0ec07684375 Mon Sep 17 00:00:00 2001 From: zounengren Date: Tue, 16 Mar 2021 15:59:07 +0800 Subject: [PATCH 12/18] Use WriteTo() instead --- tx.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tx.go b/tx.go index 4b1a64a8b..744b587b1 100644 --- a/tx.go +++ b/tx.go @@ -393,7 +393,7 @@ func (tx *Tx) CopyFile(path string, mode os.FileMode) error { return err } - err = tx.Copy(f) + _, err = tx.WriteTo(f) if err != nil { _ = f.Close() return err From 981093d53c9da28849a494ed5ecd471c0e1953e5 Mon Sep 17 00:00:00 2001 From: lzhfromustc Date: Wed, 9 Dec 2020 22:46:50 -0500 Subject: [PATCH 13/18] test: add buffer to a channel to avoid the goroutine leak --- db_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db_test.go b/db_test.go index 9b03f2ff0..79a8de8fb 100644 --- a/db_test.go +++ b/db_test.go @@ -1122,7 +1122,7 @@ func TestDB_Batch(t *testing.T) { // Iterate over multiple updates in separate goroutines. n := 2 - ch := make(chan error) + ch := make(chan error, n) for i := 0; i < n; i++ { go func(i int) { ch <- db.Batch(func(tx *bolt.Tx) error { From 8c171443bc830caa7f093a74cb352a72e6cbcb4c Mon Sep 17 00:00:00 2001 From: Makdon Date: Sat, 17 Apr 2021 05:42:49 +0800 Subject: [PATCH 14/18] tx: remove outdated comment (#271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After commit #37e96de, it reports errs in the panic Co-authored-by: makdonmai(麦栋铖) --- tx.go | 1 - 1 file changed, 1 deletion(-) diff --git a/tx.go b/tx.go index 744b587b1..869d41200 100644 --- a/tx.go +++ b/tx.go @@ -188,7 +188,6 @@ func (tx *Tx) Commit() error { } // If strict mode is enabled then perform a consistency check. - // Only the first consistency error is reported in the panic. if tx.db.StrictMode { ch := tx.Check() var errs []string From 116fbcd49033a24a1925e56001fa772b5cbec435 Mon Sep 17 00:00:00 2001 From: Makdon Date: Thu, 22 Apr 2021 02:45:35 +0800 Subject: [PATCH 15/18] Avoid unnecessary conversions (#270) --- freelist_hmap.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freelist_hmap.go b/freelist_hmap.go index 02ef2be04..dbd67a1e7 100644 --- a/freelist_hmap.go +++ b/freelist_hmap.go @@ -4,7 +4,7 @@ import "sort" // hashmapFreeCount returns count of free pages(hashmap version) func (f *freelist) hashmapFreeCount() int { - // use the forwardmap to get the total count + // use the forwardMap to get the total count count := 0 for _, size := range f.forwardMap { count += int(size) @@ -41,7 +41,7 @@ func (f *freelist) hashmapAllocate(txid txid, n int) pgid { for pid := range bm { // remove the initial - f.delSpan(pid, uint64(size)) + f.delSpan(pid, size) f.allocs[pid] = txid @@ -51,7 +51,7 @@ func (f *freelist) hashmapAllocate(txid txid, n int) pgid { f.addSpan(pid+pgid(n), remain) for i := pgid(0); i < pgid(n); i++ { - delete(f.cache, pid+pgid(i)) + delete(f.cache, pid+i) } return pid } From c69165412dd95cf7baf22ed5aad16b4637bb1415 Mon Sep 17 00:00:00 2001 From: wpedrak Date: Fri, 23 Apr 2021 13:10:08 +0200 Subject: [PATCH 16/18] Skip few long running tests in `-short` mode --- bucket_test.go | 4 ++++ db_test.go | 4 ++++ manydbs_test.go | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/bucket_test.go b/bucket_test.go index e48204b58..2ac926359 100644 --- a/bucket_test.go +++ b/bucket_test.go @@ -1185,6 +1185,10 @@ func TestBucket_Put_ValueTooLarge(t *testing.T) { // Ensure a bucket can calculate stats. func TestBucket_Stats(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + db := MustOpenDB() defer db.MustClose() diff --git a/db_test.go b/db_test.go index 79a8de8fb..84271083b 100644 --- a/db_test.go +++ b/db_test.go @@ -66,6 +66,10 @@ func TestOpen(t *testing.T) { // Regression validation for https://github.com/etcd-io/bbolt/pull/122. // Tests multiple goroutines simultaneously opening a database. func TestOpen_MultipleGoroutines(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + const ( instances = 30 iterations = 30 diff --git a/manydbs_test.go b/manydbs_test.go index fbbe8fe2e..b58fc1b26 100644 --- a/manydbs_test.go +++ b/manydbs_test.go @@ -61,6 +61,10 @@ func createAndPutKeys(t *testing.T) { } func TestManyDBs(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + for i := 0; i < 100; i++ { t.Run(fmt.Sprintf("%d", i), createAndPutKeys) } From 10ac1b3d40f5f6dc140dc8878a0b18191936e696 Mon Sep 17 00:00:00 2001 From: zc310 <29995154+zc310@users.noreply.github.com> Date: Sat, 24 Apr 2021 08:33:28 +0800 Subject: [PATCH 17/18] Add BoltDB Viewer to projects section in README (#255) Co-authored-by: zc310.tech --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5d9187409..da66c23fb 100644 --- a/README.md +++ b/README.md @@ -908,6 +908,7 @@ Below is a list of public, open source projects that use Bolt: * [BoltStore](https://github.com/yosssi/boltstore) - Session store using Bolt. * [Boltdb Boilerplate](https://github.com/bobintornado/boltdb-boilerplate) - Boilerplate wrapper around bolt aiming to make simple calls one-liners. * [BoltDbWeb](https://github.com/evnix/boltdbweb) - A web based GUI for BoltDB files. +* [BoltDB Viewer](https://github.com/zc310/rich_boltdb) - A BoltDB Viewer Can run on Windows、Linux、Android system. * [bleve](http://www.blevesearch.com/) - A pure Go search engine similar to ElasticSearch that uses Bolt as the default storage backend. * [btcwallet](https://github.com/btcsuite/btcwallet) - A bitcoin wallet. * [buckets](https://github.com/joyrexus/buckets) - a bolt wrapper streamlining From 8e7d6335c9af09dee35a4b9ded4f469ccedb266d Mon Sep 17 00:00:00 2001 From: Julien Ammous Date: Sat, 24 Apr 2021 02:34:06 +0200 Subject: [PATCH 18/18] remove dead links (#242) * remove dead link the url points towards a catch all domain * removed dead link --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index da66c23fb..f1b4a7b2b 100644 --- a/README.md +++ b/README.md @@ -940,8 +940,6 @@ Below is a list of public, open source projects that use Bolt: * [MetricBase](https://github.com/msiebuhr/MetricBase) - Single-binary version of Graphite. * [MuLiFS](https://github.com/dankomiocevic/mulifs) - Music Library Filesystem creates a filesystem to organise your music files. * [NATS](https://github.com/nats-io/nats-streaming-server) - NATS Streaming uses bbolt for message and metadata storage. -* [Operation Go: A Routine Mission](http://gocode.io) - An online programming game for Golang using Bolt for user accounts and a leaderboard. -* [photosite/session](https://godoc.org/bitbucket.org/kardianos/photosite/session) - Sessions for a photo viewing site. * [Prometheus Annotation Server](https://github.com/oliver006/prom_annotation_server) - Annotation server for PromDash & Prometheus service monitoring system. * [Rain](https://github.com/cenkalti/rain) - BitTorrent client and library. * [reef-pi](https://github.com/reef-pi/reef-pi) - reef-pi is an award winning, modular, DIY reef tank controller using easy to learn electronics based on a Raspberry Pi.