nim-libp2p/libp2p/protocols/pubsub/timedcache.nim

145 lines
3.7 KiB
Nim

# Nim-LibP2P
# Copyright (c) 2023 Status Research & Development GmbH
# Licensed under either of
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE))
# * MIT license ([LICENSE-MIT](LICENSE-MIT))
# at your option.
# This file may not be copied, modified, or distributed except according to
# those terms.
{.push raises: [].}
import std/[hashes, sets]
import chronos/timer, stew/results
import ../../utility
export results
const Timeout* = 10.seconds # default timeout in ms
type
TimedEntry*[K] = ref object of RootObj
key: K
addedAt: Moment
expiresAt: Moment
next, prev: TimedEntry[K]
TimedCache*[K] = object of RootObj
head, tail: TimedEntry[K] # nim linked list doesn't allow inserting at pos
entries: HashSet[TimedEntry[K]]
timeout: Duration
maxSize: int # Optional max size of the cache, 0 means unlimited
func `==`*[E](a, b: TimedEntry[E]): bool =
if isNil(a) == isNil(b):
isNil(a) or a.key == b.key
else:
false
func hash*(a: TimedEntry): Hash =
if isNil(a):
default(Hash)
else:
hash(a[].key)
func expire*(t: var TimedCache, now: Moment = Moment.now()) =
while t.head != nil and t.head.expiresAt < now:
t.entries.excl(t.head)
t.head.prev = nil
t.head = t.head.next
if t.head == nil:
t.tail = nil
func del*[K](t: var TimedCache[K], key: K): Opt[TimedEntry[K]] =
# Removes existing key from cache, returning the previous value if present
let tmp = TimedEntry[K](key: key)
if tmp in t.entries:
let item =
try:
t.entries[tmp] # use the shared instance in the set
except KeyError:
raiseAssert "just checked"
t.entries.excl(item)
if t.head == item:
t.head = item.next
if t.tail == item:
t.tail = item.prev
if item.next != nil:
item.next.prev = item.prev
if item.prev != nil:
item.prev.next = item.next
Opt.some(item)
else:
Opt.none(TimedEntry[K])
func put*[K](t: var TimedCache[K], k: K, now = Moment.now()): bool =
# Puts k in cache, returning true if the item was already present and false
# otherwise. If the item was already present, its expiry timer will be
# refreshed.
func ensureSizeBound(t: var TimedCache[K]) =
if t.maxSize > 0 and t.entries.len() >= t.maxSize and k notin t:
if t.head != nil:
t.entries.excl(t.head)
t.head = t.head.next
if t.head != nil:
t.head.prev = nil
else:
t.tail = nil
t.expire(now)
t.ensureSizeBound()
let
previous = t.del(k) # Refresh existing item
addedAt =
if previous.isSome():
previous[].addedAt
else:
now
let node = TimedEntry[K](key: k, addedAt: addedAt, expiresAt: now + t.timeout)
if t.head == nil:
t.tail = node
t.head = t.tail
else:
# search from tail because typically that's where we add when now grows
var cur = t.tail
while cur != nil and node.expiresAt < cur.expiresAt:
cur = cur.prev
if cur == nil:
node.next = t.head
t.head.prev = node
t.head = node
else:
node.prev = cur
node.next = cur.next
cur.next = node
if cur == t.tail:
t.tail = node
t.entries.incl(node)
previous.isSome()
func contains*[K](t: TimedCache[K], k: K): bool =
let tmp = TimedEntry[K](key: k)
tmp in t.entries
func addedAt*[K](t: var TimedCache[K], k: K): Moment =
let tmp = TimedEntry[K](key: k)
try:
if tmp in t.entries: # raising is slow
# Use shared instance from entries
return t.entries[tmp][].addedAt
except KeyError:
raiseAssert "just checked"
default(Moment)
func init*[K](T: type TimedCache[K], timeout: Duration = Timeout, maxSize: int = 0): T =
T(timeout: timeout, maxSize: maxSize)