mirror of
https://github.com/bluxmit/alnoda-workspaces.git
synced 2024-04-30 04:02:20 +12:00
199 lines
6.2 KiB
Python
199 lines
6.2 KiB
Python
# -----------------------------------------------------------------------------
|
|
# Copyright (C) Jupyter Development Team
|
|
#
|
|
# Distributed under the terms of the BSD License. The full license is in
|
|
# the file COPYING, distributed as part of this software.
|
|
# -----------------------------------------------------------------------------
|
|
import asyncio
|
|
import zlib
|
|
from asyncio import Future
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
from time import monotonic
|
|
|
|
from tornado.log import app_log
|
|
|
|
try:
|
|
import pylibmc
|
|
except ImportError:
|
|
pylibmc = None
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Code
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class MockCache(object):
|
|
"""Mock Cache. Just stores nothing and always return None on get."""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
async def get(self, key):
|
|
f = Future()
|
|
f.set_result(None)
|
|
return await f
|
|
|
|
async def set(self, key, value, *args, **kwargs):
|
|
f = Future()
|
|
f.set_result(None)
|
|
return await f
|
|
|
|
async def add(self, key, value, *args, **kwargs):
|
|
f = Future()
|
|
f.set_result(True)
|
|
return await f
|
|
|
|
async def incr(self, key):
|
|
f = Future()
|
|
f.set_result(None)
|
|
return await f
|
|
|
|
|
|
class DummyAsyncCache(object):
|
|
"""Dummy Async Cache. Just stores things in a dict of fixed size."""
|
|
|
|
def __init__(self, limit=10):
|
|
self._cache = {}
|
|
self._cache_order = []
|
|
self.limit = limit
|
|
|
|
async def get(self, key):
|
|
f = Future()
|
|
f.set_result(self._get(key))
|
|
return await f
|
|
|
|
def _get(self, key):
|
|
value, deadline = self._cache.get(key, (None, None))
|
|
if deadline and deadline < monotonic():
|
|
self._cache.pop(key)
|
|
self._cache_order.remove(key)
|
|
else:
|
|
return value
|
|
|
|
async def set(self, key, value, expires=0):
|
|
if key in self._cache and self._cache_order[-1] != key:
|
|
idx = self._cache_order.index(key)
|
|
del self._cache_order[idx]
|
|
self._cache_order.append(key)
|
|
else:
|
|
if len(self._cache) >= self.limit:
|
|
oldest = self._cache_order.pop(0)
|
|
self._cache.pop(oldest)
|
|
self._cache_order.append(key)
|
|
|
|
if not expires:
|
|
deadline = None
|
|
else:
|
|
deadline = monotonic() + expires
|
|
|
|
self._cache[key] = (value, deadline)
|
|
f = Future()
|
|
f.set_result(True)
|
|
return await f
|
|
|
|
async def add(self, key, value, expires=0):
|
|
f = Future()
|
|
if self._get(key) is not None:
|
|
f.set_result(False)
|
|
else:
|
|
await self.set(key, value, expires)
|
|
f.set_result(True)
|
|
return await f
|
|
|
|
async def incr(self, key):
|
|
f = Future()
|
|
if self._get(key) is not None:
|
|
value, deadline = self._cache[key]
|
|
value = value + 1
|
|
self._cache[key] = (value, deadline)
|
|
else:
|
|
value = None
|
|
f.set_result(value)
|
|
return await f
|
|
|
|
|
|
class AsyncMemcache(object):
|
|
"""Wrap pylibmc.Client to run in a background thread
|
|
|
|
via concurrent.futures.ThreadPoolExecutor
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.pool = kwargs.pop("pool", None) or ThreadPoolExecutor(1)
|
|
|
|
self.mc = pylibmc.Client(*args, **kwargs)
|
|
self.mc_pool = pylibmc.ThreadMappedPool(self.mc)
|
|
|
|
self.loop = asyncio.get_event_loop()
|
|
|
|
async def _call_in_thread(self, method_name, *args, **kwargs):
|
|
# https://stackoverflow.com/questions/34376814/await-future-from-executor-future-cant-be-used-in-await-expression
|
|
|
|
key = args[0]
|
|
if "multi" in method_name:
|
|
key = sorted(key)[0].decode("ascii") + "[%i]" % len(key)
|
|
app_log.debug("memcache submit %s %s", method_name, key)
|
|
|
|
def f():
|
|
app_log.debug("memcache %s %s", method_name, key)
|
|
with self.mc_pool.reserve() as mc:
|
|
meth = getattr(mc, method_name)
|
|
return meth(*args, **kwargs)
|
|
|
|
return await self.loop.run_in_executor(self.pool, f)
|
|
|
|
async def get(self, *args, **kwargs):
|
|
return await self._call_in_thread("get", *args, **kwargs)
|
|
|
|
async def set(self, *args, **kwargs):
|
|
return await self._call_in_thread("set", *args, **kwargs)
|
|
|
|
async def add(self, *args, **kwargs):
|
|
return await self._call_in_thread("add", *args, **kwargs)
|
|
|
|
async def incr(self, *args, **kwargs):
|
|
return await self._call_in_thread("incr", *args, **kwargs)
|
|
|
|
|
|
class AsyncMultipartMemcache(AsyncMemcache):
|
|
"""subclass of AsyncMemcache that splits large files into multiple chunks
|
|
|
|
because memcached limits record size to 1MB
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.chunk_size = kwargs.pop("chunk_size", 950000)
|
|
self.max_chunks = kwargs.pop("max_chunks", 16)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
async def get(self, key, *args, **kwargs):
|
|
keys = [("%s.%i" % (key, idx)).encode() for idx in range(self.max_chunks)]
|
|
values = await self._call_in_thread("get_multi", keys, *args, **kwargs)
|
|
parts = []
|
|
for key in keys:
|
|
if key not in values:
|
|
break
|
|
parts.append(values[key])
|
|
if parts:
|
|
compressed = b"".join(parts)
|
|
try:
|
|
result = zlib.decompress(compressed)
|
|
except zlib.error as e:
|
|
app_log.error("zlib decompression of %s failed: %s", key, e)
|
|
else:
|
|
return result
|
|
|
|
async def set(self, key, value, *args, **kwargs):
|
|
chunk_size = self.chunk_size
|
|
compressed = zlib.compress(value)
|
|
offsets = range(0, len(compressed), chunk_size)
|
|
app_log.debug("storing %s in %i chunks", key, len(offsets))
|
|
if len(offsets) > self.max_chunks:
|
|
raise ValueError("file is too large: %sB" % len(compressed))
|
|
values = {}
|
|
for idx, offset in enumerate(offsets):
|
|
values[("%s.%i" % (key, idx)).encode()] = compressed[
|
|
offset : offset + chunk_size
|
|
]
|
|
return await self._call_in_thread("set_multi", values, *args, **kwargs)
|