django-cacheops: cache wrongly set to empty list for get query (when accessing a foreignkey field)

I am observing a random get(..) query returning DoesNotExist for some objects (old objects that always existed in the DB). This is happening when I try to access a field that is a ForeignKey to a model (Vehicle in code below).

On digging deep, I found that Django essentially runs a filter query on the parent, then a count() on it to do a get when I try to access the foreign key field.

So I tried to simply call the filter query from shell and found a surprising result (where 409 is the ID of the object on which I am facing this issue):

In [752]: Vehicle.objects.filter(id=409)
Out[752]: <QuerySet []>

In [753]: Vehicle.objects.filter(id=409).nocache()
Out[753]: <QuerySet [<Vehicle: Vehicle object (409)>]>

On further analysis, I found that Django sets upper limit of 21 to filter query in both get and __repr__ (This is why both the field access and the above filter query are returning same empty list). The query cached by cacheops is thus : Vehicle.objects.filter(id=409)[:21] . Indeed, if I check the cache value for this query, I see:

In [748]: qs1c = Vehicle.objects.filter(id=409)
In [749]: qs1c.query.set_limits(high=21)
In [750]: rr.get(qs1c._cache_key())
Out[750]: b'\x80\x04]\x94.'

(The output value is an empty set)

So my problem is: why and when is the cache set to []?

I found that the cache is set in _fetch_all() function in cacheops/query

self._result_cache = list(self._iterable_class(self))
self._cache_results(cache_key, self._result_cache)

The only possibility seems to be list(self._iterable_class(self)) returning empty. How can that ever happen though?

I still don’t know the condition under which this happens - it’s random, but it’s only happening for that one foreign field access. I haven’t observed any other errors with any other model so far.

Can anyone guide how can I further pin-point when this is happening?

My setup: django-cacheops 6.2, Django 3.2, Python 3.11

About this issue

  • Original URL
  • State: closed
  • Created 3 months ago
  • Comments: 15 (8 by maintainers)

Most upvoted comments

You are welcome. Glad this is resolved.

Thanks for the pointers @Suor , here’s my thoughts on them:

  1. Clash by error - say you make the same query against different database all queries are on single DB
  2. A situation where you receive wrong response to database query occasionally ok, this is a possibility, however, outside of this model we have not seen any issue anywhere so far. Also, I have always seen exceptions raised whenever there’s an issue with a pool connection. Django ORM never returns wrong results - at least that is my experience using it for 10+ years now. I have never encountered unreliability. Pools and connections are managed by Django ORM, we are not doing anything special or custom at all.
  3. A database exception slurped on the middle, causing empty response, or database cursor read before Django had a chance at it, Django getting 0 rows. Any pointers on this? AFAIK database exception would be re-thrown and never return an empty response. I will search for Django returning 0 rows, but like I said above, I have never ever experienced or heard about Django ORM giving wrong results. I am keen to debug this option though.
  4. slave lagged response we are not using any slave. Also, these are old rows are in the DB, so they were 100% there when the queries updated the cache.

To further debug the issue: I am thinking of including the cacheops code in our codebase & logging a warning whenever empty query is response is received. This way, we will know when [] is being written to the cache.

Do you think this makes sense? Any further thoughts? (Really really appreciate your time!). Thanks!