Closed jasdeepsaini closed 2 years ago
Very nice catch, thanks!
Merging #137 (c522cd1) into main (5818492) will decrease coverage by
0.03%
. The diff coverage is91.94%
.
@@ Coverage Diff @@
## main #137 +/- ##
==========================================
- Coverage 95.53% 95.49% -0.04%
==========================================
Files 68 70 +2
Lines 3833 4111 +278
==========================================
+ Hits 3662 3926 +264
- Misses 171 185 +14
Impacted Files | Coverage Δ | |
---|---|---|
Tests/CurrentValueRelayTests.swift | 93.86% <91.17%> (-4.82%) |
:arrow_down: |
Sources/Common/Sink.swift | 66.00% <100.00%> (+8.50%) |
:arrow_up: |
Sources/Relays/CurrentValueRelay.swift | 94.28% <100.00%> (+0.16%) |
:arrow_up: |
Sources/Operators/PrefixWhileBehavior.swift | 92.00% <0.00%> (ø) |
|
Tests/PrefixWhileBehaviorTests.swift | 100.00% <0.00%> (ø) |
Continue to review full report at Codecov.
Legend - Click here to learn more
Δ = absolute <relative> (impact)
,ø = not affected
,? = missing data
Powered by Codecov. Last update 5818492...c522cd1. Read the comment docs.
Issue 1 (commit)
Scenario 1
Depending on the order in which a CurrentValueRelay and its subscription are deallocated, the relay can leak its stored object.
This does not leak when the containing object is deallocated.
This leaks when the containing object is deallocated.
Other conditions can lead to the leak, but this is the easiest one to reproduce and test. Here's a project which produces the issue.
When I first found this issue, I thought it was a fluke, but the order in which objects are deallocated is deterministic but not guaranteed in Swift. Here's a swift issue which documents the behavior.
Here's what happens to cause the leak.
Original Declaration:
When the containing object is deallocated,
cancellables
is deallocated first. This causescancel
to be called on the subscription to the relay. CodeThen the relay is deallocated. Code
This causes forceFinish to be called for the subscription, but
sink
is nil at this point, so nothing happens. CodeAt this point, the relay is deallocated, but the object it stored gets leaked.
This PR fixes the issue by calling
forceFinish
in thecancel
method of the subscription. That forces the sink to clean up its memory.The order of declarations determines how the stream terminates. The subscription will receive a finished event if the relay is deallocated first. If cancellable is deallocated first, the subscription will be canceled and will never receive a finished event.
This is the correct behavior. In the documentation for AnyCancellable, it says:
Once a stream has been canceled, it won't accept any new events. So canceling the stream causes the finished event to be ignored.
Scenario 2 (Project has been updated to show this scenario)
If a
withLatestFrom
is added to the relay, two objects will be leaked ifcancellables
comes before the declaration of the initial relay.This order of declarations causes a leak when the containing object is deallocated.
The flow is the same as Scenario 1, except setting the sink to nil causes the cancel method in
withLatestFrom
to be skipped. CodeSince the method is skipped, the value objects for relay
and
withLatestFromRelay` leak.Issue 2 (commit)
Fixing issue 1 led to a crash in the second scenario mentioned above. The original leak and crash caused by the fix were caught for the 'withLatestFrom' operator, but both could happen for any publisher that uses 'Sink.'
Now
cancel()
is called in thewithLatestFrom
operator which causessink?.cancelUpstream()
to be called. In this case, upstream isrelay
and has already been canceled. CodeThe
sink?.cancelUpstream
call thecancelUpstream
method inSink
. CodeThat leads to the
kill
method inDemandBuffer.
CodeThis method causes cancel to be called on
relay,
which was canceled to start this sequence of calls. Calling cancel twice on aSubscription
causes a crash. The Apple Docs states,You can only cancel a
Subscriptiononce.