Skip to content

Commit 17b82ba

Browse files
authored
feat: actually record xmlrpc cache metrics (#20347)
Signed-off-by: Mike Fiedler <miketheman@gmail.com>
1 parent dc6b05c commit 17b82ba

3 files changed

Lines changed: 78 additions & 27 deletions

File tree

tests/unit/legacy/api/xmlrpc/test_cache.py

Lines changed: 58 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
cached_return_view,
1919
services,
2020
)
21+
from warehouse.legacy.api.xmlrpc.cache.fncache import StubMetricReporter
2122
from warehouse.legacy.api.xmlrpc.cache.interfaces import CacheError, IXMLRPCCache
2223

2324

@@ -160,12 +161,29 @@ def test_create_redis_service(self, pyramid_request, mocker):
160161

161162
assert isinstance(service, RedisXMLRPCCache)
162163
assert service._purger is delay
163-
assert delay.call_args_list == [
164-
mocker.call("wu"),
165-
mocker.call("tang"),
166-
mocker.call("4"),
167-
mocker.call("evah"),
168-
]
164+
# Without this the cache falls back to StubMetricReporter and reports nothing.
165+
assert service.redis_lru.metric_reporter is pyramid_request.metrics
166+
167+
def test_create_redis_service_without_a_request(self, mocker):
168+
"""The factory tolerates being handed something that is not a request.
169+
170+
`execute_purge` calls it with the Configurator, which has no `metrics`. That
171+
path only enqueues purges, so a stubbed reporter there costs nothing, but an
172+
AttributeError would break every `after_commit`.
173+
"""
174+
delay = mocker.stub(name="delay")
175+
config = types.SimpleNamespace(
176+
registry=types.SimpleNamespace(
177+
settings={"warehouse.xmlrpc.cache.url": "redis://"}
178+
),
179+
task=mocker.Mock(return_value=mocker.Mock(delay=delay)),
180+
)
181+
182+
service = RedisXMLRPCCache.create_service(None, config)
183+
service.purge_tags(["wu", "tang"])
184+
185+
assert isinstance(service.redis_lru.metric_reporter, StubMetricReporter)
186+
assert delay.call_args_list == [mocker.call("wu"), mocker.call("tang")]
169187

170188

171189
class TestRedisLru:
@@ -193,8 +211,28 @@ def test_redis_custom_metrics(self, metrics, mockredis, mocker):
193211
func_test, [0, 1], {"kwarg0": 2, "kwarg1": 3}, None, None, None
194212
)
195213
assert metrics.increment.call_args_list == [
196-
mocker.call("lru.cache.miss"),
197-
mocker.call("lru.cache.hit"),
214+
mocker.call("warehouse.lru.cache.miss"),
215+
mocker.call("warehouse.lru.cache.hit"),
216+
]
217+
218+
def test_falsy_cached_value_is_a_single_hit(self, metrics, mockredis, mocker):
219+
"""An empty result is a real hit: counted once, and the view is not re-run.
220+
221+
`fetch` compares the cached value against None instead of testing its
222+
truthiness. Testing truthiness reports the same request as both a hit and a
223+
miss, which would make the hit rate unreadable, and re-runs the query every
224+
time. `package_roles` returns `[]` for any unknown name.
225+
"""
226+
empty_func = mocker.Mock(return_value=[], __name__="empty_func")
227+
redis_lru = RedisLru(mockredis, metric_reporter=metrics)
228+
229+
assert redis_lru.fetch(empty_func, [], {}, "[]", None, None) == []
230+
assert redis_lru.fetch(empty_func, [], {}, "[]", None, None) == []
231+
232+
empty_func.assert_called_once_with()
233+
assert metrics.increment.call_args_list == [
234+
mocker.call("warehouse.lru.cache.miss"),
235+
mocker.call("warehouse.lru.cache.hit"),
198236
]
199237

200238
def test_redis_purge(self, metrics, mockredis, mocker):
@@ -216,11 +254,11 @@ def test_redis_purge(self, metrics, mockredis, mocker):
216254
func_test, [0, 1], {"kwarg0": 2, "kwarg1": 3}, None, "test", None
217255
)
218256
assert metrics.increment.call_args_list == [
219-
mocker.call("lru.cache.miss"),
220-
mocker.call("lru.cache.hit"),
221-
mocker.call("lru.cache.purge"),
222-
mocker.call("lru.cache.miss"),
223-
mocker.call("lru.cache.hit"),
257+
mocker.call("warehouse.lru.cache.miss"),
258+
mocker.call("warehouse.lru.cache.hit"),
259+
mocker.call("warehouse.lru.cache.purge"),
260+
mocker.call("warehouse.lru.cache.miss"),
261+
mocker.call("warehouse.lru.cache.hit"),
224262
]
225263

226264
def test_redis_down(self, metrics, mocker):
@@ -242,13 +280,13 @@ def test_redis_down(self, metrics, mocker):
242280
redis_lru.purge("test")
243281

244282
assert metrics.increment.call_args_list == [
245-
mocker.call("lru.cache.error"), # Failed get
246-
mocker.call("lru.cache.miss"),
247-
mocker.call("lru.cache.error"), # Failed add
248-
mocker.call("lru.cache.error"), # Failed get
249-
mocker.call("lru.cache.miss"),
250-
mocker.call("lru.cache.error"), # Failed add
251-
mocker.call("lru.cache.error"), # Failed purge
283+
mocker.call("warehouse.lru.cache.error"), # Failed get
284+
mocker.call("warehouse.lru.cache.miss"),
285+
mocker.call("warehouse.lru.cache.error"), # Failed add
286+
mocker.call("warehouse.lru.cache.error"), # Failed get
287+
mocker.call("warehouse.lru.cache.miss"),
288+
mocker.call("warehouse.lru.cache.error"), # Failed add
289+
mocker.call("warehouse.lru.cache.error"), # Failed purge
252290
]
253291

254292

warehouse/legacy/api/xmlrpc/cache/fncache.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,16 @@ def get(self, func_name, key, tag):
4343
try:
4444
value = self.conn.hget(self.format_key(func_name, tag), str(key))
4545
except redis.exceptions.RedisError, redis.exceptions.ConnectionError:
46-
self.metric_reporter.increment(f"{self.name}.cache.error")
46+
self.metric_reporter.increment(f"warehouse.{self.name}.cache.error")
4747
return None
4848
if value:
49-
self.metric_reporter.increment(f"{self.name}.cache.hit")
49+
self.metric_reporter.increment(f"warehouse.{self.name}.cache.hit")
5050
value = orjson.loads(value)
5151
return value
5252

5353
def add(self, func_name, key, value, tag, expires):
5454
try:
55-
self.metric_reporter.increment(f"{self.name}.cache.miss")
55+
self.metric_reporter.increment(f"warehouse.{self.name}.cache.miss")
5656
pipeline = self.conn.pipeline()
5757
pipeline.hset(
5858
self.format_key(func_name, tag), str(key), orjson.dumps(value)
@@ -62,7 +62,7 @@ def add(self, func_name, key, value, tag, expires):
6262
pipeline.execute()
6363
return value
6464
except redis.exceptions.RedisError, redis.exceptions.ConnectionError:
65-
self.metric_reporter.increment(f"{self.name}.cache.error")
65+
self.metric_reporter.increment(f"warehouse.{self.name}.cache.error")
6666
return value
6767

6868
def purge(self, tag):
@@ -72,12 +72,19 @@ def purge(self, tag):
7272
for key in keys:
7373
pipeline.delete(key)
7474
pipeline.execute()
75-
self.metric_reporter.increment(f"{self.name}.cache.purge")
75+
self.metric_reporter.increment(f"warehouse.{self.name}.cache.purge")
7676
except redis.exceptions.RedisError, redis.exceptions.ConnectionError:
77-
self.metric_reporter.increment(f"{self.name}.cache.error")
77+
self.metric_reporter.increment(f"warehouse.{self.name}.cache.error")
7878
raise CacheError
7979

8080
def fetch(self, func, args, kwargs, key, tag, expires):
81-
return self.get(func.__name__, str(key), str(tag)) or self.add(
81+
# `get` returns None for both a miss and a Redis error, so compare against
82+
# None rather than testing truthiness: an empty list or dict is a real hit.
83+
# Treating it as a miss counts the request as both a hit and a miss and
84+
# re-runs the query on every call.
85+
value = self.get(func.__name__, str(key), str(tag))
86+
if value is not None:
87+
return value
88+
return self.add(
8289
func.__name__, str(key), func(*args, **kwargs), str(tag), expires
8390
)

warehouse/legacy/api/xmlrpc/cache/services.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ def create_service(cls, context, request):
4848
"warehouse.xmlrpc.cache.expires", 25 * 60 * 60
4949
)
5050
),
51+
# `execute_purge` calls this factory with the Configurator rather than a
52+
# request, and a Configurator has no `metrics`. That path only calls
53+
# `purge_tags`, which enqueues celery tasks and never touches `RedisLru`,
54+
# so falling back to `StubMetricReporter` there loses nothing: the real
55+
# `purge` runs in the `purge_tag` task, which does have a request.
56+
metric_reporter=getattr(request, "metrics", None),
5157
)
5258

5359
def fetch(self, func, args, kwargs, key, tag, expires):

0 commit comments

Comments
 (0)