Skip to content

Commit 5dcbed7

Browse files
committed
Don't hold a lock over a continuation in Transaction
Motivation: The various 'withMumbleContinuation' APIs are supposed to be invoked synchronously with the caller. This assumption allows a lock to be acquired before the call and released from the body of the 'withMumbleContinuation' after e.g. storing the continuation. However this isn't the case and the job may be re-enqueued on the executor meaning that this is pattern is vulnerable to deadlocks. Modifications: - Drop and reacquire the lock in Transaction Result: Lower chance of deadlock
1 parent b2faff9 commit 5dcbed7

File tree

1 file changed

+30
-13
lines changed

1 file changed

+30
-13
lines changed

Sources/AsyncHTTPClient/AsyncAwait/Transaction.swift

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,26 +105,43 @@ final class Transaction:
105105

106106
struct BreakTheWriteLoopError: Swift.Error {}
107107

108-
// FIXME: Refactor this to not use `self.state.unsafe`.
109108
private func writeRequestBodyPart(_ part: ByteBuffer) async throws {
110-
self.state.unsafe.lock()
111-
switch self.state.unsafe.withValueAssumingLockIsAcquired({ state in state.writeNextRequestPart() }) {
109+
let action = self.state.withLockedValue { state in
110+
state.writeNextRequestPart()
111+
}
112+
113+
switch action {
112114
case .writeAndContinue(let executor):
113-
self.state.unsafe.unlock()
114115
executor.writeRequestBodyPart(.byteBuffer(part), request: self, promise: nil)
115-
116-
case .writeAndWait(let executor):
116+
case .writeAndWait:
117+
// Holding the lock here *should* be safe but because of a bug in the runtime
118+
// it isn't, so drop the lock, create the continuation and try again.
119+
//
120+
// See https://github.com/swiftlang/swift/issues/85668
117121
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
118-
self.state.unsafe.withValueAssumingLockIsAcquired({ state in
119-
state.waitForRequestBodyDemand(continuation: continuation)
120-
})
121-
self.state.unsafe.unlock()
122+
let action = self.state.withLockedValue { state in
123+
// Check that nothing has changed between dropping and re-acquiring the lock.
124+
let action = state.writeNextRequestPart()
125+
switch action {
126+
case .writeAndContinue, .fail:
127+
()
128+
case .writeAndWait:
129+
state.waitForRequestBodyDemand(continuation: continuation)
130+
}
131+
return action
132+
}
122133

123-
executor.writeRequestBodyPart(.byteBuffer(part), request: self, promise: nil)
134+
switch action {
135+
case .writeAndContinue(let executor):
136+
executor.writeRequestBodyPart(.byteBuffer(part), request: self, promise: nil)
137+
continuation.resume()
138+
case .writeAndWait(let executor):
139+
executor.writeRequestBodyPart(.byteBuffer(part), request: self, promise: nil)
140+
case .fail:
141+
continuation.resume(throwing: BreakTheWriteLoopError())
142+
}
124143
}
125-
126144
case .fail:
127-
self.state.unsafe.unlock()
128145
throw BreakTheWriteLoopError()
129146
}
130147
}

0 commit comments

Comments
 (0)