Store
Store copied to clipboard
[Unintuitive behavior] Store.get returns can return stale cached data after sourceOfTruth update
Maybe more of a question than a bug. This is related to #78, I believe. When calling Store.get() with no active streams, it will return the memory cached value instead of the sourceOfTruth value. This was initially quite confusing to me, though seems obvious now that I understand it. Is this the intended behavior?
To Reproduce Here is a test case to reproduce the behavior:
@Test
fun `get returns source of truth`() {
runBlocking {
val ch = ConflatedBroadcastChannel<Int?>(null)
val store = StoreBuilder.from<Unit, Int, Int>(
fetcher = nonFlowValueFetcher { 0 },
sourceOfTruth = SourceOfTruth.from(
reader = { ch.asFlow() },
writer = { _, value -> ch.offer(value) }
)
).build()
assert(store.get(Unit) == 0) // prime the memory cache
ch.send(1)
delay(500) // wait for flow to update
assert(store.get(Unit) == 1)
}
}
Expected behavior It would be nice if Store could track if there were active streams and default to fetching from the sourceOfTruth if non were active. As it stands, perhaps just updating the documentation to make clear what will happen in this scenario would work.
Interesting "bug". We accept a flowing source of truth but don't update the cache live unless there's an active collector. Intended behavior but I wonder if we can/should do better?
Or maybe we should just remove get from the API. I wonder if it's more confusing than helpful
I'm starting to agree that get should never return a blank value. Store3 worked in a manner where callers don't need to know state of store. If I have 2 screens both call store.get() I would expect one to race and hit network, other to get cache value. Any thoughts on how we can get a similar behavior here?
I don't quite follow. when would we return a blank value? Did you mean to comment on #175?
The issue here is that we don't read from a SoT into the cache unless there's an active downsteam, so a new downsteam will see a cached value even if the SoT has updated a long time ago.
Some solutions here:
- update API to let user specify max staleness from the cache/SoT (and force
getto take this param) - disallow having both SoT and cache (we've discussed this in the past)
- Change the default TTL of the cache to be much much shorter when there's a SoT (in the seconds)
This is starting to look like a work as intended. There is a longer memory cache, the cache was not expired. The cache value was returned. We're going to have same issue with fetcher/SOT where SOT is not updated when no subscribers. I see a few configurations if this is not intended behavior:
- disable the memory cache
- set a short TTL
- perform a fresh call if you care to have freshest data.
Open to other suggestions. I don't think we should be changing something that can solved through configurations
Yes, disabling the cache is exactly how I fixed this issue when I encountered it. I think part of my confusion is that I tend to think of the database as a local cache of the remote server data (at least for the apps I was working on). Hence, all the documentation on the cache made me immediately think of the SoT.
So, really this is just user error on my part. My only suggestions would be:
- A per call cache configuration would have been nice in this instance, as I wanted the caching behavior in general, but in the specific code where this happened, I was not streaming results and was only use the get() API
- The docs maybe should be more explicit about when you should expect stale results. This paragraph:
Providing sourceOfTruth whose reader function can return a Flow<Value?> allows you to make Store treat your disk as source of truth. Any changes made on disk, even if it is not made by Store, will update the active Store streams.along with the name SourceOfTruth confused me into thinkingget()would always return the database value. Of course, a careful reading shows thats incorrect (it definitely says streams), but it might help to be more explicit for people like me who just skim the docs ;-)
I've arrived on this page after encountering this unintuitive behaviour myself. If I create a store with a SingleSourceOfTruth, and I then update the source of truth - the value I get from store should match that new object, surely?! I don't find the work-around suggestions very helpful. Disabling the memory cache or lowering the TTL will make every access to the data slower than it needs to be (wanting to avoid this is why we're using Store in the first place), and performing a fresh call engages the fetcher (e.g. an http call in my case) whereas all I want is to get the latest value from the SSoT. To me this default behaviour should change, but if that's not desired or possible then at least some option to manually refresh the store from the SSoT would be enough to make this problem go away for me, but I can't figure out a way to do it. Any advice?
hello @elroid I somehow missed your response until now, apologies. You are correct in thinking that changing the default behavior will confuse more users than help. We do have a configuration on StoreRequest to skip the memory cache for circumstances such as yours. Does the following api work for you?
StoreRequest.skipMemory(refresh=false)
This request will always skip memory, try to load from the source of truth and fallback to network if source of truth is empty.
The concern with changing the default behavior is that we would need to keep an endless flow from the disk to the memory cache even when no subscribers. This in turn would make testing more difficult as tests will more often give the "job has not completed" coroutine exception. This will be due to the memory cache not knowing when to unsubscribe. Let me know if above works for you
Hi @digitalbuddha thanks for getting back to me. What you suggest (skipping the memory version and going straight to the SoT) is as close as appears possible currently, but is not ideal - as I mentioned in my previous post. I don't want to bypass the in-memory value, i just want to make sure that it is kept up to date. Or are you suggesting that making a request with StoreRequest.skipMemory(refresh=false) would retrieve the value from the SoT AND update the in-memory value? Because it's the second part that i really need. For my use case I just ended up going directly to the SoT and bypassing store which feels like a hack rather than a solution.
Correct skip memory would not return the in memory value but would still update the memory cache after getting the source of truth value