Package/version: Rocket.jl v1.9.0
Environment: Julia 1.12.6
Summary
ConcatInnerActor.on_complete! has a guard that is always false, so after the first
source completes the subscription for the currently-active source is never stored.
unsubscribe!(concatSubscription) then unsubscribes a stale (completed) source and the
running source keeps emitting.
src/observable/concat.jl:93-104
function on_complete!(actor::ConcatInnerActor)
cindex = get_current_index(actor)
if cindex === length(actor.sources)
complete!(actor.actor)
else
set_current_index!(actor, cindex + 1) # line 98
subscription = subscribe!(actor.sources[cindex+1], actor) # line 99
if get_current_index(actor) === cindex # line 100 <- always false
set_subscription!(actor, subscription) # line 101
end
end
end
current_index is already advanced to cindex + 1 on line 98, so the line-100 comparison
can never be true; set_subscription! (the only update to actor.subscription for the new
active source) is dead code. on_subscribe! (lines 57-63) keeps an analogous, working guard
(get_current_index === 1), confirming line 100 should compare against cindex + 1.
Repro
sub_a = Subject(Int); sub_b = Subject(Int); out = Int[]
sub = subscribe!(concat(sub_a, sub_b), lambda(on_next=v->push!(out,v)))
next!(sub_a, 1); complete!(sub_a) # concat advances to sub_b
next!(sub_b, 2)
unsubscribe!(sub) # should detach sub_b
next!(sub_b, 3) # still delivered -> leak
sleep(0.1)
println(out) # => [1, 2, 3]
Expected vs Actual
Expected: after unsubscribe!, next!(sub_b, 3) is dropped → [1, 2].
Actual: [1, 2, 3] — the active (2nd) source is not unsubscribed. For an unbounded source
(interval, timer, network) this leaks the underlying resource forever and spurious
post-unsubscribe emissions reach the observer.
Suggested fix
Track the newly-set index:
set_current_index!(actor, cindex + 1)
subscription = subscribe!(actor.sources[cindex+1], actor)
if get_current_index(actor) === cindex + 1
set_subscription!(actor, subscription)
end
Fix PR
See prs/pr-03-concat-teardown-leak.md.
Package/version: Rocket.jl v1.9.0
Environment: Julia 1.12.6
Summary
ConcatInnerActor.on_complete!has a guard that is alwaysfalse, so after the firstsource completes the subscription for the currently-active source is never stored.
unsubscribe!(concatSubscription)then unsubscribes a stale (completed) source and therunning source keeps emitting.
src/observable/concat.jl:93-104current_indexis already advanced tocindex + 1on line 98, so the line-100 comparisoncan never be true;
set_subscription!(the only update toactor.subscriptionfor the newactive source) is dead code.
on_subscribe!(lines 57-63) keeps an analogous, working guard(
get_current_index === 1), confirming line 100 should compare againstcindex + 1.Repro
Expected vs Actual
Expected: after
unsubscribe!,next!(sub_b, 3)is dropped →[1, 2].Actual:
[1, 2, 3]— the active (2nd) source is not unsubscribed. For an unbounded source(
interval,timer, network) this leaks the underlying resource forever and spuriouspost-unsubscribe emissions reach the observer.
Suggested fix
Track the newly-set index:
Fix PR
See
prs/pr-03-concat-teardown-leak.md.