Skip to content

Conversation

@alexander-alderman-webb
Copy link
Contributor

@alexander-alderman-webb alexander-alderman-webb commented Oct 24, 2025

Description

Patch django.core.cache.get() separately from django.core.cache.get_many() to determine cache hits and misses independently for both methods.

When calling get_many() only register a cache miss when the returned value is an empty dictionary.

When calling get(), register a cache miss when

  • the return value is None and no default value is provided; or
  • the return value is equal to a provided default value.

Issues

Closes #5027

Reminders

@alexander-alderman-webb alexander-alderman-webb changed the title fix(django): Improve logic for classifying cache hits or misses fix(django): Improve logic for classifying cache hits and misses Oct 24, 2025
@codecov
Copy link

codecov bot commented Oct 24, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.93%. Comparing base (104db8c) to head (626b529).
⚠️ Report is 7 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #5029      +/-   ##
==========================================
- Coverage   83.94%   83.93%   -0.01%     
==========================================
  Files         178      178              
  Lines       17834    17850      +16     
  Branches     3170     3175       +5     
==========================================
+ Hits        14971    14983      +12     
- Misses       1901     1905       +4     
  Partials      962      962              
Files with missing lines Coverage Δ
sentry_sdk/integrations/django/caching.py 97.16% <100.00%> (+0.32%) ⬆️

... and 8 files with indirect coverage changes

Comment on lines -226 to +227
assert not second_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in second_event["spans"][0]["data"]
assert second_event["spans"][0]["data"]["cache.hit"]
assert second_event["spans"][0]["data"]["cache.item_size"] == 2
Copy link
Contributor Author

@alexander-alderman-webb alexander-alderman-webb Oct 28, 2025

Choose a reason for hiding this comment

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

Previously marked as a cache miss because [] is falsy.

I do not understand the intent behind asserting a cache miss here, because we assert information about the corresponding cache.put further up in the test.

# first_event - cache.put
assert first_event["spans"][1]["op"] == "cache.put"
assert first_event["spans"][1]["description"].startswith(
"views.decorators.cache.cache_header."
)
assert first_event["spans"][1]["data"]["network.peer.address"] is not None
assert first_event["spans"][1]["data"]["cache.key"][0].startswith(
"views.decorators.cache.cache_header."
)
assert "cache.hit" not in first_event["spans"][1]["data"]
assert first_event["spans"][1]["data"]["cache.item_size"] == 2

Copy link
Contributor

Choose a reason for hiding this comment

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

This test confuses me too. It calls the not_cached_view view, which, unlike the cached_view, doesn't have the @cache_page decorator. Is there some automatic caching going on anyway even without the decorator? Is some caching always happening because of our use of the use_django_caching_with_middlewares fixture in this test? It definitely seems like it since we're getting cache spans.

The cache miss asserts look wrong to me too. It should be a cache hit. But I don't understand why cache.item_size is now different?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the caching only occurs because of the use_django_caching_with_middlewares() fixture here. Without the fixture no spans are generated.

The item's size is 2 because len(str([])) == 2, while previously we did not store the item size at all because we were in the else branch below due to [] being falsy:

if value:
item_size = len(str(value))
span.set_data(SPANDATA.CACHE_HIT, True)
else:
span.set_data(SPANDATA.CACHE_HIT, False)

The empty list value originates from

https://github.com/django/django/blob/9ba3f74a46d15f9f2f45ad4ef8cdd245a888e58e/django/utils/cache.py#L437

Comment on lines -504 to +505
assert not second_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in second_event["spans"][0]["data"]
assert second_event["spans"][0]["data"]["cache.hit"]
assert second_event["spans"][0]["data"]["cache.item_size"] == 2
Copy link
Contributor Author

@alexander-alderman-webb alexander-alderman-webb Oct 28, 2025

Choose a reason for hiding this comment

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

Previously marked as a cache miss because [] is falsy.

Similar reasoning to the other assertion I changed.

@alexander-alderman-webb alexander-alderman-webb marked this pull request as ready for review October 28, 2025 12:27
@alexander-alderman-webb alexander-alderman-webb requested a review from a team as a code owner October 28, 2025 12:27
cursor[bot]

This comment was marked as outdated.

@alexander-alderman-webb alexander-alderman-webb marked this pull request as draft October 28, 2025 12:30
@alexander-alderman-webb alexander-alderman-webb marked this pull request as ready for review October 28, 2025 13:28
cursor[bot]

This comment was marked as outdated.

cursor[bot]

This comment was marked as outdated.

_patch_cache_method(cache, "set", address, port)
_patch_cache_method(cache, "set_many", address, port)
# Separate patch to account for custom default values on cache misses.
_patch_get_cache(cache, address, port)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why make a separate function just for this case? As far as I can tell the logic is still mostly the same as in _patch_cache_method. Would it maybe make more sense to instead modify _patch_cache_method directly to handle get too, just with some extra logic around what the default value should be?

The problem with duplicating logic like this is that the shared parts can easily go out of sync unintentionally (someone edits one of the funcs, misses the other).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes agreed, and the diff is much smaller as well now!

Comment on lines -226 to +227
assert not second_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in second_event["spans"][0]["data"]
assert second_event["spans"][0]["data"]["cache.hit"]
assert second_event["spans"][0]["data"]["cache.item_size"] == 2
Copy link
Contributor

Choose a reason for hiding this comment

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

This test confuses me too. It calls the not_cached_view view, which, unlike the cached_view, doesn't have the @cache_page decorator. Is there some automatic caching going on anyway even without the decorator? Is some caching always happening because of our use of the use_django_caching_with_middlewares fixture in this test? It definitely seems like it since we're getting cache spans.

The cache miss asserts look wrong to me too. It should be a cache hit. But I don't understand why cache.item_size is now different?

Copy link
Contributor Author

@alexander-alderman-webb alexander-alderman-webb left a comment

Choose a reason for hiding this comment

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

I tried out your suggestion and I agree that not making a separate function for each method is better here so we don't duplicate logic.

Comment on lines -226 to +227
assert not second_event["spans"][0]["data"]["cache.hit"]
assert "cache.item_size" not in second_event["spans"][0]["data"]
assert second_event["spans"][0]["data"]["cache.hit"]
assert second_event["spans"][0]["data"]["cache.item_size"] == 2
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the caching only occurs because of the use_django_caching_with_middlewares() fixture here. Without the fixture no spans are generated.

The item's size is 2 because len(str([])) == 2, while previously we did not store the item size at all because we were in the else branch below due to [] being falsy:

if value:
item_size = len(str(value))
span.set_data(SPANDATA.CACHE_HIT, True)
else:
span.set_data(SPANDATA.CACHE_HIT, False)

The empty list value originates from

https://github.com/django/django/blob/9ba3f74a46d15f9f2f45ad4ef8cdd245a888e58e/django/utils/cache.py#L437

@alexander-alderman-webb alexander-alderman-webb merged commit d7ccf06 into master Oct 29, 2025
124 checks passed
@alexander-alderman-webb alexander-alderman-webb deleted the webb/django-caching branch October 29, 2025 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Exception when the Django cache integration checks for cache hit

3 participants