Skip to content

Commit

Permalink
Add purge_on_timeout keyword arg for automatically deleting expired k…
Browse files Browse the repository at this point in the history
…eys (#7)

* Add purge_on_timeout keyword arg for automatically deleting expired keys

Implements #1. You can now pass `purge_on_timeout=true` to
`ExpiringCaches.Cache` constructor, which will result in an asynchronous
task being spawned whenever a key is added to the cache. The task will
wait for the cache's timeout length, and afterwards remove the key-val
from the cache, UNLESS it has already been replaced, in which case
another timer task was spawned to purge the replaced key.

* fix

* update ci

* fix
  • Loading branch information
quinnj authored Apr 16, 2021
1 parent be6b809 commit 14d6dc6
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 45 deletions.
50 changes: 50 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CI
on:
pull_request:
branches:
- main
push:
branches:
- main
tags: '*'
jobs:
test:
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
version:
- '1.0'
- '1' # automatically expands to the latest stable 1.x release of Julia
- 'nightly'
os:
- ubuntu-latest
arch:
- x64
include:
- os: windows-latest
version: '1'
arch: x86
steps:
- uses: actions/checkout@v2
- uses: julia-actions/setup-julia@v1
with:
version: ${{ matrix.version }}
arch: ${{ matrix.arch }}
- uses: actions/cache@v1
env:
cache-name: cache-artifacts
with:
path: ~/.julia/artifacts
key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
restore-keys: |
${{ runner.os }}-test-${{ env.cache-name }}-
${{ runner.os }}-test-
${{ runner.os }}-
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v1
with:
file: lcov.info
41 changes: 0 additions & 41 deletions .travis.yml

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ The package is registered in the [`General`](https://github.com/JuliaRegistries/


### `Cache`
ExpiringCaches.Cache{K, V}(timeout::Dates.Period)
ExpiringCaches.Cache{K, V}(timeout::Dates.Period; purge_on_timeout::Bool=false)

Create a thread-safe, expiring cache where values older than `timeout`
are "invalid" and will be deleted.
Expand Down
27 changes: 24 additions & 3 deletions src/ExpiringCaches.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ end

TimestampedValue(x::T) where {T} = TimestampedValue{T}(x, Dates.now(Dates.UTC))
TimestampedValue{T}(x) where {T} = TimestampedValue{T}(x, Dates.now(Dates.UTC))
timestamp(x::TimestampedValue) = x.timestamp

"""
ExpiringCaches.Cache{K, V}(timeout::Dates.Period)
ExpiringCaches.Cache{K, V}(timeout::Dates.Period; purge_on_timeout::Bool=false)
Create a thread-safe, expiring cache where values older than `timeout`
are "invalid" and will be deleted.
Expand All @@ -24,13 +25,20 @@ calculating a value is expensive and is able to be "cached" for a certain
amount of time. To avoid using the cache (i.e. to invalidate the cache),
a `Cache` supports the `delete!` and `empty!` methods to remove values
manually.
By default, expired keys will remain in the cache until requested (via
`haskey` or `get`); if `purge_on_timeout=true` keyword argument is passed,
then an async task will be spawned for each key upon entry. When the
timeout task has waited `timeout` length of time, the key will be removed
from the cache.
"""
struct Cache{K, V, P <: Dates.Period} <: AbstractDict{K, V}
lock::ReentrantLock
cache::Dict{K, TimestampedValue{V}}
timeout::P
purge_on_timeout::Bool
end
Cache{K, V}(timeout::Dates.Period=Dates.Minute(1)) where {K, V} = Cache(ReentrantLock(), Dict{K, TimestampedValue{V}}(), timeout)
Cache{K, V}(timeout::Dates.Period=Dates.Minute(1); purge_on_timeout::Bool=false) where {K, V} = Cache(ReentrantLock(), Dict{K, TimestampedValue{V}}(), timeout, purge_on_timeout)

expired(x::TimestampedValue, timeout) = (Dates.now(Dates.UTC) - x.timestamp) > timeout

Expand Down Expand Up @@ -84,7 +92,20 @@ end

function Base.setindex!(cache::Cache{K, V}, val::V, key::K) where {K, V}
lock(cache.lock) do
cache.cache[key] = TimestampedValue{V}(val)
val1 = TimestampedValue{V}(val)
cache.cache[key] = val1
ts = timestamp(val1)
if cache.purge_on_timeout
Timer(div(Dates.toms(cache.timeout), 1000)) do _
lock(cache.lock) do
val2 = get(cache.cache, key, nothing)
# only delete if timestamp of original key matches
if val2 !== nothing && timestamp(val2) == ts
delete!(cache.cache, key)
end
end
end
end
return val
end
end
Expand Down
12 changes: 12 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,16 @@ sleep(3)
tm = @elapsed foo(1, "ffff")
@test tm > 2 # test that normal function body was executed

cache = ExpiringCaches.Cache{Int, Int}(Dates.Second(5); purge_on_timeout=true)
cache[1] = 2
@test !isempty(cache)
sleep(3)
cache[1] = 3
sleep(2.5)
# key isn't purged because we replaced it, so timer is "reset"
@test !isempty(cache)
sleep(3)
# key is now purged w/o being accessed
@test isempty(cache)

end

0 comments on commit 14d6dc6

Please sign in to comment.