Index: appengine/swarming/server/bot_code.py |
diff --git a/appengine/swarming/server/bot_code.py b/appengine/swarming/server/bot_code.py |
index 81a56e0d7ad39fbc51469174e872f37d4705ad7a..f7a8a5a69eebdec8a8e5be811c03eab043fea0a6 100644 |
--- a/appengine/swarming/server/bot_code.py |
+++ b/appengine/swarming/server/bot_code.py |
@@ -28,6 +28,8 @@ from server import config as local_config |
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
+MAX_MEMCACHED_SIZE_BYTES = 1000000 |
+BOT_CODE_NS = 'bot_code' |
### Models. |
@@ -222,7 +224,7 @@ def get_swarming_bot_zip(host): |
A string representing the zipped file's contents. |
""" |
version, additionals = get_bot_version(host) |
- content = memcache.get('code-' + version, namespace='bot_code') |
+ content = get_cached_swarming_bot_zip(version) |
if content: |
logging.debug('memcached bot code %s; %d bytes', version, len(content)) |
return content |
@@ -236,12 +238,80 @@ def get_swarming_bot_zip(host): |
content, version = bot_archive.get_swarming_bot_zip( |
bot_dir, host, utils.get_app_version(), additionals, |
local_config.settings().enable_ts_monitoring) |
- # This is immutable so not no need to set expiration time. |
- memcache.set('code-' + version, content, namespace='bot_code') |
logging.info('generated bot code %s; %d bytes', version, len(content)) |
+ cache_swarming_bot_zip(version, content) |
return content |
+def get_cached_swarming_bot_zip(version): |
+ """Returns the bot contents if its been cached, or None if missing.""" |
+ # see cache_swarming_bot_zip for how the "meta" entry is set |
+ meta = bot_memcache_get(version, 'meta').get_result() |
+ if meta is None: |
+ logging.info('memcache did not include metadata for version %s', version) |
+ return None |
+ num_parts, true_sig = meta.split(':') |
+ |
+ # Get everything asynchronously. If something's missing, the hash will be |
+ # wrong so no need to check that we got something from each call. |
+ futures = [bot_memcache_get(version, 'content', p) |
+ for p in range(int(num_parts))] |
+ content = '' |
+ for f in futures: |
+ chunk = f.get_result() |
+ if chunk is None: |
+ logging.error('bot code %s was missing some of its contents', version) |
+ return None |
+ content += chunk |
+ h = hashlib.sha256() |
+ h.update(content) |
+ if h.hexdigest() != true_sig: |
+ logging.error('bot code %s had signature %s instead of expected %s', |
+ version, h.hexdigest(), true_sig) |
+ return None |
+ return content |
+ |
+ |
+def cache_swarming_bot_zip(version, content): |
+ """Caches the bot code to memcache.""" |
+ h = hashlib.sha256() |
+ h.update(content) |
+ p = 0 |
+ futures = [] |
+ while len(content) > 0: |
+ chunk_size = min(MAX_MEMCACHED_SIZE_BYTES, len(content)) |
+ futures.append(bot_memcache_set(content[0:chunk_size], |
+ version, 'content', p)) |
+ content = content[chunk_size:] |
+ p += 1 |
+ meta = "%s:%s" % (p, h.hexdigest()) |
+ for f in futures: |
+ f.check_success() |
+ bot_memcache_set(meta, version, 'meta').check_success() |
+ logging.info('bot %s with sig %s saved in memcached in %d chunks', |
+ version, h.hexdigest(), p) |
+ |
+ |
+def bot_memcache_get(version, desc, part=None): |
+ """Mockable async memcache getter.""" |
+ return ndb.get_context().memcache_get(bot_key(version, desc, part), |
+ namespace=BOT_CODE_NS) |
+ |
+ |
+def bot_memcache_set(value, version, desc, part=None): |
+ """Mockable async memcache setter.""" |
+ return ndb.get_context().memcache_set(bot_key(version, desc, part), |
+ value, namespace=BOT_CODE_NS) |
+ |
+ |
+def bot_key(version, desc, part=None): |
+ """Returns a memcache key for bot entries.""" |
+ key = 'code-%s-%s' % (version, desc) |
+ if part is not None: |
+ key = '%s-%d' % (key, part) |
+ return key |
+ |
+ |
### Bootstrap token. |