-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Description
Description
withObservationTracking
is supposed to ensure that, if called in a loop, it will reach "eventual consistency" with code modifying the observed properties. This is not currently true if the properties observed by withObservationTracking
's first closure are modified during or concurrently with the execution of that closure.
Two reproductions
- one for concurrent modification (this came up during the review of the swift-foundation "ProgressReporter" proposal)
- one for coincident modification (the problem is similar, but doesn't require concurrency to trigger)
Although these examples use Observations
, the bug is because withObservationTracking
waits until after the first closure has returned, to install tracking. It needs to install tracking as soon as access
is called.
Reproduction
Concurrent
import Observation
import Synchronization
final class MyObservable: Observable, Sendable {
let registrar = ObservationRegistrar()
private let _value = Mutex(0)
var value: Int {
registrar.access(self, keyPath: \.value)
return _value.withLock { $0 }
}
func increment() {
registrar.willSet(self, keyPath: \.value)
_value.withLock { $0 += 1 }
registrar.didSet(self, keyPath: \.value)
}
}
var test = 0
while true {
let object = MyObservable()
await withDiscardingTaskGroup { [test] group in
group.addTask {
var received = 0
for await _ in Observations({ object.value }).prefix(while: { $0 < 1 }) {
received += 1
}
print("test \(test) completed after receiving \(received) values")
}
group.addTask {
object.increment()
}
}
test += 1
}
swiftc -swift-version 6 WithObservationTrackingConcurrentModification.swift && ./WithObservationTrackingConcurrentModification
test 0 completed after receiving 0 values
test 1 completed after receiving 0 values
test 2 completed after receiving 1 values
test 3 completed after receiving 0 values
test 4 completed after receiving 1 values
test 5 completed after receiving 0 values
test 6 completed after receiving 0 values
test 7 completed after receiving 0 values
test 8 completed after receiving 0 values
test 9 completed after receiving 0 values
^C
Both "0" and "1" values received are legitimate (depending on whether Observations sees the initial 0 or not), but eventually it will hang, because the increment
call will run concurrently with the block passed to Observations
, and the update of the value to 1 will never be observed.
Coincident
import Observation
import Synchronization
final class MyObservable: Observable {
private let registrar = ObservationRegistrar()
private var _value = 0
var value: Int {
registrar.access(self, keyPath: \.value)
let result = _value
registrar.willSet(self, keyPath: \.value)
_value += 1
registrar.didSet(self, keyPath: \.value)
return result
}
}
let object = MyObservable()
for await value in Observations({ object.value }).prefix(while: { $0 < 1 }) {
print(value)
}
swiftc -swift-version 6 WithObservationTrackingCoincidentalMutation.swift && ./WithObservationTrackingCoincidentalMutation
0
^C
The sequence should observe the increment after the access, and be terminated by prefix
, but it never does, because the mutation happens during Observations' closure.
Expected behavior
Both examples should terminate (reliably)
Environment
swift-driver version: 1.127.10 Apple Swift version 6.2 (swiftlang-6.2.0.14.8 clang-1700.3.14.6)
Target: arm64-apple-macosx26.0
Additional information
No response