launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #27728
[Merge] ~jugmac00/launchpad:fix-pymemcache-regression-bytes-vs-str into launchpad:master
Jürgen Gmach has proposed merging ~jugmac00/launchpad:fix-pymemcache-regression-bytes-vs-str into launchpad:master.
Commit message:
Fix pymemcache regression in SnapStoreClient.listChannels
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1950811 in Launchpad itself: "pymemcache regression in SnapStoreClient.listChannels"
https://bugs.launchpad.net/launchpad/+bug/1950811
For more details, see:
https://code.launchpad.net/~jugmac00/launchpad/+git/launchpad/+merge/411913
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~jugmac00/launchpad:fix-pymemcache-regression-bytes-vs-str into launchpad:master.
diff --git a/lib/lp/code/model/branch.py b/lib/lp/code/model/branch.py
index b915da8..37ee026 100644
--- a/lib/lp/code/model/branch.py
+++ b/lib/lp/code/model/branch.py
@@ -9,7 +9,6 @@ __all__ = [
from datetime import datetime
from functools import partial
-import json
import operator
import os.path
@@ -824,8 +823,7 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
# and this should be relatively rare.
enable_memcache = False
dirname = os.path.dirname(filename)
- unset = object()
- file_list = unset
+ file_list = None
if enable_memcache:
memcache_client = getUtility(IMemcacheClient)
instance_name = urlsplit(
@@ -833,16 +831,12 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
memcache_key = six.ensure_binary(
'%s:bzr-file-list:%s:%s:%s' % (
instance_name, self.id, revision_id, dirname))
- cached_file_list = memcache_client.get(memcache_key)
- if cached_file_list is not None:
- try:
- file_list = json.loads(cached_file_list)
- except Exception:
- logger.exception(
- 'Cannot load cached file list for %s:%s:%s; deleting' %
- (self.unique_name, revision_id, dirname))
- memcache_client.delete(memcache_key)
- if file_list is unset:
+ message = "file list for %s:%s:%s" % (
+ self.unique_name, revision_id, dirname
+ )
+ file_list = memcache_client.get_json(memcache_key, logger, message)
+
+ if file_list is None:
try:
inventory = hosting_client.getInventory(
self.id, dirname, rev=revision_id)
@@ -854,7 +848,7 @@ class Branch(SQLBase, WebhookTargetMixin, BzrIdentityMixin):
if enable_memcache:
# Cache the file list in case there's a request for another
# file in the same directory.
- memcache_client.set(memcache_key, json.dumps(file_list))
+ memcache_client.set_json(memcache_key, file_list)
file_id = (file_list or {}).get(os.path.basename(filename))
if file_id is None:
raise BranchFileNotFound(
diff --git a/lib/lp/code/model/gitref.py b/lib/lp/code/model/gitref.py
index ca4c05b..73c877f 100644
--- a/lib/lp/code/model/gitref.py
+++ b/lib/lp/code/model/gitref.py
@@ -9,7 +9,6 @@ __all__ = [
]
from functools import partial
-import json
import re
from lazr.lifecycle.event import ObjectCreatedEvent
@@ -353,23 +352,17 @@ class GitRefMixin:
if stop is not None:
memcache_key += ":stop=%s" % stop
memcache_key = six.ensure_binary(memcache_key)
- cached_log = memcache_client.get(memcache_key)
- if cached_log is not None:
- try:
- log = json.loads(cached_log)
- except Exception:
- if logger is not None:
- logger.exception(
- "Cannot load cached log information for %s:%s; "
- "deleting" % (path, start))
- memcache_client.delete(memcache_key)
+
+ message = "log information for %s:%s" % (path, start)
+ log = memcache_client.get_json(memcache_key, logger, message)
+
if log is None:
if enable_hosting:
hosting_client = getUtility(IGitHostingClient)
log = removeSecurityProxy(hosting_client.getLog(
path, start, limit=limit, stop=stop, logger=logger))
if enable_memcache:
- memcache_client.set(memcache_key, json.dumps(log))
+ memcache_client.set_json(memcache_key, log)
else:
# Fall back to synthesising something reasonable based on
# information in our own database.
diff --git a/lib/lp/code/model/tests/test_branch.py b/lib/lp/code/model/tests/test_branch.py
index 9379266..defa405 100644
--- a/lib/lp/code/model/tests/test_branch.py
+++ b/lib/lp/code/model/tests/test_branch.py
@@ -3440,7 +3440,6 @@ class TestBranchGetBlob(TestCaseWithFactory):
self.assertRaises(
BranchFileNotFound, branch.getBlob,
'src/README.txt', revision_id='some-rev')
- self.assertEqual([], hosting_fixture.getInventory.calls)
self.assertEqual([], hosting_fixture.getBlob.calls)
diff --git a/lib/lp/services/memcache/client.py b/lib/lp/services/memcache/client.py
index e04be16..3aa7f06 100644
--- a/lib/lp/services/memcache/client.py
+++ b/lib/lp/services/memcache/client.py
@@ -4,9 +4,11 @@
"""Launchpad Memcache client."""
__all__ = [
+ 'JsonClient',
'memcache_client_factory',
]
+import json
import re
from pymemcache.client.hash import HashClient
@@ -18,8 +20,30 @@ from pymemcache.serde import (
from lp.services.config import config
+class JsonClient(HashClient):
+ """memcached client with added JSON handling"""
+
+ def get_json(self, key, logger, message):
+ data = self.get(key)
+ if data is not None:
+ try:
+ data = json.loads(data)
+ # the exceptions are chosen deliberately in order to gracefully
+ # handle invalid data
+ except (TypeError, ValueError):
+ if logger and message:
+ logger.exception(
+ "Cannot load cached %s;deleting" % message
+ )
+ self.delete(key)
+ return data
+
+ def set_json(self, key, value, expire=0):
+ self.set(key, json.dumps(value), expire)
+
+
def memcache_client_factory(timeline=True):
- """Return a pymemcache HashClient for Launchpad."""
+ """Return an extended pymemcache client for Launchpad."""
# example value for config.memcache.servers:
# (127.0.0.1:11242,1)
servers = [
@@ -31,7 +55,7 @@ def memcache_client_factory(timeline=True):
from lp.services.memcache.timeline import TimelineRecordingClient
client_factory = TimelineRecordingClient
else:
- client_factory = HashClient
+ client_factory = JsonClient
return client_factory(
servers,
serializer=python_memcache_serializer,
diff --git a/lib/lp/services/memcache/testing.py b/lib/lp/services/memcache/testing.py
index c4809f4..51df536 100644
--- a/lib/lp/services/memcache/testing.py
+++ b/lib/lp/services/memcache/testing.py
@@ -5,6 +5,7 @@ __all__ = [
'MemcacheFixture',
]
+import json
import time as _time
import fixtures
@@ -30,6 +31,19 @@ class MemcacheFixture(fixtures.Fixture):
else:
return value
+ def get_json(self, key, logger, message):
+ data = self.get(key)
+ if data is not None:
+ try:
+ data = json.loads(data)
+ except (TypeError, ValueError):
+ if logger and message:
+ logger.exception(
+ "Cannot load cached %s;deleting" % message
+ )
+ self.delete(key)
+ return data
+
def set(self, key, val, expire=0):
# memcached accepts either delta-seconds from the current time or
# absolute epoch-seconds, and tells them apart using a magic
@@ -40,6 +54,9 @@ class MemcacheFixture(fixtures.Fixture):
self._cache[key] = (val, expire)
return 1
+ def set_json(self, key, value, expire=0):
+ self.set(key, json.dumps(value), expire)
+
def delete(self, key):
self._cache.pop(key, None)
return 1
diff --git a/lib/lp/services/memcache/timeline.py b/lib/lp/services/memcache/timeline.py
index 9e1bf3e..74c2c6e 100644
--- a/lib/lp/services/memcache/timeline.py
+++ b/lib/lp/services/memcache/timeline.py
@@ -10,13 +10,13 @@ __all__ = [
import logging
from lazr.restful.utils import get_current_browser_request
-from pymemcache.client.hash import HashClient
from lp.services import features
+from lp.services.memcache.client import JsonClient
from lp.services.timeline.requesttimeline import get_request_timeline
-class TimelineRecordingClient(HashClient):
+class TimelineRecordingClient(JsonClient):
def __get_timeline_action(self, suffix, key):
request = get_current_browser_request()
@@ -36,7 +36,7 @@ class TimelineRecordingClient(HashClient):
return None
action = self.__get_timeline_action("get", key)
try:
- return HashClient.get(self, key)
+ return JsonClient.get(self, key)
finally:
action.finish()
@@ -45,7 +45,7 @@ class TimelineRecordingClient(HashClient):
return None
action = self.__get_timeline_action("set", key)
try:
- success = HashClient.set(self, key, value, expire=expire)
+ success = JsonClient.set(self, key, value, expire=expire)
if success:
logging.debug("Memcache set succeeded for %s", key)
else:
diff --git a/lib/lp/snappy/model/snapstoreclient.py b/lib/lp/snappy/model/snapstoreclient.py
index c43d705..30e7c45 100644
--- a/lib/lp/snappy/model/snapstoreclient.py
+++ b/lib/lp/snappy/model/snapstoreclient.py
@@ -390,19 +390,14 @@ class SnapStoreClient:
"""See `ISnapStoreClient`."""
if config.snappy.store_search_url is None:
return _default_store_channels
- channels = None
memcache_client = getUtility(IMemcacheClient)
search_host = urlsplit(config.snappy.store_search_url).hostname
memcache_key = ("%s:channels" % search_host).encode("UTF-8")
- cached_channels = memcache_client.get(memcache_key)
- if cached_channels is not None:
- try:
- channels = json.loads(cached_channels)
- except ValueError:
- log.exception(
- "Cannot load cached channels for %s; deleting" %
- search_host)
- memcache_client.delete(memcache_key)
+ message = "channels for %s" % search_host
+
+ channels = memcache_client.get_json(
+ key=memcache_key, logger=log, message=message)
+
if (channels is None and
not getFeatureFlag(u"snap.disable_channel_search")):
path = "api/v1/channels"
@@ -423,8 +418,8 @@ class SnapStoreClient:
DAY_IN_SECONDS = 60 * 60 * 24
# pymemcache's `expire` expects an int
expire_time = int(time.time()) + DAY_IN_SECONDS
- memcache_client.set(
- memcache_key, json.dumps(channels), expire_time)
+ memcache_client.set_json(
+ memcache_key, channels, expire_time)
if channels is None:
channels = _default_store_channels
return channels
Follow ups