Skip to content

Commit

Permalink
Add support for serialization of large caches (#46)
Browse files Browse the repository at this point in the history
* add serialization support via extension

* change version, update compat

* chg test to check if key-value sets are same for caches

Co-authored-by: Eric Hanson <[email protected]>

* chg: julia ext for v >= 1.9, dep for v < 1.9

* chg: Dict of pointers to IdDict

* chg: File write to IOBuffer

Co-authored-by: Eric Hanson <[email protected]>

* rm: unnecessary exports

Co-authored-by: Eric Hanson <[email protected]>

* fix

* chg: clean up next/prev calc using mod1

* chg: distinguise size from serializer

Co-authored-by: Lukas <[email protected]>

* chg: add sizehint! for IdDict

Co-authored-by: Lukas <[email protected]>

* chg: clean up keyset test

* chg: remove intermediate steps for serialize

* clean up and handle/test edge cases

* test: mutable reference

* chg: serialize size

* fix: update cache on every iteration but the first

* fix: compat with Julia 1.x

---------

Co-authored-by: Eric Hanson <[email protected]>
Co-authored-by: Lukas <[email protected]>
  • Loading branch information
3 people authored Jan 25, 2024
1 parent 3125353 commit bc3fd23
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 2 deletions.
14 changes: 12 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
name = "LRUCache"
uuid = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637"
version = "1.6.0"
version = "1.6.1"

[deps]
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[weakdeps]
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"

[extensions]
SerializationExt = ["Serialization"]

[compat]
julia = "1"

[extras]
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "Random"]
test = ["Test", "Random", "Serialization"]
76 changes: 76 additions & 0 deletions ext/SerializationExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
module SerializationExt
using LRUCache
using Serialization

function Serialization.serialize(s::AbstractSerializer, lru::LRU{K, V}) where {K, V}
Serialization.writetag(s.io, Serialization.OBJECT_TAG)
serialize(s, typeof(lru))
@assert lru.currentsize == length(lru)
serialize(s, lru.currentsize)
serialize(s, lru.maxsize)
serialize(s, lru.hits)
serialize(s, lru.misses)
serialize(s, lru.lock)
serialize(s, lru.by)
serialize(s, lru.finalizer)
for (k, val) in lru
serialize(s, k)
serialize(s, val)
sz = lru.dict[k][3]
serialize(s, sz)
end
end

function Serialization.deserialize(s::AbstractSerializer, ::Type{LRU{K, V}}) where {K, V}
currentsize = Serialization.deserialize(s)
maxsize = Serialization.deserialize(s)
hits = Serialization.deserialize(s)
misses = Serialization.deserialize(s)
lock = Serialization.deserialize(s)
by = Serialization.deserialize(s)
finalizer = Serialization.deserialize(s)

dict = Dict{K, Tuple{V, LRUCache.LinkedNode{K}, Int}}()
sizehint!(dict, currentsize)
# Create node chain
first = nothing
node = nothing
for i in 1:currentsize
prev = node
k = deserialize(s)
node = LRUCache.LinkedNode{K}(k)
val = deserialize(s)
sz = deserialize(s)
dict[k] = (val, node, sz)
if i == 1
first = node
continue
else
prev.next = node
node.prev = prev
end
end
# close the chain if any node exists
if node !== nothing
node.next = first
first.prev = node
end

# Createa cyclic ordered set from the node chain
keyset = LRUCache.CyclicOrderedSet{K}()
keyset.first = first
keyset.length = currentsize

# Create the LRU
lru = LRU{K,V}(maxsize=maxsize)
lru.dict = dict
lru.keyset = keyset
lru.currentsize = currentsize
lru.hits = hits
lru.misses = misses
lru.lock = lock
lru.by = by
lru.finalizer = finalizer
lru
end
end
4 changes: 4 additions & 0 deletions src/LRUCache.jl
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,8 @@ function _finalize_evictions!(finalizer, evictions)
return
end

if !isdefined(Base, :get_extension)
include("../ext/SerializationExt.jl")
end

end # module
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -297,3 +297,4 @@ end
end

include("originaltests.jl")
include("serializationtests.jl")
51 changes: 51 additions & 0 deletions test/serializationtests.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Serialization
@testset "Large Serialize and Deserialize" begin

cache = LRU{Int, Int}(maxsize=100_000)

# Populate the cache with dummy data
num_entries_to_test = [0, 1, 2, 3, 4, 5, 100_000, 1_000_000]
for i in 0:maximum(num_entries_to_test)
# Add dummy data on all but the first iteration,
# to test an empty cache
i > 0 && (cache[i] = i+1)
i num_entries_to_test || continue
io = IOBuffer()
serialize(io, cache)
seekstart(io)
deserialized_cache = deserialize(io)

# Check that the cache is the same
@test cache.maxsize == deserialized_cache.maxsize
@test cache.currentsize == deserialized_cache.currentsize
@test cache.hits == deserialized_cache.hits
@test cache.misses == deserialized_cache.misses
@test cache.by == deserialized_cache.by
@test cache.finalizer == deserialized_cache.finalizer
@test cache.keyset.length == deserialized_cache.keyset.length
@test issetequal(collect(cache), collect(deserialized_cache))
# Check that the cache has the same keyset
@test length(cache.keyset) == length(deserialized_cache.keyset)
@test all(((c_val, d_val),) -> c_val == d_val, zip(cache.keyset, deserialized_cache.keyset))
# Check that the cache keys, values, and sizes are the same
for (key, (c_value, c_node, c_s)) in cache.dict
d_value, d_node, d_s = deserialized_cache.dict[key]
c_value == d_value || @test false
c_node.val == d_node.val || @test false
c_s == d_s || @test false
end
end
end

@testset "Serialize mutable references" begin
lru = LRU(; maxsize=5)
a = b = [1]
lru[1] = a
lru[2] = b
@test lru[1] === lru[2]
io = IOBuffer()
serialize(io, lru)
seekstart(io)
lru2 = deserialize(io)
@test lru2[1] === lru2[2]
end

2 comments on commit bc3fd23

@ericphanson
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/99507

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v1.6.1 -m "<description of version>" bc3fd236f9c86e6a167b1985ca0d4fce69d19579
git push origin v1.6.1

Please sign in to comment.