Day 20: CacheTable

Today we will implement an expiry feature on keys over nim tables

What to expect

  var c = newCacheTable[string, string](initDuration(seconds = 2))
  c.setKey("name", "ahmed", initDuration(seconds = 10))
  c.setKey("color", "blue", initDuration(seconds = 5))
  c.setKey("akey", "a value", DefaultExpiration)
  c.setKey("akey2", "a value2", DefaultExpiration)

  c.setKey("lang", "nim", NeverExpires)
  • Here will will create a new Table from string to string
  • we are allowed to set the default expiration to 2 seconds using Duration object globally on the Table newCacheTable[string, string](initDuration(seconds = 2))
  • We are allowed to override the default expiration when setKey by passing a duration object
  • We are allowed to set a key to NeverExpires

Here's a small example to see the internals of execution


  for i in countup(0, 20):
    echo "has key name? " & $c.hasKey("name")
    echo $c.getCache
    echo $c.get("name")
    echo $c.get("color")
    echo $c.get("lang")
    echo $c.get("akey")
    echo $c.get("akey2")
    os.sleep(1*1000)

Implementation

Imports

import tables, times, os, options, locks
type Expiration* = enum NeverExpires, DefaultExpiration

We have to types of Expiration

  • NeverExpires basically the key stays there forever.
  • DefaultExpiration to use whatever global expiration value defined on the Table
type Entry*[V] = object
  value*: V
  ttl*: int64

type CacheTable*[K, V] = ref object
  cache: Table[K, Entry[V]]
  lock*: locks.Lock
  defaultExpiration*: Duration

proc newCacheTable*[K, V](defaultExpiration = initDuration(
    seconds = 5)): CacheTable[K, V] =
  ## Create new CacheTable
  result = CacheTable[K, V]()
  result.cache = initTable[K, Entry[V]]()
  result.defaultExpiration = defaultExpiration

The only difference between our CacheTable and Nim's Table is the entries are keeping track of Time To Live TTL

  • Entry is a Generic entry we store in the CacheTable that has a value of a type V and keeps track of its ttl
  • CacheTable is a Table from keys of type K to values of of type Entry[V] and keeps track of default expiration
  • newCacheTable is a helper to create a new CacheTable.
proc getCache*[K, V](t: CacheTable[K, V]): Table[K, Entry[V]] =
  result = t.cache

a helper to get the underlying Table

proc setKey*[K, V](t: CacheTable[K, V], key: K, value: V, d: Duration) =
  ## Set ``Key`` of type ``K`` (needs to be hashable) to ``value`` of type ``V`` with duration ``d``
  let rightnow = times.getTime()
  let rightNowDur = times.initDuration(seconds = rightnow.toUnix(),
      nanoseconds = rightnow.nanosecond)

  let ttl = d.inNanoseconds + rightNowDur.inNanoseconds
  let entry = Entry[V](value: value, ttl: ttl)
  t.cache.add(key, entry)

a helper to set a new key in the CacheTable with a specific Duration

proc setKey*[K, V](t: CacheTable[K, V], key: K, value: V,
    expiration: Expiration = NeverExpires) =
  ## Sets key with `Expiration` strategy
  var entry: Entry[V]
  case expiration:
  of NeverExpires:
    entry = Entry[V](value: value, ttl: 0)
    t.cache.add(key, entry)
  of DefaultExpiration:
    t.setKey(key, value, d = t.defaultExpiration)

a helper to set key based on an Expiration strategy

  • if NeverExpires : ttl should be 0
  • if DefaultExpiration: ttl will be the same as the Cachetable defaultExpiration duration
proc setKeyWithDefaultTtl*[K, V](t: CacheTable[K, V], key: K, value: V) =
  ## Sets a key with default Ttl duration.
  t.setKey(key, value, DefaultExpiration)

sets a key to value with default expiration

proc hasKey*[K, V](t: CacheTable[K, V], key: K): bool =
  ## Checks if `key` exists in cache
  result = t.cache.hasKey(key)

Check if the cache underneath has a specific key

proc isExpired(ttl: int64): bool =
  if ttl == 0:
    # echo "duration 0 never expires."
    result = false
  else:
    let rightnow = times.getTime()
    let rightNowDur = times.initDuration(seconds = rightnow.toUnix(),
        nanoseconds = rightnow.nanosecond)
    # echo "Now is : " & $rightnow
    result = rightnowDur.inNanoseconds > ttl

Helper to check if a ttl expired relative to the time right now.

proc get*[K, V](t: CacheTable[K, V], key: K): Option[V] =
  ## Get value of `key` from cache
  var entry: Entry[V]
  try:
      entry = t.cache[key]
  except:
    return none(V)

  # echo "getting entry for key: " & key  & $entry
  if not isExpired(entry.ttl):
    # echo "k: " & key & " didn't expire"
    return some(entry.value)
  else:
    # echo "k: " & key & " expired"
    del(t.cache, key)
    return none(V)

Getting a key from the cache to returns an Option[V] of the value of type V stored in the Entry[V].

Thank you for reading! and please feel free to open an issue or a PR to improve to content of Nim Days :)