Skip to content

Commit 1021363

Browse files
authored
feat: warm caches during app startup (#3500)
* feat: implement getReferencedReleases for SuperBlob This will be useful when warming caches. * refactor: add helper to find release regardless of which table it is in * 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 f8d603f commit 1021363

File tree

6 files changed

+125
-15
lines changed

6 files changed

+125
-15
lines changed

src/auslib/AUS.py

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from random import randint
55
from urllib.parse import urlparse
66

7-
from auslib.blobs.base import ServeUpdate, createBlob
7+
from auslib.blobs.base import ServeUpdate
88
from auslib.global_state import cache, dbo
99
from auslib.services import releases
1010
from auslib.util.versions import PinVersion
@@ -135,18 +135,7 @@ def evaluateRules(self, updateQuery, transaction=None):
135135
# 3) Incoming release is older than the one in the mapping, defined as one of:
136136
# * version decreases
137137
# * version is the same and buildID doesn't increase
138-
def get_blob(mapping):
139-
release = releases.get_release(mapping, transaction, include_sc=False)
140-
blob = None
141-
if release:
142-
blob = createBlob(release["blob"])
143-
# TODO: remove me when old releases table dies
144-
else:
145-
release = dbo.releases.getReleases(name=mapping, limit=1, transaction=transaction)[0]
146-
blob = release["data"]
147-
return blob
148-
149-
blob = get_blob(mapping)
138+
blob = releases.get_release_blob(mapping)
150139
if not blob:
151140
return None, None, eval_metadata
152141
candidate = blob.shouldServeUpdate(updateQuery)
@@ -179,7 +168,7 @@ def get_blob(mapping):
179168
# installations vulnerable.
180169
if pin_mapping is not None:
181170
mapping = pin_mapping
182-
blob = get_blob(mapping)
171+
blob = releases.get_release_blob(mapping)
183172
if not blob or not blob.shouldServeUpdate(updateQuery):
184173
return None, None, eval_metadata
185174

src/auslib/blobs/superblob.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,9 @@ def getInnerHeaderXML(self, updateQuery, update_type, allowlistedDomains, specia
3737

3838
def getInnerFooterXML(self, updateQuery, update_type, allowlistedDomains, specialForceHosts):
3939
return " </addons>"
40+
41+
def getReferencedReleases(self):
42+
if responseBlobs := self.getResponseBlobs():
43+
return set(responseBlobs)
44+
else:
45+
return set()

src/auslib/services/releases.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,3 +927,17 @@ def get_version(mapping, trans):
927927
data_version=old_row["data_version"],
928928
)
929929
return {".": {"sc_id": sc_id, "change_type": "update", "data_version": old_row["data_version"] + 1, "signoffs": {}, "when": when}}
930+
931+
932+
def get_release_blob(name, trans=None):
933+
"""Get a release blob regardless of whether it's in the old releases or new releases."""
934+
release = releases.get_release(name, trans, include_sc=False)
935+
blob = None
936+
if release:
937+
blob = createBlob(release["blob"])
938+
# TODO: remove me when old releases table dies
939+
else:
940+
release = dbo.releases.getReleases(name=name, limit=1, transaction=trans)
941+
if release:
942+
blob = release[0]["data"]
943+
return blob

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

uwsgi/public.wsgi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ statsd.defaults.PREFIX = "balrog.public"
7070

7171
from auslib.global_state import cache, dbo # noqa
7272
from auslib.web.public.base import create_app
73+
from auslib.web.public.helpers import warm_caches
7374

7475
application = create_app().app
7576

@@ -133,3 +134,5 @@ if os.environ.get("CACHE_CONTROL"):
133134

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

0 commit comments

Comments
 (0)