Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix race conditions in SharedObservableRequest #35

Merged
merged 6 commits into from
Apr 25, 2023

Conversation

yatsinar
Copy link
Member

Replaces synchronisation with ConcurrentHashMap;

Exposes removeRequest(params) to be invoked by SharedSingleRequest in its own Single lifecycle;

Replaces synchronisation with ConcurrentHashMap;
Exposes removeRequest(params) to be invoked by SharedSingleRequest in its own Single lifecycle;
@yatsinar yatsinar requested a review from Karloid April 25, 2023 10:21
Data(cachedDomain, null, false),
// 2nd iteration
Data(cachedDomain, null, true),
Data(cachedDomain, null, false),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was previously asserting a buggy behaviour where last emit here would be the 4th
Data(cachedDomain, null, true).

This wasn't the idea as can be seen in the ReloadingDataScannerTest:

newEvent(Data(null, loading = true), shouldEmit = true)
newEvent(Data("a", loading = true), shouldEmit = true)
newEvent(Data("a", loading = false), shouldEmit = true)

newEvent(Data("a", loading = true), shouldEmit = true)
newEvent(Data("a", loading = false), shouldEmit = true)

The changes in this PR made for avoiding race conditions, apparently fixed that, so I've rewritten this test for better clarity.

load(params).toObservable()
load(params)
.doOnSuccess { removeRequest(params) }
.doOnError { removeRequest(params) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we need to call removeRequest at dispose as well (and yes, that removal should be more sophisticated and actually check a correspondence of the deleted request to the current one, or enforce somehow At Most Once behaviour for removeRequest)

there is test which checks that especial issue:

    @Test
    fun `GIVEN network not answer before unsubscription of first observe WHEN observe again THEN second observer see network response`() {
        // given
        whenever(fromNetwork.invoke(any())).thenReturn(Single.never())
        storage.remove(params)
        memCache.remove(params)

        val observer1 = dataObservableDelegate.observe(params = params, forceReload = false)
            .skipWhileLoading()
            .test()
        ioScheduler.triggerActions()

        observer1.dispose()
        observer1.assertValues()

        // when
        whenever(fromNetwork.invoke(any())).thenReturn(Single.just(domain))
        val observer2 = dataObservableDelegate.observe(params = params, forceReload = false)
            .skipWhileLoading()
            .test()
        ioScheduler.triggerActions()

        // then
        observer2.assertValues(Data(domain))    // ACTUAL: EMPTY LIST
    }
    ```

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do remove request on dispose, it's handled in SharedObservableRequest class via doFinally { removeRequest(params) }. here we only add extra removals on events that are specific to Single lifecycle - i.e. doOnSuccess, which doesn't exist in Observable lifecycle. Actually even doOnError is redundant.

Your test case fails because DoD has internal timeout logic (60s by default ) and it's keeping Single.never() alive during this period. Observer2 basically resubscribes to this request in this case. If you change it slightly to accomodate for timeouts:

        // when
        whenever(fromNetwork.invoke(any())).thenReturn(Single.just(domain))
        ioScheduler.advanceTimeBy(180, TimeUnit.SECONDS)

        val observer2 = dataObservableDelegate.observe(params = params, forceReload = false)
            .skipWhileLoading()
            .test()
        ioScheduler.triggerActions()

then it passes.

It's covered in WHEN unsubscribed from network and data NOT arrives in 60 seconds THEN data not saved so I won't be adding this case

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, thanks

Karloid
Karloid previously approved these changes Apr 25, 2023
@yatsinar yatsinar merged commit cca625a into master Apr 25, 2023
@yatsinar yatsinar deleted the fix_fetch_from_network_shared_observables branch April 25, 2023 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

3 participants