Skip to content

Commit ded5801

Browse files
authored
Merge pull request #267 from ReactiveCocoa/signal-deadlock-fix
Fixed a race condition in `Signal` terminal event handling.
2 parents ef9718e + b9b7ea7 commit ded5801

File tree

3 files changed

+39
-4
lines changed

3 files changed

+39
-4
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ branches:
99
- master
1010
# Credit: @Omnikron13, https://github.com/mojombo/semver/issues/32
1111
- /^(\d+\.\d+\.\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/
12+
- /^hotfix-(\d+\.\d+\.\d+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/
1213
before_script:
1314
- git submodule update --init --recursive
1415
script:

Sources/Signal.swift

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,24 @@ public final class Signal<Value, Error: Swift.Error> {
313313
return ActionDisposable { [weak self] in
314314
if let s = self {
315315
s.updateLock.lock()
316+
316317
if case let .alive(snapshot) = s.state {
317318
var observers = snapshot.observers
318319
observers.remove(using: token)
319-
s.state = .alive(AliveState(observers: observers,
320-
retaining: observers.isEmpty ? nil : self))
320+
321+
// Ensure the old signal state snapshot does not deinitialize before
322+
// `updateLock` is released. Otherwise, it might result in a
323+
// deadlock in cases where a `Signal` legitimately receives terminal
324+
// events recursively as a result of the deinitialization of the
325+
// snapshot.
326+
withExtendedLifetime(snapshot) {
327+
s.state = .alive(AliveState(observers: observers,
328+
retaining: observers.isEmpty ? nil : self))
329+
s.updateLock.unlock()
330+
}
331+
} else {
332+
s.updateLock.unlock()
321333
}
322-
s.updateLock.unlock()
323334
}
324335
}
325336
} else {

Tests/ReactiveSwiftTests/ActionSpec.swift

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ class ActionSpec: QuickSpec {
5858
action.errors.observeValues { errors.append($0) }
5959
action.completed.observeValues { completedCount += 1 }
6060
}
61-
61+
6262
it("should retain the state property") {
6363
var property: MutableProperty<Bool>? = MutableProperty(false)
6464
weak var weakProperty = property
@@ -114,6 +114,29 @@ class ActionSpec: QuickSpec {
114114
expect(action.isExecuting.value) == false
115115
}
116116

117+
it("should not deadlock") {
118+
final class ViewModel {
119+
let action2 = Action<(), (), NoError> { SignalProducer(value: ()) }
120+
}
121+
122+
let action1 = Action<(), ViewModel, NoError> { SignalProducer(value: ViewModel()) }
123+
124+
// Fixed in #267. (https://github.com/ReactiveCocoa/ReactiveSwift/pull/267)
125+
//
126+
// The deadlock happened as the observer disposable releases the closure
127+
// `{ _ in viewModel }` here without releasing the mapped signal's
128+
// `updateLock` first. The deinitialization of the closure triggered the
129+
// propagation of terminal event of the `Action`, which eventually hit
130+
// the mapped signal and attempted to acquire `updateLock` to transition
131+
// the signal's state.
132+
action1.values
133+
.flatMap(.latest) { viewModel in viewModel.action2.values.map { _ in viewModel } }
134+
.observeValues { _ in }
135+
136+
action1.apply().start()
137+
action1.apply().start()
138+
}
139+
117140
if #available(macOS 10.10, *) {
118141
it("should not loop indefinitely") {
119142
let condition = MutableProperty(1)

0 commit comments

Comments
 (0)