/ Misc / src / ttl.jl
ttl.jl
  1  @doc """ Module for managing time-to-live cache.
  2  
  3  """
  4  module TimeToLive
  5  using ConcurrentCollections: modify!, Delete, ConcurrentDict
  6  
  7  export TTL, safettl
  8  
  9  using Base.Iterators: peel
 10  using Dates: DateTime, Period, now
 11  using ..DocStringExtensions
 12  
 13  struct Node{T}
 14      value::T
 15      expiry::DateTime
 16  end
 17  
 18  isexpired(v::Node) = now() > v.expiry
 19  isexpired(time::DateTime) = v::Node -> time > v.expiry
 20  
 21  @doc """
 22      TTL(ttl::Period; refresh_on_access::Bool=false)
 23      TTL{K, V}(ttl::Period; refresh_on_access::Bool=false)
 24  
 25  $(TYPEDFIELDS)
 26  
 27  An associative [TTL](https://en.wikipedia.org/wiki/Time_to_live) cache.
 28  If `refresh_on_access` is set, expiries are reset whenever they are accessed.
 29  """
 30  struct TTL{K,V,D<:AbstractDict,P<:Period} <: AbstractDict{K,V}
 31      dict::D where {D<:AbstractDict{K,Node{V}}}
 32      ttl::P
 33      refresh::Bool
 34  
 35      function TTL{K,V}(
 36          ttl::P; refresh_on_access::Bool=false, dict_type=Dict
 37      ) where {K,V,P<:Period}
 38          new{K,V,dict_type,P}(dict_type{K,Node{V}}(), ttl, refresh_on_access)
 39      end
 40      function TTL(ttl::Period; refresh_on_access::Bool=false)
 41          TTL{Any,Any}(ttl; refresh_on_access=refresh_on_access)
 42      end
 43  end
 44  
 45  @doc """ Safely instantiate a TTL dictionary.
 46  
 47  $(TYPEDSIGNATURES)
 48  
 49  This function safely creates a Time-to-Live (TTL) dictionary with specified key and value types, along with an optional ttl parameter.
 50  """
 51  function safettl(K::Type, V::Type, ttl; kwargs...)
 52      TTL{K,V}(ttl; dict_type=ConcurrentDict, kwargs...)
 53  end
 54  
 55  Base.delete!(t::TTL, key) = (delete!(t.dict, key); t)
 56  Base.empty!(t::ConcurrentDict{K,V}) where {K,V} =
 57      for k in keys(t)
 58          modify!(t, k) do value
 59              Delete(value)
 60          end
 61      end
 62  Base.delete!(t::ConcurrentDict{K,V}, k) where {K,V} =
 63      modify!(t, k) do value
 64          Delete(value)
 65      end
 66  
 67  Base.empty!(t::TTL) = (empty!(t.dict); t)
 68  # Specifying ::Function fixes some method invalidations
 69  Base.get(f::Function, t::TTL, key) = haskey(t, key) ? t[key] : f()
 70  Base.get!(t::TTL, key, default) = haskey(t, key) ? t[key] : (t[key] = default)
 71  Base.length(t::TTL) = count(!isexpired(now()), values(t.dict))
 72  Base.push!(t::TTL, p::Pair) = (t[p.first] = p.second; t)
 73  Base.setindex!(t::TTL{K,V}, v, k) where {K,V} = t.dict[k] = Node{V}(v, now() + t.ttl)
 74  Base.sizehint!(t::TTL, newsz) = (sizehint!(t.dict, newsz); t)
 75  
 76  function Base.pop!(t::TTL)
 77      p = pop!(t.dict)
 78      return isexpired(p.second) ? pop!(t) : p.first => p.second.value
 79  end
 80  
 81  function Base.pop!(t::TTL, key)
 82      v = pop!(t.dict, key)
 83      isexpired(v) && throw(KeyError(key))
 84      return v.value
 85  end
 86  
 87  function Base.get(t::TTL, key, default)
 88      haskey(t.dict, key) || return default
 89      v = t.dict[key]
 90      if isexpired(v)
 91          delete!(t, key)
 92          return default
 93      end
 94      t.refresh && (t[key] = v.value)
 95      return v.value
 96  end
 97  
 98  function Base.getkey(t::TTL, key, default)
 99      return if haskey(t, key)
100          if isexpired(t.dict[key])
101              delete!(t, key)
102              default
103          else
104              key
105          end
106      else
107          default
108      end
109  end
110  
111  function Base.iterate(t::TTL, ks=keys(t.dict))
112      isempty(ks) && return nothing
113      k, rest = peel(ks)
114      v = t.dict[k]
115      return if isexpired(v)
116          delete!(t, k)
117          iterate(t, rest)
118      else
119          k => v.value, rest
120      end
121  end
122  
123  function Base.getindex(t::TTL, key)
124      v = t.dict[key]
125      if isexpired(v)
126          delete!(t, key)
127          throw(KeyError(key))
128      elseif t.refresh
129          t[key] = v.value
130      end
131      return v.value
132  end
133  
134  end