mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
feat: cache package search results with Redis
The queries being done on the package search page are quite costly. (Especially the default one ordered by "Popularity" when navigating to /packages) Let's add the search results to the Redis cache: Every result of a search query is being pushed to Redis until we hit our maximum of 50k. An entry expires after 3 minutes before it's evicted from the cache. Lifetime an Max values are configurable. Signed-off-by: moson-mo <mo-son@mailbox.org>
This commit is contained in:
parent
7c8b9ba6bc
commit
3acfb08a0f
8 changed files with 173 additions and 74 deletions
|
@ -1,6 +1,8 @@
|
|||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from aurweb import cache, db
|
||||
from aurweb import cache, config, db
|
||||
from aurweb.models.account_type import USER_ID
|
||||
from aurweb.models.user import User
|
||||
|
||||
|
@ -10,68 +12,85 @@ def setup(db_test):
|
|||
return
|
||||
|
||||
|
||||
class StubRedis:
|
||||
"""A class which acts as a RedisConnection without using Redis."""
|
||||
|
||||
cache = dict()
|
||||
expires = dict()
|
||||
|
||||
def get(self, key, *args):
|
||||
if "key" not in self.cache:
|
||||
self.cache[key] = None
|
||||
return self.cache[key]
|
||||
|
||||
def set(self, key, *args):
|
||||
self.cache[key] = list(args)[0]
|
||||
|
||||
def expire(self, key, *args):
|
||||
self.expires[key] = list(args)[0]
|
||||
|
||||
async def execute(self, command, key, *args):
|
||||
f = getattr(self, command)
|
||||
return f(key, *args)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def redis():
|
||||
yield StubRedis()
|
||||
def user() -> User:
|
||||
with db.begin():
|
||||
user = db.create(
|
||||
User,
|
||||
Username="test",
|
||||
Email="test@example.org",
|
||||
RealName="Test User",
|
||||
Passwd="testPassword",
|
||||
AccountTypeID=USER_ID,
|
||||
)
|
||||
yield user
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_fakeredis_cache():
|
||||
cache._redis.flushall()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_count_cache(redis):
|
||||
db.create(
|
||||
User,
|
||||
Username="user1",
|
||||
Email="user1@example.org",
|
||||
Passwd="testPassword",
|
||||
AccountTypeID=USER_ID,
|
||||
)
|
||||
|
||||
async def test_db_count_cache(user):
|
||||
query = db.query(User)
|
||||
|
||||
# Now, perform several checks that db_count_cache matches query.count().
|
||||
|
||||
# We have no cached value yet.
|
||||
assert await cache.db_count_cache(redis, "key1", query) == query.count()
|
||||
assert cache._redis.get("key1") is None
|
||||
|
||||
# Add to cache
|
||||
assert await cache.db_count_cache("key1", query) == query.count()
|
||||
|
||||
# It's cached now.
|
||||
assert await cache.db_count_cache(redis, "key1", query) == query.count()
|
||||
assert cache._redis.get("key1") is not None
|
||||
|
||||
# It does not expire
|
||||
assert cache._redis.ttl("key1") == -1
|
||||
|
||||
# Cache a query with an expire.
|
||||
value = await cache.db_count_cache("key2", query, 100)
|
||||
assert value == query.count()
|
||||
|
||||
assert cache._redis.ttl("key2") == 100
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_db_count_cache_expires(redis):
|
||||
db.create(
|
||||
User,
|
||||
Username="user1",
|
||||
Email="user1@example.org",
|
||||
Passwd="testPassword",
|
||||
AccountTypeID=USER_ID,
|
||||
)
|
||||
|
||||
async def test_db_query_cache(user):
|
||||
query = db.query(User)
|
||||
|
||||
# Cache a query with an expire.
|
||||
value = await cache.db_count_cache(redis, "key1", query, 100)
|
||||
assert value == query.count()
|
||||
# We have no cached value yet.
|
||||
assert cache._redis.get("key1") is None
|
||||
|
||||
assert redis.expires["key1"] == 100
|
||||
# Add to cache
|
||||
await cache.db_query_cache("key1", query)
|
||||
|
||||
# It's cached now.
|
||||
assert cache._redis.get("key1") is not None
|
||||
|
||||
# Modify our user and make sure we got a cached value
|
||||
user.Username = "changed"
|
||||
cached = await cache.db_query_cache("key1", query)
|
||||
assert cached[0].Username != query.all()[0].Username
|
||||
|
||||
# It does not expire
|
||||
assert cache._redis.ttl("key1") == -1
|
||||
|
||||
# Cache a query with an expire.
|
||||
value = await cache.db_query_cache("key2", query, 100)
|
||||
assert len(value) == query.count()
|
||||
assert value[0].Username == query.all()[0].Username
|
||||
|
||||
assert cache._redis.ttl("key2") == 100
|
||||
|
||||
# Test "max_search_entries" options
|
||||
def mock_max_search_entries(section: str, key: str, fallback: int) -> str:
|
||||
if section == "cache" and key == "max_search_entries":
|
||||
return 1
|
||||
return config.getint(section, key)
|
||||
|
||||
with mock.patch("aurweb.config.getint", side_effect=mock_max_search_entries):
|
||||
# Try to add another entry (we already have 2)
|
||||
await cache.db_query_cache("key3", query)
|
||||
|
||||
# Make sure it was not added because it exceeds our max.
|
||||
assert cache._redis.get("key3") is None
|
||||
|
|
|
@ -5,7 +5,7 @@ from unittest import mock
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from aurweb import asgi, config, db, time
|
||||
from aurweb import asgi, cache, config, db, time
|
||||
from aurweb.filters import datetime_display
|
||||
from aurweb.models import License, PackageLicense
|
||||
from aurweb.models.account_type import USER_ID, AccountType
|
||||
|
@ -63,6 +63,11 @@ def setup(db_test):
|
|||
return
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clear_fakeredis_cache():
|
||||
cache._redis.flushall()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client() -> TestClient:
|
||||
"""Yield a FastAPI TestClient."""
|
||||
|
@ -815,6 +820,8 @@ def test_packages_search_by_keywords(client: TestClient, packages: list[Package]
|
|||
|
||||
# And request packages with that keyword, we should get 1 result.
|
||||
with client as request:
|
||||
# clear fakeredis cache
|
||||
cache._redis.flushall()
|
||||
response = request.get("/packages", params={"SeB": "k", "K": "testKeyword"})
|
||||
assert response.status_code == int(HTTPStatus.OK)
|
||||
|
||||
|
@ -870,6 +877,8 @@ def test_packages_search_by_maintainer(
|
|||
|
||||
# This time, we should get `package` returned, since it's now an orphan.
|
||||
with client as request:
|
||||
# clear fakeredis cache
|
||||
cache._redis.flushall()
|
||||
response = request.get("/packages", params={"SeB": "m"})
|
||||
assert response.status_code == int(HTTPStatus.OK)
|
||||
root = parse_root(response.text)
|
||||
|
@ -902,6 +911,8 @@ def test_packages_search_by_comaintainer(
|
|||
|
||||
# Then test that it's returned by our search.
|
||||
with client as request:
|
||||
# clear fakeredis cache
|
||||
cache._redis.flushall()
|
||||
response = request.get(
|
||||
"/packages", params={"SeB": "c", "K": maintainer.Username}
|
||||
)
|
||||
|
|
|
@ -5,7 +5,8 @@ import fastapi
|
|||
import pytest
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from aurweb import filters, util
|
||||
from aurweb import db, filters, util
|
||||
from aurweb.models.user import User
|
||||
from aurweb.testing.requests import Request
|
||||
|
||||
|
||||
|
@ -146,3 +147,26 @@ def assert_multiple_keys(pks):
|
|||
assert key1 == k1[1]
|
||||
assert pfx2 == k2[0]
|
||||
assert key2 == k2[1]
|
||||
|
||||
|
||||
def test_hash_query():
|
||||
# No conditions
|
||||
query = db.query(User)
|
||||
assert util.hash_query(query) == "75e76026b7d576536e745ec22892cf8f5d7b5d62"
|
||||
|
||||
# With where clause
|
||||
query = db.query(User).filter(User.Username == "bla")
|
||||
assert util.hash_query(query) == "4dca710f33b1344c27ec6a3c266970f4fa6a8a00"
|
||||
|
||||
# With where clause and sorting
|
||||
query = db.query(User).filter(User.Username == "bla").order_by(User.Username)
|
||||
assert util.hash_query(query) == "ee2c7846fede430776e140f8dfe1d83cd21d2eed"
|
||||
|
||||
# With where clause, sorting and specific columns
|
||||
query = (
|
||||
db.query(User)
|
||||
.filter(User.Username == "bla")
|
||||
.order_by(User.Username)
|
||||
.with_entities(User.Username)
|
||||
)
|
||||
assert util.hash_query(query) == "c1db751be61443d266cf643005eee7a884dac103"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue