[Fix] Update key generation for adding handlers#13
[Fix] Update key generation for adding handlers#13Daniel1of1 wants to merge 3 commits intoRuiAAPeres:masterfrom
Conversation
Previous strategy was failing with Swift 5. It seems that Dictionary.keys is somehow not atomic. I have not checked the diffs to get to the root cause, however this now passes tests and I added a performance test just to check that it does not add any overhead.
Previous solution did not take into account disposing subscriptions. This fixes this, using `Int.random` for key generation. Also added unsubscribing (disposing) to the performance test.
|
Update: I quickly saw that my first solution -
let receiver = Receiver<Int>.make()
let d1 = receiver.listen(h1) // 1
let d2 = receiver.listen(h2) // 2
d1.dispose() // 3
d3 = receiver.listen(h3) // 4
// handler mutations
// 1 - adding h1
_key = _handlers.count // _key = 0
_handlers[_key] = handle
// _handlers = [ 0: h1 ]
// 2 - adding h2
_key = _handlers.count // _key = 1
_handlers[_key] = handle
// _handlers = [ 0: h1 , 1: h2 ]
// 3 - removing h1
_handlers[_key] = nil // _key = 0 as obtained from d1
// _handlers = [ 1: h2 ]
// 4 - adding h3
_key = _handlers.count // _key = 1
_handlers[_key] = handle
// _handlers = [ 1: h3 ] ❌
// expected [ 1: h2, 2: h3 ] or anything that does not overwrite h2It turns out this is essentially the same reason for the failure of the original implementation (i.e generating a key that already exists), it was just less obvious and I falsely assumed was some kind of race issue (which I thought was odd since it was done inside an atomic apply) Original implementation (current master): (failure case) let receiver = Receiver<Int>.make()
let d1 = receiver.listen(h1) // 1
let d2 = receiver.listen(h2) // 2
d1.dispose() // 3
d3 = receiver.listen(h3) // 4
// handler mutations
// 1 - adding h1
_key = (_handlers.keys.map { $0.hashValue }.max() ?? -1) + 1
// _key == ([] ->(hashValue)-> [] -(max)-> nil ?? -1) + 1 == 0
_handlers[_key] = handle
// _handlers = [ 0: h1 ]
// 2 - adding h2 // e.g 0.hashValue == 100
_key = (_handlers.keys.map { $0.hashValue }.max() ?? -1) + 1
// _key == ([0] ->(hashValue)-> [100] -(max)-> 100) + 1 == 101
_handlers[_key] = handle
// _handlers = [ 0: h1 , 101: h2 ]
// 3 - removing h1 - unlike `be8ee36` this does not have any affect on correctness
_handlers[_key] = nil // _key = 0 as obtained from d1
// _handlers = [ 101: h2 ]
// 4 - adding h3 // e.g 101.hashValue == 30
_key = (_handlers.keys.map { $0.hashValue }.max() ?? -1) + 1
// [0,101] ->(hashValue)-> [100, 33] -(max)-> 100
// _key == ([0,101] ->(hashValue)-> [100,33] -(max)-> 100) + 1 == 101
_handlers[_key] = handle
// _handlers = [ 101: h3 ] ❌
// expected [ 101: h2, `X`: h3 ] for some X != 101The new Implementation simply uses _key = Int.random(in: Int.min...Int.max)
while _handlers[_key] != nil {
_key = Int.random(in: Int.min...Int.max)
}
_handlers[_key] = handle As mentioned in this comment in #11 by @kevincador (also apologies for stepping on your toes a bit here, I should have reached out before proposing a solution)... Thanks for reading 😊 |
|
Nice @Daniel1of1 ! Note: I didn't investigate it yet but iOS 13 will introduce a new framework (https://developer.apple.com/documentation/combine). I don't know if it will replace Receiver (I'd like to have @RuiAAPeres input on this one 😇). |
|
Oh wow, thanks for all this work guys! I need to see if this is working as intended. @kevincador are you using Receiver in production? |
|
Yes, I'm using Receiver in production since 1.0 of Rippple (https://itunes.apple.com/us/app/rippple-tv-movie-comments/id1309894528) launched in early 2018. |
|
@kevincador regarding Combine, it doesn't support iOS 12. It's slightly more complex than Receiver as well: it has more functionality. |
|
@Daniel1of1 I am checking how ReactiveSwift does it, and I think it would provide a more robust solution: https://github.com/ReactiveCocoa/ReactiveSwift/blob/master/Sources/Bag.swift#L45 What do you think? |
|
@RuiAAPeres This seems perfectly suitable, I can essentially just lift it from there. And I'll update accordingly. Do you have a preference on whether to wrap the key in a |
|
I am ok with either. |
|
@RuiAAPeres done |
Previous strategy was failing with newer swift versions, this is the same issue as #11.
[Edit]
It seems thatThis is not true, see below [/Edit]Dictionary.keysis somehow not atomic.I have not checked the diffs to get to the root cause, however this now passes tests and I added
a performance test just to check that it does not add any overhead.