apollo-kotlin icon indicating copy to clipboard operation
apollo-kotlin copied to clipboard

Stuck in constant query loop

Open giantramen opened this issue 4 years ago • 18 comments

Summary I have two different queries that return the same data fragment (cached by id) that changes every time it is fetched from the server. When one query completes it causes the other query to fire again and gets into a never-ending loop.

Description

I have a schema that has something like this: Site ..[Photo]

Gallery ..[Photo]

Example: I fetch a site with a photo with id "1", and then I fetch a gallery which returns the same photo with id "1" but with some different data, the site query fires again and returns photo with id "1" and then it causes the gallery query to fire, repeat indefinitely.

Am I doing something wrong? Why do the queries fire again when the underlying data changes, shouldn't it just update in cache?

Version 1.4.5 / 2.0.0 I did not see this issue in 1.2.0

giantramen avatar Apr 28 '20 15:04 giantramen

Ah, I have to set refetchResponseFetcher on the watcher to CACHE_ONLY. Shouldn't that be the default?

giantramen avatar Apr 28 '20 15:04 giantramen

strange, as default fetcher for watchers is ApolloResponseFetchers.CACHE_FIRST so it should hit cache first and skip network if there is data in cache.

sav007 avatar May 01 '20 00:05 sav007

Is this issue still valid? @giantramen can you share your queries and how you are executing them (coroutines, rxjava, callback, etc...)? Also, do you use a CustomKeyResolver?

martinbonnin avatar Aug 25 '20 11:08 martinbonnin

this happens to be too, but my use case is more complicated and so i cannot provide queries. I use coroutines this is my keyresolver


class DefaultCacheKeyResolver : CacheKeyResolver() {

    override fun fromFieldArguments(field: ResponseField, variables: Operation.Variables): CacheKey {
        return formatCacheKey(field.resolveArgument("id", variables) as? String)
    }

    override fun fromFieldRecordSet(field: ResponseField, recordSet: Map<String, Any>): CacheKey {
        return formatCacheKey(recordSet["id"] as? String)
    }

    private fun formatCacheKey(id: String?): CacheKey {
        return if (id == null || id.isEmpty()) {
            CacheKey.NO_KEY
        } else {
            CacheKey.from(id)
        }
    }

}


nicusorflorin avatar Aug 26 '20 12:08 nicusorflorin

@nicusorflorin if you get a chance, it would be interesting to investigate why there's a cache miss. The process is not straightfoward at the moment and we should definitely make it easier (see https://github.com/apollographql/apollo-android/issues/2397). But you should be able to get some information about the missing key (if any) by adding breakpoints to CacheFieldValueResolver: https://github.com/apollographql/apollo-android/blob/905620cd98dfb5cbb07b5a540558c1cc761c45e1/apollo-normalized-cache/src/commonMain/kotlin/com/apollographql/apollo/cache/normalized/internal/CacheFieldValueResolver.kt#L42

martinbonnin avatar Aug 26 '20 12:08 martinbonnin

@martinbonnin i'm getting Missing value: outer for fieldKey: outer({"first":150})

I have a query that gets the ids for several lists, then i query each list for it's elements. sometimes it seems to loop itself. if i check the logs only the main query keeps calling the api again and again. note that this doesn't happen always

nicusorflorin avatar Aug 26 '20 14:08 nicusorflorin

Thanks. It might be the pagination parameters that trigger the cache miss. I'll try to reproduce.

martinbonnin avatar Aug 26 '20 14:08 martinbonnin

There aren't any except the "first" tried with a page size of "2" and it happens even more often

nicusorflorin avatar Aug 26 '20 14:08 nicusorflorin

There aren't any except the "first" tried with a page size of "2" and it happens even more often

Not sure I get it. There aren't any cache miss? Or are pagination parameters always the same?

martinbonnin avatar Aug 26 '20 15:08 martinbonnin

for the main query the one that gets looped they are always the same. only a page size is specified via the "first" parameter. regarding cache misses, i do keep getting that "outer" error when it loops

nicusorflorin avatar Aug 27 '20 06:08 nicusorflorin

I'm encountering a similar infinite refresh loop on the watcher when a query returns two objects of the same type with the same id. ex:

query FetchBuildings() {
   user { 
        id 
        primaryBuilding {
              id
              name
        }
        buildings {
             id
        }
   }
} 
apolloClient.query(FetchBuildingsQuery())
                .toBuilder().responseFetcher(ApolloResponseFetchers.CACHE_ONLY).build()
                .watcher().toFlow().collect { response ->
                        Timber.d("Response is valid ${response.data?.user?.buildings?.size}")
                 }

I'm using the ID cache key resolver since all objects are fetched with a UUID. When I comment out primaryBuilding the query and watchers work as normal. Even when I set it to CACHE_ONLY it fetches network as per the profiler. The returned response every time the watcher is called (5-6 times a second) is correct.

Been stuck on this issue for a while, would love any help!

Nealsoni00 avatar Sep 03 '20 12:09 Nealsoni00

@Nealsoni00 It seems weird that CACHE_ONLY watcher would go to the network, I wouldn't expect that to happen. How do you trigger the cache update? Do you have another query that goes to the network? Or do you write the cache directly?

I tried putting together a reproducer there: https://github.com/martinbonnin/apollo-android-samples/blob/aabd437becac27d251f1691ea257bd47b70d3b9d/query-loop/src/test/kotlin/MainTest.kt#L57

Any chance you can see what's different from your use case? That'll help a lot.

martinbonnin avatar Sep 03 '20 13:09 martinbonnin

Hi Martin, The one difference I see is that I have a different query run on the initialization of the application (before the main page appears). That query has the same data as the FetchBuildingsQuery and a lot more (initializing user app state, etc).

Nealsoni00 avatar Sep 03 '20 13:09 Nealsoni00

How many watchers do you have ultimately? I feel like this issue should only happen with two watchers? And the initialization query you're doing doesn't seem like a good candidate to be a watcher. Or is it?

martinbonnin avatar Sep 03 '20 13:09 martinbonnin

I commented out all other watchers in the entire application. That is the only one that is left. The initialization query is not itself being watched. The watcher is so that if any other views call the GetBuildingsQuery that interface is updated.

Nealsoni00 avatar Sep 03 '20 13:09 Nealsoni00

I see, it's the same query being watched from different places. Well that's unexpected given that's exactly what my reproducer does. Maybe it's a timing issue somewhere, having a reproducer somewhere would be super useful.

martinbonnin avatar Sep 03 '20 13:09 martinbonnin

@Nealsoni00 I know this thread is old but did you resolve your issue? I'm encountering the same problem where a CACHE_ONLY watcher seemingly triggers a network request and I can't figure out why.

roum-old1 avatar Jul 06 '22 21:07 roum-old1

@Nealsoni00 I know this thread is old but did you resolve your issue? I'm encountering the same problem where a CACHE_ONLY watcher seemingly triggers a network request and I can't figure out why.

Sorry have since moved off native android. I vaguely remember it being solved by ensuring the retuned data only returned an object with ID XXXXX once. (No fetching same object more than once).

Nealsoni00 avatar Jul 06 '22 21:07 Nealsoni00

Hello everyone 👋 This issue is quite the oldie and I'm not sure how to move forward without a reproducer. I'm going to close it for now but if anyone wants to deep dive in this again, please feel free to leave a comment and I'll reopen.

martinbonnin avatar Mar 21 '24 09:03 martinbonnin

Do you have any feedback for the maintainers? Please tell us by taking a one-minute survey. Your responses will help us understand Apollo Kotlin usage and allow us to serve you better.

github-actions[bot] avatar Mar 21 '24 09:03 github-actions[bot]