1

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

Vlad Piscu
  • 11
  • 2
  • You may have to serialize the whole source creation, removal and subscription process. for example, by submitting orders to a trampoline scheduler's worker. – akarnokd Feb 09 '21 at 16:08

0 Answers0