Skip to content

[MAJOR] concat() unsubscribe does not tear down the active source after the first source completes #71

Description

@docxology

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions