Skip to content

Commit aace40f

Browse files
authored
Merge pull request #471 from ReactiveCocoa/aggregate-builder-deadlock-fix
Fixed a deadlock upon disposal in AggregateBuilder.
2 parents 9f2171b + b82ea4f commit aace40f

File tree

3 files changed

+222
-83
lines changed

3 files changed

+222
-83
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# master
22
*Please add new entries at the top.*
33

4+
1. Fixed a deadlock upon disposal when combining operators, i.e. `zip` and `combineLatest`, are used. (#471, kudos to @stevebrambilla for catching the bug)
5+
46
# 2.0.0-rc.1
57
1. If the input observer of a `Signal` deinitializes while the `Signal` has not yet terminated, an `interrupted` event would now be automatically sent. (#463, kudos to @andersio)
68

Sources/Signal.swift

Lines changed: 116 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1959,42 +1959,48 @@ private enum ThrottleWhileState<Value> {
19591959
}
19601960
}
19611961

1962-
private protocol SignalAggregateStrategy {
1962+
private protocol SignalAggregateStrategy: class {
19631963
/// Update the latest value of the signal at `position` to be `value`.
19641964
///
19651965
/// - parameters:
19661966
/// - value: The latest value emitted by the signal at `position`.
19671967
/// - position: The position of the signal.
1968-
///
1969-
/// - returns: `true` if the aggregating signal should terminate as a result of the
1970-
/// update. `false` otherwise.
1971-
mutating func update(_ value: Any, at position: Int) -> Bool
1968+
func update(_ value: Any, at position: Int)
19721969

19731970
/// Record the completion of the signal at `position`.
19741971
///
19751972
/// - parameters:
19761973
/// - position: The position of the signal.
1977-
///
1978-
/// - returns: `true` if the aggregating signal should terminate as a result of the
1979-
/// completion. `false` otherwise.
1980-
mutating func complete(at position: Int) -> Bool
1974+
func complete(at position: Int)
1975+
1976+
init(count: Int, action: @escaping (AggregateStrategyEvent) -> Void)
1977+
}
19811978

1982-
init(count: Int, action: @escaping (ContiguousArray<Any>) -> Void)
1979+
private enum AggregateStrategyEvent {
1980+
case value(ContiguousArray<Any>)
1981+
case completed
19831982
}
19841983

19851984
extension Signal {
1986-
private struct CombineLatestStrategy: SignalAggregateStrategy {
1985+
// Threading of `CombineLatestStrategy` and `ZipStrategy`.
1986+
//
1987+
// The threading models of these strategies mirror that of `Signal.Core` to allow
1988+
// recursive termial event from the upstreams that is triggered by the combined
1989+
// values.
1990+
//
1991+
// The strategies do not unique the delivery of `completed`, since `Signal` already
1992+
// guarantees that no event would ever be delivered after a terminal event.
1993+
1994+
private final class CombineLatestStrategy: SignalAggregateStrategy {
19871995
private enum Placeholder {
19881996
case none
19891997
}
19901998

1991-
private var values: ContiguousArray<Any>
1992-
private var completionCount: Int
1993-
private let action: (ContiguousArray<Any>) -> Void
1999+
var values: ContiguousArray<Any>
19942000

19952001
private var _haveAllSentInitial: Bool
19962002
private var haveAllSentInitial: Bool {
1997-
mutating get {
2003+
get {
19982004
if _haveAllSentInitial {
19992005
return true
20002006
}
@@ -2004,47 +2010,74 @@ extension Signal {
20042010
}
20052011
}
20062012

2007-
mutating func update(_ value: Any, at position: Int) -> Bool {
2013+
private let count: Int
2014+
private let lock: Lock
2015+
2016+
private let completion: Atomic<Int>
2017+
private let action: (AggregateStrategyEvent) -> Void
2018+
2019+
func update(_ value: Any, at position: Int) {
2020+
lock.lock()
20082021
values[position] = value
20092022

20102023
if haveAllSentInitial {
2011-
action(values)
2024+
action(.value(values))
20122025
}
20132026

2014-
return false
2027+
lock.unlock()
2028+
2029+
if completion.value == self.count, lock.try() {
2030+
action(.completed)
2031+
lock.unlock()
2032+
}
20152033
}
20162034

2017-
mutating func complete(at position: Int) -> Bool {
2018-
completionCount += 1
2019-
return completionCount == values.count
2035+
func complete(at position: Int) {
2036+
let count: Int = completion.modify { count in
2037+
count += 1
2038+
return count
2039+
}
2040+
2041+
if count == self.count, lock.try() {
2042+
action(.completed)
2043+
lock.unlock()
2044+
}
20202045
}
20212046

2022-
init(count: Int, action: @escaping (ContiguousArray<Any>) -> Void) {
2023-
values = ContiguousArray(repeating: Placeholder.none, count: count)
2024-
completionCount = 0
2025-
_haveAllSentInitial = false
2047+
init(count: Int, action: @escaping (AggregateStrategyEvent) -> Void) {
2048+
self.count = count
2049+
self.lock = Lock.make()
2050+
self.values = ContiguousArray(repeating: Placeholder.none, count: count)
2051+
self._haveAllSentInitial = false
2052+
self.completion = Atomic(0)
20262053
self.action = action
20272054
}
20282055
}
20292056

2030-
private struct ZipStrategy: SignalAggregateStrategy {
2057+
private final class ZipStrategy: SignalAggregateStrategy {
2058+
private let stateLock: Lock
2059+
private let sendLock: Lock
2060+
20312061
private var values: ContiguousArray<[Any]>
2062+
private var canEmit: Bool {
2063+
return values.reduce(true) { $0 && !$1.isEmpty }
2064+
}
2065+
2066+
private var hasConcurrentlyCompleted: Bool
20322067
private var isCompleted: ContiguousArray<Bool>
2033-
private let action: (ContiguousArray<Any>) -> Void
20342068

20352069
private var hasCompletedAndEmptiedSignal: Bool {
20362070
return Swift.zip(values, isCompleted).contains(where: { $0.0.isEmpty && $0.1 })
20372071
}
20382072

2039-
private var canEmit: Bool {
2040-
return values.reduce(true) { $0 && !$1.isEmpty }
2041-
}
2042-
20432073
private var areAllCompleted: Bool {
20442074
return isCompleted.reduce(true) { $0 && $1 }
20452075
}
20462076

2047-
mutating func update(_ value: Any, at position: Int) -> Bool {
2077+
private let action: (AggregateStrategyEvent) -> Void
2078+
2079+
func update(_ value: Any, at position: Int) {
2080+
stateLock.lock()
20482081
values[position].append(value)
20492082

20502083
if canEmit {
@@ -2055,33 +2088,61 @@ extension Signal {
20552088
buffer.append(values[index].removeFirst())
20562089
}
20572090

2058-
action(buffer)
2091+
let shouldComplete = areAllCompleted || hasCompletedAndEmptiedSignal
2092+
sendLock.lock()
2093+
stateLock.unlock()
2094+
2095+
action(.value(buffer))
20592096

2060-
if hasCompletedAndEmptiedSignal {
2061-
return true
2097+
if shouldComplete {
2098+
action(.completed)
2099+
}
2100+
2101+
sendLock.unlock()
2102+
2103+
stateLock.lock()
2104+
2105+
if hasConcurrentlyCompleted {
2106+
sendLock.lock()
2107+
action(.completed)
2108+
sendLock.unlock()
20622109
}
20632110
}
20642111

2065-
return false
2112+
stateLock.unlock()
20662113
}
20672114

2068-
mutating func complete(at position: Int) -> Bool {
2115+
func complete(at position: Int) {
2116+
stateLock.lock()
20692117
isCompleted[position] = true
20702118

2071-
// `zip` completes when all signals has completed, or any of the signals
2072-
// has completed without any buffered value.
2073-
return hasCompletedAndEmptiedSignal || areAllCompleted
2119+
if hasConcurrentlyCompleted || areAllCompleted || hasCompletedAndEmptiedSignal {
2120+
if sendLock.try() {
2121+
stateLock.unlock()
2122+
2123+
action(.completed)
2124+
sendLock.unlock()
2125+
return
2126+
}
2127+
2128+
hasConcurrentlyCompleted = true
2129+
}
2130+
2131+
stateLock.unlock()
20742132
}
20752133

2076-
init(count: Int, action: @escaping (ContiguousArray<Any>) -> Void) {
2077-
values = ContiguousArray(repeating: [], count: count)
2078-
isCompleted = ContiguousArray(repeating: false, count: count)
2134+
init(count: Int, action: @escaping (AggregateStrategyEvent) -> Void) {
2135+
self.values = ContiguousArray(repeating: [], count: count)
2136+
self.hasConcurrentlyCompleted = false
2137+
self.isCompleted = ContiguousArray(repeating: false, count: count)
20792138
self.action = action
2139+
self.sendLock = Lock.make()
2140+
self.stateLock = Lock.make()
20802141
}
20812142
}
20822143

20832144
private final class AggregateBuilder<Strategy: SignalAggregateStrategy> {
2084-
fileprivate var startHandlers: [(_ index: Int, _ strategy: Atomic<Strategy>, _ action: @escaping (Signal<Never, Error>.Event) -> Void) -> Disposable?]
2145+
fileprivate var startHandlers: [(_ index: Int, _ strategy: Strategy, _ action: @escaping (Signal<Never, Error>.Event) -> Void) -> Disposable?]
20852146

20862147
init() {
20872148
self.startHandlers = []
@@ -2093,22 +2154,10 @@ extension Signal {
20932154
return signal.observe { event in
20942155
switch event {
20952156
case let .value(value):
2096-
let shouldComplete = strategy.modify {
2097-
return $0.update(value, at: index)
2098-
}
2099-
2100-
if shouldComplete {
2101-
action(.completed)
2102-
}
2157+
strategy.update(value, at: index)
21032158

21042159
case .completed:
2105-
let shouldComplete = strategy.modify {
2106-
return $0.complete(at: index)
2107-
}
2108-
2109-
if shouldComplete {
2110-
action(.completed)
2111-
}
2160+
strategy.complete(at: index)
21122161

21132162
case .interrupted:
21142163
action(.interrupted)
@@ -2126,17 +2175,20 @@ extension Signal {
21262175
private convenience init<Strategy: SignalAggregateStrategy>(_ builder: AggregateBuilder<Strategy>, _ transform: @escaping (ContiguousArray<Any>) -> Value) {
21272176
self.init { observer in
21282177
let disposables = CompositeDisposable()
2129-
let strategy = Atomic(Strategy(count: builder.startHandlers.count) { observer.send(value: transform($0)) })
2178+
let strategy = Strategy(count: builder.startHandlers.count) { event in
2179+
switch event {
2180+
case let .value(value):
2181+
observer.send(value: transform(value))
2182+
case .completed:
2183+
observer.sendCompleted()
2184+
}
2185+
}
21302186

21312187
for (index, action) in builder.startHandlers.enumerated() where !disposables.isDisposed {
21322188
disposables += action(index, strategy) { observer.action($0.map { _ in fatalError() }) }
21332189
}
21342190

2135-
return AnyDisposable {
2136-
strategy.modify { _ in
2137-
disposables.dispose()
2138-
}
2139-
}
2191+
return disposables
21402192
}
21412193
}
21422194

0 commit comments

Comments
 (0)