I'm trying to share the same Flowable to multiple subscribers based on an id, while each subscriber can cancel the subscription when it needs in order to unsubscribe. A new subscription should be created if all of the subscribers for a certain id unsubscribed and then another one tries to subscribe for that id. The following code is written in Kotlin, trying to achieve this functionality.
class SharedFlowProvider() {
private val flowProvider = FlowProvider()
private val eventFlowById = HashMap<String, Flowable<ComputedProperties>>()
@Synchronized
override fun subscribeToProperties(subscriber: CancellableSubscriber<ComputedProperties>, id: String){
eventFlowById.computeIfAbsent(id) { buildFlowable(id) }
.subscribe(orderBasedSubscriber)
}
private fun buildFlowable(id: String) =
flowProvider.getFlowable(id)
.doFinally { removeSubscription(id) }
.share()
@Synchronized
private fun removeSubscription(id: String) {
eventFlowById.remove(id)
}
}
The problem appears when there is a cancel followed by a subscribe on the same id. I discovered a possible deadlock in this approach, because of the way the FlowableRefCount returned by share works. In the underlying implementation, FlowableRefCount uses a synchronization mechanism in its subscribeActual and cancel methods, and while it isn't obvious at first sight, the action from the doFinally method is run inside that locking mechanism. This is a little bit counterintuitive, since in the doFinally doc it says that:
Note that the onFinally action is shared between subscriptions and as such should be thread-safe.
Because of this, the following scenario appeared:
- lock on the FlowableRefCount instance through cancel
- subscribeToProperties called on the same id as in the cancel
- subscribeToProperties blocked when calling subscribe(and then subscribeActual), since the lock is already acquired on the FlowableRefCount through cancel
- call on removeOrderSubscription blocked, since the lock on SharedFlowProvider is already taken through subscribeToProperties method
I've also tried to split the locking from subscribeToProperties method, using a ConcurrentHashMap in the following way:
private val eventFlowById = ConcurrentHashMap<String, Flowable<ComputedProperties>>()
override fun subscribeToProperties(subscriber: CancellableSubscriber<ComputedProperties>, id: String){
val flow = eventFlowById.computeIfAbsent(id) { buildFlowable(id) }
flow.subscribe(orderBasedSubscriber)
//the flow associated with an id could be removed by a cancel done before the subscribe call, so we need to make sure it is added to the map
eventFlowById.computeIfAbsent(id) { flow }
}
But in this approach, if we have a subscribe followed by a quick cancel done before the last line from the method, we could end up with the flow being put in the map, even though we don't have subscribers to it.
I would appreciate some ideas on how I could achieve this functionality without a deadlock or a race condition.
Thank you