diff --git a/go.mod b/go.mod index 17677944ae..afbbd49d55 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,8 @@ require ( github.com/go-playground/assert/v2 v2.0.1 github.com/go-resty/resty/v2 v2.6.0 github.com/gorilla/websocket v1.5.0 - github.com/iotaledger/hive.go/core v1.0.0-rc.2.0.20230110152711-02063266eb24 + github.com/hashicorp/golang-lru/v2 v2.0.1 + github.com/iotaledger/hive.go/core v1.0.0-rc.2.0.20230112011142-169d55ffd5cb github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1 github.com/labstack/echo v3.3.10+incompatible github.com/libp2p/go-libp2p v0.23.4 diff --git a/go.sum b/go.sum index 1ea6d9298d..aa98e964e3 100644 --- a/go.sum +++ b/go.sum @@ -410,6 +410,8 @@ github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= +github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -440,8 +442,8 @@ github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/C github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/iotaledger/grocksdb v1.7.5-0.20221128103803-fcdb79760195 h1:W5v+7oSXtSq2OSadYPyaAbPjTJW10T2bOgMDGZcyVOc= github.com/iotaledger/grocksdb v1.7.5-0.20221128103803-fcdb79760195/go.mod h1:AoAM7v6lyWRQzrmmegOEq759o1PgvvKvn2bEe1A1mc8= -github.com/iotaledger/hive.go/core v1.0.0-rc.2.0.20230110152711-02063266eb24 h1:HiZNj4cCgqiCNolYk6qCF9FaYdUcTThufV/O9h+gTqI= -github.com/iotaledger/hive.go/core v1.0.0-rc.2.0.20230110152711-02063266eb24/go.mod h1:POmRNWlS/NWFCrMowt+CcE4H8GAVe1BotWLN9QD6FLE= +github.com/iotaledger/hive.go/core v1.0.0-rc.2.0.20230112011142-169d55ffd5cb h1:X9mv19JMH10yxA9sM/3haYTx0PM8tl6uMl0CDtzkpvA= +github.com/iotaledger/hive.go/core v1.0.0-rc.2.0.20230112011142-169d55ffd5cb/go.mod h1:POmRNWlS/NWFCrMowt+CcE4H8GAVe1BotWLN9QD6FLE= github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1 h1:x3xsI32h+1wTIzLWInC+AcwrUyk9/l7z2RFMQiuua2E= github.com/iotaledger/hive.go/serializer/v2 v2.0.0-rc.1/go.mod h1:jkV//O5d+HHm32qDmTy6AWZUgxuZaXazTUVqox+5z4g= github.com/ipfs/go-cid v0.3.2 h1:OGgOd+JCFM+y1DjWPmVH+2/4POtpDzwcr7VgnB7mZXc= diff --git a/packages/core/ads/cachedmap.go b/packages/core/ads/cachedmap.go new file mode 100644 index 0000000000..b6ff9af7d8 --- /dev/null +++ b/packages/core/ads/cachedmap.go @@ -0,0 +1,116 @@ +package ads + +import ( + "sync" + + lru "github.com/hashicorp/golang-lru/v2" + "github.com/iotaledger/hive.go/core/generics/constraints" + "github.com/iotaledger/hive.go/core/generics/lo" + "github.com/iotaledger/hive.go/core/generics/shrinkingmap" + "github.com/iotaledger/hive.go/core/kvstore" + "github.com/iotaledger/hive.go/core/typeutils" +) + +type CachedMap[K, V constraints.Serializable, KPtr constraints.MarshalablePtr[K], VPtr constraints.MarshalablePtr[V]] struct { + storedMap *Map[K, V, KPtr, VPtr] + + writeCache *shrinkingmap.ShrinkingMap[string, VPtr] + readCache *lru.Cache[string, VPtr] + + mutex sync.Mutex + newElements *sync.Cond + writeCacheEmpty *sync.Cond +} + +func NewCachedMap[K, V constraints.Serializable, KPtr constraints.MarshalablePtr[K], VPtr constraints.MarshalablePtr[V]](store kvstore.KVStore, cacheSize int) (newMap *CachedMap[K, V, KPtr, VPtr]) { + newMap = &CachedMap[K, V, KPtr, VPtr]{ + storedMap: NewMap[K, V, KPtr, VPtr](store), + writeCache: shrinkingmap.New[string, VPtr](), + readCache: lo.PanicOnErr(lru.New[string, VPtr](cacheSize)), + } + + newMap.newElements = &sync.Cond{ + L: &newMap.mutex, + } + + newMap.writeCacheEmpty = &sync.Cond{ + L: &newMap.mutex, + } + + go newMap.writeLoop() + + return +} + +func (c *CachedMap[K, V, KPtr, VPtr]) Set(key K, value VPtr) { + keyString := typeutils.BytesToString(lo.PanicOnErr(key.Bytes())) + + c.mutex.Lock() + defer c.mutex.Unlock() + + if c.writeCache.Set(keyString, value) && c.writeCache.Size() == 1 { + c.newElements.Signal() + } + + c.readCache.Remove(keyString) +} + +func (c *CachedMap[K, V, KPtr, VPtr]) Delete(key K) { + keyString := typeutils.BytesToString(lo.PanicOnErr(key.Bytes())) + + c.mutex.Lock() + defer c.mutex.Unlock() + + c.writeCache.Set(keyString, nil) + c.readCache.Remove(keyString) +} + +func (c *CachedMap[K, V, KPtr, VPtr]) Has(key K) (has bool) { + return lo.Return1(c.Get(key)) != nil +} + +func (c *CachedMap[K, V, KPtr, VPtr]) Get(key K) (value VPtr, exists bool) { + keyBytes := lo.PanicOnErr(key.Bytes()) + keyString := typeutils.BytesToString(keyBytes) + + c.mutex.Lock() + defer c.mutex.Unlock() + + if writtenValue, writtenValueExists := c.writeCache.Get(keyString); writtenValueExists { + return writtenValue, writtenValue != nil + } + + if readValue, readValueExists := c.readCache.Get(keyString); readValueExists { + return readValue, readValue != nil + } + + value, exists = c.storedMap.Get(key) + c.readCache.Add(keyString, value) + + return +} + +func (c *CachedMap[K, V, KPtr, VPtr]) writeLoop() { + for { + c.mutex.Lock() + for c.writeCache.Size() == 0 { + c.writeCacheEmpty.Broadcast() + + c.newElements.Wait() + } + + keyToWrite, valueToWrite, exists := c.writeCache.Pop() + if !exists { + panic("writeCache should not be empty") + } + + c.readCache.Add(keyToWrite, valueToWrite) + c.mutex.Unlock() + + if valueToWrite == nil { + c.storedMap.delete(typeutils.StringToBytes(keyToWrite)) + } else { + c.storedMap.set(typeutils.StringToBytes(keyToWrite), lo.PanicOnErr(valueToWrite.Bytes())) + } + } +} diff --git a/packages/core/ads/map.go b/packages/core/ads/map.go index 7ad8df597f..4ba9ff0a1c 100644 --- a/packages/core/ads/map.go +++ b/packages/core/ads/map.go @@ -51,19 +51,21 @@ func (m *Map[K, V, KPtr, VPtr]) Root() (root types.Identifier) { // Set sets the output to unspent outputs set. func (m *Map[K, V, KPtr, VPtr]) Set(key K, value VPtr) { - m.mutex.Lock() - defer m.mutex.Unlock() - valueBytes := lo.PanicOnErr(value.Bytes()) if len(valueBytes) == 0 { panic("value cannot be empty") } - keyBytes := lo.PanicOnErr(key.Bytes()) + m.set(lo.PanicOnErr(key.Bytes()), valueBytes) +} - m.root.Set(lo.PanicOnErr(m.tree.Update(keyBytes, valueBytes))) +func (m *Map[K, V, KPtr, VPtr]) set(key, value []byte) { + m.mutex.Lock() + defer m.mutex.Unlock() - if err := m.rawKeysStore.Set(keyBytes, []byte{}); err != nil { + m.root.Set(lo.PanicOnErr(m.tree.Update(key, value))) + + if err := m.rawKeysStore.Set(key, []byte{}); err != nil { panic(err) } } @@ -74,14 +76,18 @@ func (m *Map[K, V, KPtr, VPtr]) Delete(key K) (deleted bool) { return } - m.mutex.Lock() - defer m.mutex.Unlock() - keyBytes := lo.PanicOnErr(key.Bytes()) - if deleted = m.has(keyBytes); !deleted { - return + if deleted = m.has(keyBytes); deleted { + m.delete(keyBytes) } + return +} + +func (m *Map[K, V, KPtr, VPtr]) delete(keyBytes []byte) { + m.mutex.Lock() + defer m.mutex.Unlock() + m.root.Set(lo.PanicOnErr(m.tree.Delete(keyBytes))) if err := m.rawKeysStore.Delete(keyBytes); err != nil {