alnoda-workspaces/workspaces/notebook-old-workspace/nbviewer/nbviewer/cache.py
2022-05-30 07:24:06 +00:00

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)