FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

⚡️ Skip doing any work when sub dependant is already cached by xkabylgSICKAG · Pull Request #11323 · fastapi/fastapi · GitHub

⚡️ Skip doing any work when sub dependant is already cached - #11323

Open
xkabylgSICKAG wants to merge 3 commits into
fastapi:masterfrom
xkabylgSICKAG:master
Open

⚡️ Skip doing any work when sub dependant is already cached#11323
xkabylgSICKAG wants to merge 3 commits into
fastapi:masterfrom
xkabylgSICKAG:master

Conversation

xkabylgSICKAG commented Mar 20, 2024
edited
Loading

Copy link
Copy Markdown

Hi, we noticed very large performance gains after making this change for our app that heavily uses the dependency injection system. One of our slower endpoints went from taking ~1100ms to ~400ms.

xkabylgSICKAG force-pushed the master branch 2 times, most recently from 8bb7d6d to 69fca88 Compare March 20, 2024 17:57

YuriiMotov left a comment
edited
Loading

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

It looks really reasonable to check whether the sub_dependency is already solved and cached before solving it!

Related PR, that uses almost the same approach: #4089 (closed)

Comment thread fastapi/dependencies/utils.py Outdated

Copy link
Copy Markdown
Author

@YuriiMotov

Related PR, that uses almost the same approach: #4089

That PR also has a test! :)

I think that other PR, with your suggested changes, is a better change than this PR. Do you think it makes more sense to close this in favour of the other one?

xkabylgSICKAG force-pushed the master branch 3 times, most recently from c01036d to 1697c05 Compare March 21, 2024 13:03

Copy link
Copy Markdown
Author

I've rebased and updated the PR with the suggested changes from @YuriiMotov and with the change to dependency_cache empty dict checking from #4089.

svlandeg changed the title perf(dependencies): skip doing any work when sub dependant is already cached ⚡️ Skip doing any work when sub dependant is already cached Feb 18, 2025
svlandeg added feature New feature or request and removed enhancement labels Feb 18, 2025

This comment was marked as resolved.

Copy link
Copy Markdown
Author

@svlandeg Sure! I can spend some time on that later tonight. I would be very happy to get this merged so we can stop running a forked version of fastapi 🙂

xkabylgSICKAG force-pushed the master branch 2 times, most recently from 9fdb058 to 0821c80 Compare February 19, 2025 21:55

xkabylgSICKAG commented Feb 19, 2025
edited
Loading

Copy link
Copy Markdown
Author

@svlandeg I updated the code to add that test from #4089. I would have liked to create a similar test that didn't use include pydantic, but I wasn't able to figure out how to do that and still test the same thing.

I verified that the test fails before this and passes after this.

svlandeg self-assigned this Feb 20, 2025

This comment was marked as resolved.

Comment thread fastapi/dependencies/utils.py Outdated
Comment on lines +600 to +607
# Use a unique object to compare against in case the cached value is None
cache_miss = object()
cached_value = dependency_cache.get(sub_dependant.cache_key, cache_miss)
# If the sub dependant is already cached, skip doing any more work
if cached_value is not cache_miss:
if sub_dependant.name is not None:
values[sub_dependant.name] = cached_value
continue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Are there any reasons not to simplify it this way?

Suggested change
# Use a unique object to compare against in case the cached value is None
cache_miss = object()
cached_value = dependency_cache.get(sub_dependant.cache_key, cache_miss)
# If the sub dependant is already cached, skip doing any more work
if cached_value is not cache_miss:
if sub_dependant.name is not None:
values[sub_dependant.name] = cached_value
continue
if sub_dependant.cache_key in dependency_cache:
cached_value = dependency_cache[sub_dependant.cache_key]
if sub_dependant.name is not None:
values[sub_dependant.name] = cached_value
continue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

dict.get(...) is atomic operation, and thus thread safe, while consequent key in dict; dict[key] is not

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Good point!
But currently dependency solving process is not parallel.
Also, it's highly unlikely (useless?) that somebody decide to delete existing key from dependency_cache.

I would vote for clarity in this case if nobody brings other arguments

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

We have been running this fork in our production application for quite a while now, and before we changed to an atomic .get() we got hard to reproduce KeyErrors.

svlandeg removed their assignment Jun 23, 2025

YuriiMotov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

LGTM

I still believe we can safely simplify this part, but it's not critical

xkabylgSICKAG commented Aug 4, 2025
edited
Loading

Copy link
Copy Markdown
Author

I still believe we can safely simplify this part, but it's not critical

I would have thought it could be simplified too, but we did get KeyErrors before introducing that change to an atomic operation. It only happened a couple of times a week in total, but that's still too many times. 🤷‍♀️

It might be that we are doing something unusually weird with the dependency injection system in our app, but I couldn't find a clean way to fix the issue without this change in FastAPI.

Any idea if this has a chance to get merged soon? I would love to not have an internal fork of FastAPI anymore 😄

github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Nov 3, 2025

This comment was marked as resolved.

github-actions Bot removed the conflicts Automatically generated when a PR has a merge conflict label Nov 3, 2025

Copy link
Copy Markdown
Author

Rebased to resolve the conflicts

codspeed-hq Bot commented Jan 8, 2026
edited
Loading

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing xkabylgSICKAG:master (e2775a9) with master (8e89896)1

Footnotes

  1. No successful run was found on master (c3f316b) during the generation of this report, so 8e89896 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Copy Markdown

Looking forward for this fix since this improves the performance of our apps in production

This comment was marked as off-topic.

github-actions Bot added the conflicts Automatically generated when a PR has a merge conflict label Jul 24, 2026

This comment was marked as outdated.

github-actions Bot removed the conflicts Automatically generated when a PR has a merge conflict label Jul 27, 2026
Comment thread fastapi/dependencies/utils.py Outdated
solved = await run_in_threadpool(call, **solved_result.values)
if sub_dependant.name is not None:
values[sub_dependant.name] = solved
if sub_dependant_cache_key not in dependency_cache:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality
Suggested change
if sub_dependant.use_cache:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

Maybe makes sense (not sure actually, we need to think if it can break some specific use cases), but I think this is out of scope for this PR

dolfinus Jul 28, 2026
edited
Loading

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

This PR moves the check for key in dependency_cache into block above with continue, so check in this particular line will always be true.
Also there is no need to update dependency cache if dependency does not require caching in the first place.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Choose a reason Spam Abuse Off Topic Outdated Duplicate Resolved Low Quality

If there is another call of the same dependency that would use the cache, then it wouldn't find the cached value from this call? (I'm not sure if that is what I would expect to happen, but it is the current behavior.)

Imagine the following:

async def needy_dependency(
    fresh_value: Annotated[str, Depends(get_value, use_cache=False)],
    cached_value: Annotated[str, Depends(get_value, use_cache=True)],
):

Previously fresh_value would be resolved and then added to the cache, and cached_value would be grabbed from the cache. With the suggested change it would not add the result from fresh_value to the cache and have to resolve cached_value.

Copy link
Copy Markdown
Author

Rebased on latest upstream and moved the cache_miss sentinel value outside the function as suggested by @dolfinus

This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants


Back | FazBrowse Home | New Git URL