Skip to content

Commit eb295c2

Browse files
committed
feat: warm caches during app startup
This code runs before the public app begins accepting requests, which should mean that we don't need to hit the database very much after beginning to serve requests. Doing this at start-up also means we fetch the releases sequentially, which should (slightly) spread the load out more. I verified locally that the app is incapable of responding to requests until `public.py` finishes executing by adding a `time.sleep` to the end of it. While the sleep was running requests to the app hung. This means that `/__heartbeat__` requests won't complete successfully until caches have been warmed up. Hopefully this will help with database load during scaling events. See: https://mozilla-hub.atlassian.net/browse/SVCSE-3425
1 parent cb35965 commit eb295c2

File tree

3 files changed

+102
-1
lines changed

3 files changed

+102
-1
lines changed

src/auslib/web/public/helpers.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from auslib.AUS import AUS
77
from auslib.global_state import cache, dbo
8+
from auslib.services.releases import get_release_blob
89
from auslib.util.autograph import make_hash, sign_hash
910

1011
log = logging.getLogger(__name__)
@@ -52,4 +53,24 @@ def sign():
5253
return headers
5354

5455

56+
def warm_caches():
57+
"""Fetch all releases pointed at by a rule. This is intended to run at app
58+
startup, to ensure that it has all necessary information is in memory before
59+
it begins serving requests."""
60+
mapped_releases = set()
61+
for rule in dbo.rules.select(columns=[dbo.rules.mapping, dbo.rules.fallbackMapping], distinct=True):
62+
if rule["mapping"]:
63+
mapped_releases.add(rule["mapping"])
64+
if rule["fallbackMapping"]:
65+
mapped_releases.add(rule["fallbackMapping"])
66+
67+
for release in mapped_releases:
68+
blob = get_release_blob(release)
69+
if not blob:
70+
continue
71+
72+
for referenced_release in blob.getReferencedReleases():
73+
get_release_blob(referenced_release)
74+
75+
5576
AUS = AUS()

tests/web/test_helpers.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from unittest.mock import MagicMock
22

33
import auslib.web.public.helpers
4-
import auslib.web.public.json
4+
from auslib.blobs.base import createBlob
5+
from auslib.global_state import cache, dbo
56

67

78
def test_get_content_signature_headers(monkeypatch):
@@ -32,3 +33,79 @@ def mock_sign_hash(_, key, *__):
3233
assert auslib.web.public.helpers.get_content_signature_headers(content, product) == {"Content-Signature": f"x5u={x5u}; p384ecdsa={ecdsa}"}
3334

3435
assert mocksign.call_count == 1
36+
37+
38+
def test_warm_caches(db_schema, insert_release, firefox_54_0_1_build1, firefox_67_0_build1, superblob_e8f4a19, hotfix_bug_1548973_1_1_4, timecop_1_0):
39+
dbo.setDb("sqlite:///:memory:")
40+
db_schema.create_all(dbo.engine)
41+
dbo.rules.t.insert().execute(
42+
rule_id=5,
43+
priority=90,
44+
backgroundRate=100,
45+
mapping="Firefox-67.0-build1",
46+
fallbackMapping="Firefox-54.0.1-build1",
47+
update_type="minor",
48+
product="Firefox",
49+
mig64=True,
50+
data_version=1,
51+
)
52+
dbo.rules.t.insert().execute(
53+
priority=300,
54+
product="SystemAddons",
55+
channel="releasesjson",
56+
mapping="Superblob-e8f4a19cfd695bf0eb66a2115313c31cc23a2369c0dc7b736d2f66d9075d7c66",
57+
backgroundRate=100,
58+
update_type="minor",
59+
data_version=1,
60+
)
61+
dbo.releases.t.insert().execute(
62+
name="Firefox-54.0.1-build1",
63+
product="Firefox",
64+
data_version=1,
65+
data=createBlob(firefox_54_0_1_build1),
66+
)
67+
insert_release(firefox_67_0_build1, "Firefox", history=False)
68+
insert_release(superblob_e8f4a19, "SystemAddons", history=False)
69+
insert_release(hotfix_bug_1548973_1_1_4, "SystemAddons", history=False)
70+
insert_release(timecop_1_0, "SystemAddons", history=False)
71+
cache.reset()
72+
cache.make_cache("blob", 50, 3600)
73+
cache.make_cache("releases", 50, 3600)
74+
cache.make_cache("release_assets", 50, 3600)
75+
76+
for cache_name in ("blob", "releases", "release_assets"):
77+
c = cache.caches[cache_name]
78+
assert c.lookups == 0, cache_name
79+
assert c.hits == 0, cache_name
80+
assert c.misses == 0, cache_name
81+
82+
auslib.web.public.helpers.warm_caches()
83+
84+
# one lookup per release, all misses
85+
assert cache.caches["blob"].lookups == 1
86+
assert cache.caches["blob"].hits == 0
87+
assert cache.caches["blob"].misses == 1
88+
for cache_name in ("releases", "release_assets"):
89+
c = cache.caches[cache_name]
90+
# There are 13 lookups here, which cover the following:
91+
# - an attempt to look up 54.0.1 in this table (1)
92+
# - an attempt to look up the 3 releases referenced by 54.0.1 (4)
93+
# - the superblob (5)
94+
# - the two releases referenced by the superblob (7)
95+
# - 67.0 (8)
96+
# - the 5 releases referenced by 67.0 (13)
97+
assert c.lookups == 13, cache_name
98+
assert c.hits == 0, cache_name
99+
assert c.misses == 13, cache_name
100+
101+
auslib.web.public.helpers.warm_caches()
102+
103+
# another lookup per release, all hits this time
104+
assert cache.caches["blob"].lookups == 2
105+
assert cache.caches["blob"].hits == 1
106+
assert cache.caches["blob"].misses == 1
107+
for cache_name in ("releases", "release_assets"):
108+
c = cache.caches[cache_name]
109+
assert c.lookups == 26, cache_name
110+
assert c.hits == 13, cache_name
111+
assert c.misses == 13, cache_name

uvicorn/public.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969

7070
from auslib.global_state import cache, dbo # noqa: E402
7171
from auslib.web.public.base import create_app # noqa: E402
72+
from auslib.web.public.helpers import warm_caches # noqa: E402
7273

7374
connexion_app = create_app()
7475
application = connexion_app.app
@@ -133,3 +134,5 @@
133134

134135
if STAGING:
135136
application.config["SWAGGER_DEBUG"] = True
137+
138+
warm_caches()

0 commit comments

Comments
 (0)