I'm having trouble with Swift Combine resulting in leaks of upstream publishers, specifically when using makeConnectable(). I have created demo code to highlight the issue but it's complicated, so let me break down the variables:
- Synchronously vs Asynchronously cancelling the connection returned by
MakeConnectable.connect(). - Storing
Publisher.MakeConnectablein a local variable. - Finishing or leaving the upstream publisher in an unfinished state.
3 variables results in 8 expanded cases. Here are the results:
| Cancel Timing | Local Variable | Finish Upstream | Bad Result (Leak) |
|---|---|---|---|
| SYNC | YES | YES | NO |
| ASYNC | YES | YES | NO |
| SYNC | YES | NO | NO |
| ASYNC | YES | NO | YES |
| SYNC | NO | YES | NO |
| ASYNC | NO | YES | NO |
| SYNC | NO | NO | YES |
| ASYNC | NO | NO | YES |
See Code
let pubCancelSyncThenFinishIndirect = PassthroughSubject<Void, Never>()
let pubCancelAsyncThenFinishIndirect = PassthroughSubject<Void, Never>()
let pubCancelSyncNoFinishIndirect = PassthroughSubject<Void, Never>()
let pubCancelAsyncNoFinishIndirect = PassthroughSubject<Void, Never>() // <- leaks
let pubCancelSyncThenFinishDirect = PassthroughSubject<Void, Never>()
let pubCancelAsyncThenFinishDirect = PassthroughSubject<Void, Never>()
let pubCancelSyncNoFinishDirect = PassthroughSubject<Void, Never>() // <- leaks
let pubCancelAsyncNoFinishDirect = PassthroughSubject<Void, Never>() // <- leaks
let cancelSyncThenFinishConnectable = pubCancelSyncThenFinishIndirect.makeConnectable()
let cancelAsyncThenFinishConnectable = pubCancelAsyncThenFinishIndirect.makeConnectable()
let cancelSyncNoFinishConnectable = pubCancelSyncNoFinishIndirect.makeConnectable()
let cancelAsyncNoFinishConnectable = pubCancelAsyncNoFinishIndirect.makeConnectable()
let cancelSyncThenFinishConnectionIndirect = cancelSyncThenFinishConnectable.connect()
let cancelAsyncThenFinishConnectionIndirect = cancelAsyncThenFinishConnectable.connect()
let cancelSyncNoFinishConnectionIndirect = cancelSyncNoFinishConnectable.connect()
let cancelAsyncNoFinishConnectionIndirect = cancelAsyncNoFinishConnectable.connect()
let cancelSyncThenFinishConnectionDirect = pubCancelSyncThenFinishDirect.makeConnectable().connect()
let cancelAsyncThenFinishConnectionDirect = pubCancelAsyncThenFinishDirect.makeConnectable().connect()
let cancelSyncNoFinishConnectionDirect = pubCancelSyncNoFinishDirect.makeConnectable().connect()
let cancelAsyncNoFinishConnectionDirect = pubCancelAsyncNoFinishDirect.makeConnectable().connect()
cancelSyncThenFinishConnectionIndirect.cancel()
cancelSyncNoFinishConnectionIndirect.cancel()
cancelSyncThenFinishConnectionDirect.cancel()
cancelSyncNoFinishConnectionDirect.cancel()
DispatchQueue.main.async {
cancelAsyncThenFinishConnectionIndirect.cancel()
cancelAsyncNoFinishConnectionIndirect.cancel()
cancelAsyncThenFinishConnectionDirect.cancel()
cancelAsyncNoFinishConnectionDirect.cancel()
pubCancelSyncThenFinishIndirect.send(completion: .finished)
pubCancelAsyncThenFinishIndirect.send(completion: .finished)
pubCancelSyncThenFinishDirect.send(completion: .finished)
pubCancelAsyncThenFinishDirect.send(completion: .finished)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4) { [
weak pubCancelSyncThenFinishIndirect,
weak pubCancelAsyncThenFinishIndirect,
weak pubCancelSyncNoFinishIndirect,
weak pubCancelAsyncNoFinishIndirect,
weak pubCancelSyncThenFinishDirect,
weak pubCancelAsyncThenFinishDirect,
weak pubCancelSyncNoFinishDirect,
weak pubCancelAsyncNoFinishDirect,
] in
print("*** pubCancelSyncThenFinishIndirect is still alive: \(pubCancelSyncThenFinishIndirect == nil ? "NO" : "YES")")
print("*** pubCancelAsyncThenFinishIndirect is still alive: \(pubCancelAsyncThenFinishIndirect == nil ? "NO" : "YES")")
print("*** pubCancelSyncNoFinishIndirect is still alive: \(pubCancelSyncNoFinishIndirect == nil ? "NO" : "YES")")
print("*** pubCancelAsyncNoFinishIndirect is still alive: \(pubCancelAsyncNoFinishIndirect == nil ? "NO" : "YES")")
print("*** pubCancelSyncThenFinishDirect is still alive: \(pubCancelSyncThenFinishDirect == nil ? "NO" : "YES")")
print("*** pubCancelAsyncThenFinishDirect is still alive: \(pubCancelAsyncThenFinishDirect == nil ? "NO" : "YES")")
print("*** pubCancelSyncNoFinishDirect is still alive: \(pubCancelSyncNoFinishDirect == nil ? "NO" : "YES")")
print("*** pubCancelAsyncNoFinishDirect is still alive: \(pubCancelAsyncNoFinishDirect == nil ? "NO" : "YES")")
}
I would really appreciate it if someone can explain why this code consistently keeps the upstream publisher alive in some cases when I would expect it to disappear in every case.