Skip to content

Commit 634140a

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 a4affc2 commit 634140a

File tree

3 files changed

+98
-1
lines changed

3 files changed

+98
-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+
# TODO: fetch all mappings, fallbackMappings, and blobs referenced in a SuperBlob
68+
for release in mapped_releases:
69+
blob = get_release_blob(release)
70+
if blob and blob["schema_version"] == 4000:
71+
if response_blobs := blob.getResponseBlobs():
72+
for response_blob in response_blobs:
73+
get_release_blob(response_blob)
74+
75+
5576
AUS = AUS()

tests/web/test_helpers.py

Lines changed: 73 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,74 @@ 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_56_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-56.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_56_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+
# we account for the v1 release here as well, because these tables are
91+
# checked before the v1 release table
92+
assert c.lookups == 5, cache_name
93+
assert c.hits == 0, cache_name
94+
assert c.misses == 5, cache_name
95+
96+
auslib.web.public.helpers.warm_caches()
97+
98+
# another lookup per release, all hits this time
99+
assert cache.caches["blob"].lookups == 2
100+
assert cache.caches["blob"].hits == 1
101+
assert cache.caches["blob"].misses == 1
102+
for cache_name in ("releases", "release_assets"):
103+
c = cache.caches[cache_name]
104+
assert c.lookups == 10, cache_name
105+
assert c.hits == 5, cache_name
106+
assert c.misses == 5, cache_name

uvicorn/public.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,7 @@
133133

134134
if STAGING:
135135
application.config["SWAGGER_DEBUG"] = True
136+
137+
from auslib.web.public.helpers import warm_caches
138+
139+
warm_caches()

0 commit comments

Comments
 (0)