From 64ffac5671ec998a3f2172a612be16d87066f7be Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Sun, 7 Sep 2014 14:18:00 +0200 Subject: [PATCH] Update goleveldb (fixes #644, closes #648) --- Godeps/Godeps.json | 2 +- .../syndtr/goleveldb/leveldb/cache/cache.go | 3 +- .../goleveldb/leveldb/cache/cache_test.go | 56 ++- .../goleveldb/leveldb/cache/lru_cache.go | 419 ++++++++++++++---- .../syndtr/goleveldb/leveldb/db_test.go | 2 +- .../syndtr/goleveldb/leveldb/external_test.go | 2 +- .../syndtr/goleveldb/leveldb/opt/options.go | 32 +- .../syndtr/goleveldb/leveldb/session.go | 2 +- .../syndtr/goleveldb/leveldb/util/range.go | 1 + cmd/syncthing/main.go | 2 +- 10 files changed, 403 insertions(+), 118 deletions(-) diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index f8368e416..8ca0af1b7 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -49,7 +49,7 @@ }, { "ImportPath": "github.com/syndtr/goleveldb/leveldb", - "Rev": "2b99e8d4757bf06eeab1b0485d80b8ae1c088874" + "Rev": "457e6f75905f7c1316afd8c43ad323f4c32b31c2" }, { "ImportPath": "github.com/vitrun/qart/coding", diff --git a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache.go b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache.go index 49f82f0fb..baced7717 100644 --- a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache.go +++ b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache.go @@ -128,7 +128,8 @@ const ( type nodeState int const ( - nodeEffective nodeState = iota + nodeZero nodeState = iota + nodeEffective nodeEvicted nodeDeleted ) diff --git a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go index 6735e02ef..299f6f6c9 100644 --- a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go +++ b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/cache_test.go @@ -512,6 +512,56 @@ func TestLRUCache_Finalizer(t *testing.T) { } } +func BenchmarkLRUCache_Set(b *testing.B) { + c := NewLRUCache(0) + ns := c.GetNamespace(0) + b.ResetTimer() + for i := uint64(0); i < uint64(b.N); i++ { + set(ns, i, "", 1, nil) + } +} + +func BenchmarkLRUCache_Get(b *testing.B) { + c := NewLRUCache(0) + ns := c.GetNamespace(0) + b.ResetTimer() + for i := uint64(0); i < uint64(b.N); i++ { + set(ns, i, "", 1, nil) + } + b.ResetTimer() + for i := uint64(0); i < uint64(b.N); i++ { + ns.Get(i, nil) + } +} + +func BenchmarkLRUCache_Get2(b *testing.B) { + c := NewLRUCache(0) + ns := c.GetNamespace(0) + b.ResetTimer() + for i := uint64(0); i < uint64(b.N); i++ { + set(ns, i, "", 1, nil) + } + b.ResetTimer() + for i := uint64(0); i < uint64(b.N); i++ { + ns.Get(i, func() (charge int, value interface{}) { + return 0, nil + }) + } +} + +func BenchmarkLRUCache_Release(b *testing.B) { + c := NewLRUCache(0) + ns := c.GetNamespace(0) + handles := make([]Handle, b.N) + for i := uint64(0); i < uint64(b.N); i++ { + handles[i] = set(ns, i, "", 1, nil) + } + b.ResetTimer() + for _, h := range handles { + h.Release() + } +} + func BenchmarkLRUCache_SetRelease(b *testing.B) { capacity := b.N / 100 if capacity <= 0 { @@ -521,7 +571,7 @@ func BenchmarkLRUCache_SetRelease(b *testing.B) { ns := c.GetNamespace(0) b.ResetTimer() for i := uint64(0); i < uint64(b.N); i++ { - set(ns, i, nil, 1, nil).Release() + set(ns, i, "", 1, nil).Release() } } @@ -538,10 +588,10 @@ func BenchmarkLRUCache_SetReleaseTwice(b *testing.B) { nb := b.N - na for i := uint64(0); i < uint64(na); i++ { - set(ns, i, nil, 1, nil).Release() + set(ns, i, "", 1, nil).Release() } for i := uint64(0); i < uint64(nb); i++ { - set(ns, i, nil, 1, nil).Release() + set(ns, i, "", 1, nil).Release() } } diff --git a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru_cache.go b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru_cache.go index a29b5d088..168ebcd66 100644 --- a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru_cache.go +++ b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/cache/lru_cache.go @@ -13,6 +13,13 @@ import ( "github.com/syndtr/goleveldb/leveldb/util" ) +// The LLRB implementation were taken from https://github.com/petar/GoLLRB, +// which conatins the following header: +// +// Copyright 2010 Petar Maymounkov. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + // lruCache represent a LRU cache state. type lruCache struct { mu sync.Mutex @@ -74,11 +81,7 @@ func (c *lruCache) GetNamespace(id uint64) Namespace { return ns } - ns := &lruNs{ - lru: c, - id: id, - table: make(map[uint64]*lruNode), - } + ns := &lruNs{lru: c, id: id} c.table[id] = ns return ns } @@ -130,130 +133,267 @@ func (c *lruCache) evict() { } type lruNs struct { - lru *lruCache - id uint64 - table map[uint64]*lruNode - state nsState + lru *lruCache + id uint64 + rbRoot *lruNode + state nsState +} + +func (ns *lruNs) rbGetOrCreateNode(h *lruNode, key uint64) (hn, n *lruNode) { + if h == nil { + n = &lruNode{ns: ns, key: key} + return n, n + } + + if key < h.key { + hn, n = ns.rbGetOrCreateNode(h.rbLeft, key) + if hn != nil { + h.rbLeft = hn + } else { + return nil, n + } + } else if key > h.key { + hn, n = ns.rbGetOrCreateNode(h.rbRight, key) + if hn != nil { + h.rbRight = hn + } else { + return nil, n + } + } else { + return nil, h + } + + if rbIsRed(h.rbRight) && !rbIsRed(h.rbLeft) { + h = rbRotLeft(h) + } + if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) { + h = rbRotRight(h) + } + if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) { + rbFlip(h) + } + return h, n +} + +func (ns *lruNs) getOrCreateNode(key uint64) *lruNode { + hn, n := ns.rbGetOrCreateNode(ns.rbRoot, key) + if hn != nil { + ns.rbRoot = hn + ns.rbRoot.rbBlack = true + } + return n +} + +func (ns *lruNs) rbGetNode(key uint64) *lruNode { + h := ns.rbRoot + for h != nil { + switch { + case key < h.key: + h = h.rbLeft + case key > h.key: + h = h.rbRight + default: + return h + } + } + return nil +} + +func (ns *lruNs) getNode(key uint64) *lruNode { + return ns.rbGetNode(key) +} + +func (ns *lruNs) rbDeleteNode(h *lruNode, key uint64) *lruNode { + if h == nil { + return nil + } + + if key < h.key { + if h.rbLeft == nil { // key not present. Nothing to delete + return h + } + if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) { + h = rbMoveLeft(h) + } + h.rbLeft = ns.rbDeleteNode(h.rbLeft, key) + } else { + if rbIsRed(h.rbLeft) { + h = rbRotRight(h) + } + // If @key equals @h.key and no right children at @h + if h.key == key && h.rbRight == nil { + return nil + } + if h.rbRight != nil && !rbIsRed(h.rbRight) && !rbIsRed(h.rbRight.rbLeft) { + h = rbMoveRight(h) + } + // If @key equals @h.key, and (from above) 'h.Right != nil' + if h.key == key { + var x *lruNode + h.rbRight, x = rbDeleteMin(h.rbRight) + if x == nil { + panic("logic") + } + x.rbLeft, h.rbLeft = h.rbLeft, nil + x.rbRight, h.rbRight = h.rbRight, nil + x.rbBlack = h.rbBlack + h = x + } else { // Else, @key is bigger than @h.key + h.rbRight = ns.rbDeleteNode(h.rbRight, key) + } + } + + return rbFixup(h) +} + +func (ns *lruNs) deleteNode(key uint64) { + ns.rbRoot = ns.rbDeleteNode(ns.rbRoot, key) + if ns.rbRoot != nil { + ns.rbRoot.rbBlack = true + } +} + +func (ns *lruNs) rbIterateNodes(h *lruNode, pivot uint64, iter func(n *lruNode) bool) bool { + if h == nil { + return true + } + if h.key >= pivot { + if !ns.rbIterateNodes(h.rbLeft, pivot, iter) { + return false + } + if !iter(h) { + return false + } + } + return ns.rbIterateNodes(h.rbRight, pivot, iter) +} + +func (ns *lruNs) iterateNodes(iter func(n *lruNode) bool) { + ns.rbIterateNodes(ns.rbRoot, 0, iter) } func (ns *lruNs) Get(key uint64, setf SetFunc) Handle { ns.lru.mu.Lock() + defer ns.lru.mu.Unlock() if ns.state != nsEffective { - ns.lru.mu.Unlock() return nil } - node, ok := ns.table[key] - if ok { - switch node.state { - case nodeEvicted: - // Insert to recent list. - node.state = nodeEffective - node.ref++ - ns.lru.used += node.charge - ns.lru.evict() - fallthrough - case nodeEffective: - // Bump to front. - node.rRemove() - node.rInsert(&ns.lru.recent) - } - node.ref++ - } else { - if setf == nil { - ns.lru.mu.Unlock() + var n *lruNode + if setf == nil { + n = ns.getNode(key) + if n == nil { return nil } - + } else { + n = ns.getOrCreateNode(key) + } + switch n.state { + case nodeZero: charge, value := setf() if value == nil { - ns.lru.mu.Unlock() + ns.deleteNode(key) return nil } - - node = &lruNode{ - ns: ns, - key: key, - value: value, - charge: charge, - ref: 1, + if charge < 0 { + charge = 0 } - ns.table[key] = node + + n.value = value + n.charge = charge + n.state = nodeEvicted ns.lru.size += charge ns.lru.alive++ - if charge > 0 { - node.ref++ - node.rInsert(&ns.lru.recent) - ns.lru.used += charge - ns.lru.evict() - } - } - ns.lru.mu.Unlock() - return &lruHandle{node: node} + fallthrough + case nodeEvicted: + if n.charge == 0 { + break + } + + // Insert to recent list. + n.state = nodeEffective + n.ref++ + ns.lru.used += n.charge + ns.lru.evict() + + fallthrough + case nodeEffective: + // Bump to front. + n.rRemove() + n.rInsert(&ns.lru.recent) + } + n.ref++ + + return &lruHandle{node: n} } func (ns *lruNs) Delete(key uint64, fin DelFin) bool { ns.lru.mu.Lock() + defer ns.lru.mu.Unlock() if ns.state != nsEffective { if fin != nil { fin(false, false) } - ns.lru.mu.Unlock() return false } - node, exist := ns.table[key] - if !exist { + n := ns.getNode(key) + if n == nil { if fin != nil { fin(false, false) } - ns.lru.mu.Unlock() return false + } - switch node.state { + switch n.state { + case nodeEffective: + ns.lru.used -= n.charge + n.state = nodeDeleted + n.delfin = fin + n.rRemove() + n.derefNB() + case nodeEvicted: + n.state = nodeDeleted + n.delfin = fin case nodeDeleted: if fin != nil { fin(true, true) } - ns.lru.mu.Unlock() return false - case nodeEffective: - ns.lru.used -= node.charge - node.state = nodeDeleted - node.delfin = fin - node.rRemove() - node.derefNB() default: - node.state = nodeDeleted - node.delfin = fin + panic("invalid state") } - ns.lru.mu.Unlock() return true } func (ns *lruNs) purgeNB(fin PurgeFin) { - if ns.state != nsEffective { - return - } - - for _, node := range ns.table { - switch node.state { - case nodeDeleted: - case nodeEffective: - ns.lru.used -= node.charge - node.state = nodeDeleted - node.purgefin = fin - node.rRemove() - node.derefNB() - default: - node.state = nodeDeleted - node.purgefin = fin + if ns.state == nsEffective { + var nodes []*lruNode + ns.iterateNodes(func(n *lruNode) bool { + nodes = append(nodes, n) + return true + }) + for _, n := range nodes { + switch n.state { + case nodeEffective: + ns.lru.used -= n.charge + n.state = nodeDeleted + n.purgefin = fin + n.rRemove() + n.derefNB() + case nodeEvicted: + n.state = nodeDeleted + n.purgefin = fin + case nodeDeleted: + default: + panic("invalid state") + } } } } @@ -265,22 +405,22 @@ func (ns *lruNs) Purge(fin PurgeFin) { } func (ns *lruNs) zapNB() { - if ns.state != nsEffective { - return - } + if ns.state == nsEffective { + ns.state = nsZapped - ns.state = nsZapped + ns.iterateNodes(func(n *lruNode) bool { + if n.state == nodeEffective { + ns.lru.used -= n.charge + n.rRemove() + } + ns.lru.size -= n.charge + n.state = nodeDeleted + n.fin() - for _, node := range ns.table { - if node.state == nodeEffective { - ns.lru.used -= node.charge - node.rRemove() - } - ns.lru.size -= node.charge - node.state = nodeDeleted - node.fin() + return true + }) + ns.rbRoot = nil } - ns.table = nil } func (ns *lruNs) Zap() { @@ -293,7 +433,9 @@ func (ns *lruNs) Zap() { type lruNode struct { ns *lruNs - rNext, rPrev *lruNode + rNext, rPrev *lruNode + rbLeft, rbRight *lruNode + rbBlack bool key uint64 value interface{} @@ -344,7 +486,7 @@ func (n *lruNode) derefNB() { if n.ref == 0 { if n.ns.state == nsEffective { // Remove elemement. - delete(n.ns.table, n.key) + n.ns.deleteNode(n.key) n.ns.lru.size -= n.charge n.ns.lru.alive-- n.fin() @@ -380,3 +522,92 @@ func (h *lruHandle) Release() { h.node.deref() h.node = nil } + +func rbIsRed(h *lruNode) bool { + if h == nil { + return false + } + return !h.rbBlack +} + +func rbRotLeft(h *lruNode) *lruNode { + x := h.rbRight + if x.rbBlack { + panic("rotating a black link") + } + h.rbRight = x.rbLeft + x.rbLeft = h + x.rbBlack = h.rbBlack + h.rbBlack = false + return x +} + +func rbRotRight(h *lruNode) *lruNode { + x := h.rbLeft + if x.rbBlack { + panic("rotating a black link") + } + h.rbLeft = x.rbRight + x.rbRight = h + x.rbBlack = h.rbBlack + h.rbBlack = false + return x +} + +func rbFlip(h *lruNode) { + h.rbBlack = !h.rbBlack + h.rbLeft.rbBlack = !h.rbLeft.rbBlack + h.rbRight.rbBlack = !h.rbRight.rbBlack +} + +func rbMoveLeft(h *lruNode) *lruNode { + rbFlip(h) + if rbIsRed(h.rbRight.rbLeft) { + h.rbRight = rbRotRight(h.rbRight) + h = rbRotLeft(h) + rbFlip(h) + } + return h +} + +func rbMoveRight(h *lruNode) *lruNode { + rbFlip(h) + if rbIsRed(h.rbLeft.rbLeft) { + h = rbRotRight(h) + rbFlip(h) + } + return h +} + +func rbFixup(h *lruNode) *lruNode { + if rbIsRed(h.rbRight) { + h = rbRotLeft(h) + } + + if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) { + h = rbRotRight(h) + } + + if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) { + rbFlip(h) + } + + return h +} + +func rbDeleteMin(h *lruNode) (hn, n *lruNode) { + if h == nil { + return nil, nil + } + if h.rbLeft == nil { + return nil, h + } + + if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) { + h = rbMoveLeft(h) + } + + h.rbLeft, n = rbDeleteMin(h.rbLeft) + + return rbFixup(h), n +} diff --git a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_test.go b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_test.go index 5aadc12d7..f728e7e71 100644 --- a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_test.go +++ b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/db_test.go @@ -1210,7 +1210,7 @@ func TestDb_DeletionMarkers2(t *testing.T) { } func TestDb_CompactionTableOpenError(t *testing.T) { - h := newDbHarnessWopt(t, &opt.Options{MaxOpenFiles: 0}) + h := newDbHarnessWopt(t, &opt.Options{CachedOpenFiles: -1}) defer h.close() im := 10 diff --git a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/external_test.go b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/external_test.go index f74f354c6..02fa3a652 100644 --- a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/external_test.go +++ b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/external_test.go @@ -21,7 +21,7 @@ var _ = testutil.Defer(func() { BlockRestartInterval: 5, BlockSize: 50, Compression: opt.NoCompression, - MaxOpenFiles: 0, + CachedOpenFiles: -1, Strict: opt.StrictAll, WriteBuffer: 1000, } diff --git a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/opt/options.go b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/opt/options.go index 21e7a8a7f..2a375ba60 100644 --- a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/opt/options.go +++ b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/opt/options.go @@ -24,7 +24,7 @@ const ( DefaultBlockRestartInterval = 16 DefaultBlockSize = 4 * KiB DefaultCompressionType = SnappyCompression - DefaultMaxOpenFiles = 1000 + DefaultCachedOpenFiles = 500 DefaultWriteBuffer = 4 * MiB ) @@ -125,6 +125,13 @@ type Options struct { // The default value is 4KiB. BlockSize int + // CachedOpenFiles defines number of open files to kept around when not + // in-use, the counting includes still in-use files. + // Set this to negative value to disable caching. + // + // The default value is 500. + CachedOpenFiles int + // Comparer defines a total ordering over the space of []byte keys: a 'less // than' relationship. The same comparison algorithm must be used for reads // and writes over the lifetime of the DB. @@ -165,13 +172,6 @@ type Options struct { // The default value is nil. Filter filter.Filter - // MaxOpenFiles defines maximum number of open files to kept around - // (cached). This is not an hard limit, actual open files may exceed - // the defined value. - // - // The default value is 1000. - MaxOpenFiles int - // Strict defines the DB strict level. Strict Strict @@ -213,6 +213,15 @@ func (o *Options) GetBlockSize() int { return o.BlockSize } +func (o *Options) GetCachedOpenFiles() int { + if o == nil || o.CachedOpenFiles == 0 { + return DefaultCachedOpenFiles + } else if o.CachedOpenFiles < 0 { + return 0 + } + return o.CachedOpenFiles +} + func (o *Options) GetComparer() comparer.Comparer { if o == nil || o.Comparer == nil { return comparer.DefaultComparer @@ -248,13 +257,6 @@ func (o *Options) GetFilter() filter.Filter { return o.Filter } -func (o *Options) GetMaxOpenFiles() int { - if o == nil || o.MaxOpenFiles <= 0 { - return DefaultMaxOpenFiles - } - return o.MaxOpenFiles -} - func (o *Options) GetStrict(strict Strict) bool { if o == nil || o.Strict == 0 { return DefaultStrict&strict != 0 diff --git a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session.go b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session.go index 8c64fb8f2..7fc08e7de 100644 --- a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session.go +++ b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/session.go @@ -58,7 +58,7 @@ func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) { storLock: storLock, } s.setOptions(o) - s.tops = newTableOps(s, s.o.GetMaxOpenFiles()) + s.tops = newTableOps(s, s.o.GetCachedOpenFiles()) s.setVersion(&version{s: s}) s.log("log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock D·DeletedEntry L·Level Q·SeqNum T·TimeElapsed") return diff --git a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/util/range.go b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/util/range.go index 24891d529..85159583d 100644 --- a/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/util/range.go +++ b/Godeps/_workspace/src/github.com/syndtr/goleveldb/leveldb/util/range.go @@ -25,6 +25,7 @@ func BytesPrefix(prefix []byte) *Range { limit = make([]byte, i+1) copy(limit, prefix) limit[i] = c + 1 + break } } return &Range{prefix, limit} diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 70ec15d67..ccb720ebe 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -391,7 +391,7 @@ func syncthingMain() { // If this is the first time the user runs v0.9, archive the old indexes and config. archiveLegacyConfig() - db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{MaxOpenFiles: 100}) + db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), &opt.Options{CachedOpenFiles: 100}) if err != nil { l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?") }