-
-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cache results of common database queries #349
Cache results of common database queries #349
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even aside from the performance improvements, it will be great to have so much duplicated code factored out into properly shared functions!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To alpha it goes!
Works fine, until someone triggers a 404 response:
Looks like SqlAlchemy automatically expires all ORM objects when an exception is raised (because we don't for 404 errors, only for real 500s), maybe through a roolback or something. Preferably we'd find a way to completely decouple these objects from SqlAlchemy/the ORM//the database/the session; we don't need any of those features after they are cached. I kind of hoped Some useful links:
It sounds like Edit: Having trouble reproducing this bug locally. Something behaves differently. |
Okay, I managed to somewhat reproduce it by creating a new session inside However, I fear that this is also affection mods returned in I'm gonna look into it a bit further, but I might end up removing the cache decorators (but keep the refactoring) until we have figured out the correct way to do this (if it's even possible). |
Sounds good to me. We should keep alpha in a working state as much as possible so we can deploy smoothly whenever our site owner finds it convenient. |
Motivation
Thanks to #346 we now have very valuable profiling data for our backend. After fixing the previous bottleneck, repeated template bytecode compilation due to disabled caching, the major part of most requests is now spent on database queries.
The answer: moar caching!
This PR does an initial step at caching the most common database queries (some of them executed on every request), that tend to not change at all or only very infrequently, or don't hurt if they are a bit out of date.
It's the low hanging fruit, there's still a lot more we should eventually cache, but those need some refactoring and other bigger changes, or careful planning. I preferred to get the easy stuff in first.
Changes
We are caching all of the following requests in a
TTLCache
, that is s LRU cache with timeouts, using thecachetools
library:It allows caching the return values of a function by simply slapping a decorator on it, which makes it pretty easy to use.
Cached lookups:
inject()
(executed for every request) cached for 30 minutes. If we create a new announcement or delete/edit an existing one, which happens very rarely, it doesn't hurt if it takes some time to be visible to all users.Caveat: it can happen that the workers get "out of sync" for up to 30 minutes, and the banner might change with every request depending on which worker your request goes to.
/browse
,/browse/*.rss
,/<gameshort>/browse
,/<gameshort>
,/<gameshort>/browse/*.rss
cached for 30 minutes. They are factored out intoget_*_mods()
functions incommon.py
, one for each of those categories (featured/new/updated/top). What changes most there are the new and updated mods, but they should be fine being 30 minutes behind as well./browse/all
,/browse/top
,/<gameshort>/browse/all
,/<gameshort>/browse/top
,/api/browse/top
, as well as/search
and/<gameshort>/search
throughsearch_mods()
cached for 10 minutes. I decided to give them a lower timeout, since mod searches also go through this function, and we don't really need to cache them. Maybe we should split the functions called by*/top
and*/search
at some point (and merge it withget_top_mods()
).search_mods()
now takes aGame.id
instead of the wholeGame
object, since that's all it needed anyway, and theGame
objects change every time they're looked up from the database (i.e. with every request, they're never the same), which bypassed the cache./mod/<mod_id>
,/mod/<mod_id>/<mod_name>/stats/(downloads|referrals|followers)
cached for 30 minutes.There could be some issues due slightly mismatching 30 day time frames of the downloads/follows and
thirty_days_ago
which is then used by the graph workers. I might do some database manipulation to test it. But most likely we are fine there.The cache times are a blind guess at what could be acceptable as lag behind current data and effectiveness of the cache.
The derivation of the cache sizes is documented in the code. I basically did a generous calculation of the amount of possible function argument combinations (since each combination gets its own key in the cache), then added or removed a bit depending on the situation.
Every cache size gets multiplied by 48 for the amoutnt of worker (and the actual size in bytes of the returned object), which needs to be considered.
I'm open for suggestions for better values of these two fields.
Additional changes
get_mods()
toget_paginated_mods()
andpaginate_mods()
topaginate_query()
to help distinguishing them from each other (andsearch_mods()
). Alsopaginate_query()
could be used for other objects as well theoretically, not only mods.Some numbers
/<gameshort>
/browse
) were so bad is that we sorted byMod.download_count
, which is not indexed./<gameshort>/browse
/<gameshort>/browse/top
/mod/<mod_id>
/