diff --git a/src/containers/interface.jl b/src/containers/interface.jl index f37f4d6..0ad2532 100644 --- a/src/containers/interface.jl +++ b/src/containers/interface.jl @@ -133,6 +133,8 @@ Base.isempty(c::AbstractCache) = @lock c.lock isempty(c.index) # Iteration walks a snapshot copied under the lock, so it is safe while other tasks use the # cache (holding the lock across `iterate` calls would deadlock on an early `break`). +# The length may change between `length` and `iterate`, so `collect` must not rely on it. +Base.IteratorSize(::Type{<:AbstractCache}) = Base.SizeUnknown() function Base.iterate(c::AbstractCache) snapshot = @lock c.lock _pairs(c) return iterate(c, (snapshot, 1)) diff --git a/test/test_containers.jl b/test/test_containers.jl index e83b854..99d9788 100644 --- a/test/test_containers.jl +++ b/test/test_containers.jl @@ -105,6 +105,24 @@ end @test all(((k, v),) -> v == 3k, c) end +@testset "$C: collect while other tasks write" for C in CACHETYPES + c = C{Int, Int}(; maxsize = 50) + done = Threads.Atomic{Bool}(false) + writer = Threads.@spawn while !done[] + foreach(k -> get!(() -> k, c, k), 1:200) + empty!(c) + yield() + end + ok = true + for _ in 1:2_000 + ok &= all(((k, v),) -> k == v, collect(c)) # used to throw when the length changed + yield() + end + done[] = true + wait(writer) + @test ok +end + @testset "$C: freed slots release their values" for C in CACHETYPES c = C{Int, Base.RefValue{Int}}(; maxsize = 3) # build the value inside a function so no local keeps it alive