Apollo-android: Query watcher with SQLNormalizedCache not triggering updates

Created on 24 Aug 2020  路  10Comments  路  Source: apollographql/apollo-android

Summary
The query watcher is not triggering any updates (institutesGraphQL) when I issue a query fetch (updateInstitutes). However if I restart my activity after a successful updateInstitutes the watcher will initially emit the previously fetched query, but again wont notify future changes for the remaining lifecycle of the activity. Also, shouldn't I be able to see the contents of the cache with the dump inside updateInstitutes?! Thanks in advance.

Version
Tested with 2.2.0 and 2.3.0

Description
I have the following in my viewmodel class

     private val institutesGraphQL =
        apolloClient.query(GetSupportedInstitutesQuery())
            .responseFetcher(ApolloResponseFetchers.CACHE_ONLY)
            .watcher()
            .toFlow().asLiveData()

     val institutesEntries = institutesGraphQL.switchMap {
        liveData {
            val tmpInstitutesEntries = ArrayList<String>()
            it.data?.let { data ->
                data.institutes!!.forEach { instGraphQl ->
                    tmpInstitutesEntries.add(instGraphQl.name)
                }
            }
            emit(tmpInstitutesEntries)
        }
    }

    fun updateInstitutes() {
        launchDataLoad {
            val response = apolloClient.query(GetSupportedInstitutesQuery())
                .responseFetcher(ApolloResponseFetchers.NETWORK_ONLY) // tested also with CACHE_AND_NETWORK and NETWORK_FIRST
                .toDeferred()
                .await()

//          Interestingly the following dump reports these in my console "OptimisticNormalizedCache {}, SqlNormalizedCache {}"
//          val dump = apolloClient.apolloStore.normalizedCache().dump()
//          println(NormalizedCache.prettifyDump(dump))

            println(response)
        }
    }

    private fun launchDataLoad(block: suspend () -> Unit): Job {
        return viewModelScope.launch {
            try {
                block()
            } catch (t: Throwable) {
                println(t)
            } finally {

            }
        }
    }

and tested with the following key resolver


      val resolver: CacheKeyResolver = object : CacheKeyResolver() {
        override fun fromFieldRecordSet(
            field: ResponseField,
            recordSet: Map<String, Any>
        ): CacheKey {
            // Retrieve the id from the object itself
            when (val type = recordSet["__typename"].toString()) {
                "Institute" -> {
                    return CacheKey.NO_KEY
                }
//             Tested also with the following
//              "Institute" -> {
//                 return CacheKey.from(type + "_" + recordSet["id"].toString())
//              }
            }
            throw Exception()
        }

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

All 10 comments

Hi!

//          Interestingly the following dump reports these in my console "OptimisticNormalizedCache {}, SqlNormalizedCache {}"
//          val dump = apolloClient.apolloStore.normalizedCache().dump()
//          println(NormalizedCache.prettifyDump(dump))

The dump operation is not implemented on the SQL normalized cache (which is a bummer, I'll look into that). For debugging, can you try swapping your SQL cache with a LruNormalizedCache ?

Hi!

//          Interestingly the following dump reports these in my console "OptimisticNormalizedCache {}, SqlNormalizedCache {}"
//          val dump = apolloClient.apolloStore.normalizedCache().dump()
//          println(NormalizedCache.prettifyDump(dump))

The dump operation is not implemented on the SQL normalized cache (which is a bummer, I'll look into that). For debugging, can you try swapping your SQL cache with a LruNormalizedCache ?

Yeap with LruNormalizedCache everything works as expected (the watch gets triggered) 馃檪 Anything more I could do, to help in the debugging process?

PS: and the dump, actually dumps things now :smile:

Interesting, that's definitely unexpected. Both caches should behave the same. Can you extract the SQL file manually and see if there's anything in there with a tool such as https://sqlitebrowser.org/? Or I think Android Studio canary has a database viewer too now.

Ok I reverted back to the SQLNormalizedCache. Once again my fetch completed successfully but the watcher didn't trigger any changes. I took a look and it seems that indeed the records are written inside the sqlite3 db file. As I mentioned in my original post if I restart my activity now that I have populated the db with one successful fetch the watcher will initially give me the appropriate results but it won't trigger in any future updates

sql_with_screens.zip

Thanks for the dumps. Maybe try without the LiveData? But I don't see a reason this should behave differently with SQL vs Lru, it looks like a bug. I'll try to reproduce that later today.

Thanks for the assistance @martinbonnin . Although livedata is kinda necessary for my flow (binding to the UI) I already tested the watcher without it, just to see if I experience the same behavior, and it was the same.

I can reproduce the issue, fix is there: https://github.com/apollographql/apollo-android/pull/2538.
To workaround the issue, you can chain a memory cache after the SQL one. That's the opposite of what you would normally do but that'll trigger the update:

            val cache = SqlNormalizedCacheFactory("jdbc:sqlite:")
            cache.chain(LruNormalizedCacheFactory(EvictionPolicy.NO_EVICTION /* or any other more strict eviction policy */))

Thanks for the ninja reflexes @martinbonnin . Indeed the workaround seems to do the trick with SqlNormalizedCacheFactory and query watcher. Out of curiosity, to check if my understanding is correct, since I chain the SqlNormalizedCacheFactory with the LruNormalizedCacheFactory (with that order), I will never fill the LruNormalizedCacheFactory because the SqlNormalizedCacheFactory will never evict any data to it, since it will save all the data itself?!

I will never fill the LruNormalizedCacheFactory because the SqlNormalizedCacheFactory will never evict any data to it, since it will save all the data itself?!

Not really, it's more "writes go to both caches, reads will only read from SQL", which is not really optimal. It will end up duplicating some data and using some RAM so it's really a workaround if you need a quick and dirty solution.

Hmm ok I guess for prototyping and developing the workaround can be useful, but for production this won't cut it, and I will wait for 2.3.1 that I hope it will include this fix 馃檪 Anyway thanks for looking into that.

Was this page helpful?
0 / 5 - 0 ratings