Skip to content

Commit

Permalink
Merge pull request #893 from ix5/caching-drop-in
Browse files Browse the repository at this point in the history
[server] core: Replace flask-caching dependency with drop-in
  • Loading branch information
ix5 authored May 28, 2022
2 parents 360bc6c + 190ce76 commit 8dfd661
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Changelog for Isso
- Re-enable ``no-intra-emphasis`` misaka extension in default config.
- Allow ``sup`` and ``sub`` HTML elements by default
- Move ``isso-dev.cfg`` to ``contrib/`` folder
- Drop ``Flask-Caching`` dependency and use drop-in solution instead
- Run automated screenshot comparisons for testing (`#889`_)
- wsgi: Return HTTP errors as JSON if client prefers it (`#488`_, sundbry)
- Add ``data-isso-page-author-hashes`` option to client which makes it possible
Expand Down
4 changes: 2 additions & 2 deletions isso/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

import _thread as thread

from flask_caching.backends.nullcache import NullCache
from flask_caching.backends.simplecache import SimpleCache
from isso.utils.cache import NullCache
from isso.utils.cache import SimpleCache

logger = logging.getLogger("isso")

Expand Down
138 changes: 138 additions & 0 deletions isso/utils/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import pickle
from time import time

# Imported from cachelib: https://github.com/pallets-eco/cachelib
# As of v0.7.0, commit 4e23926effc41303c9e305663eafa81192442241
# License: BSD 3-Clause, Copyright 2018 Pallets

# BaseSerializer and SimpleSerializer: src/cachelib/serializers.py
# BaseCache and NullCache: src/cachelib/base.py
# SimpleCache: src/cachelib/simple.py
# Type hints and unused functions removed


class BaseSerializer:
"""BaseSerializer.loads and BaseSerializer.dumps
work on top of pickle.loads and pickle.dumps. Dumping/loading
strings and byte strings is the default for most cache types.
"""

def dumps(self, value, protocol=pickle.HIGHEST_PROTOCOL):
try:
serialized = pickle.dumps(value, protocol)
except (pickle.PickleError, pickle.PicklingError) as e:
self._warn(e)
return serialized

def loads(self, bvalue):
try:
data = pickle.loads(bvalue)
except pickle.PickleError as e:
self._warn(e)
return None
else:
return data


class SimpleSerializer(BaseSerializer):
"""Default serializer for SimpleCache."""


class BaseCache:
def __init__(self, default_timeout: int = 300):
self.default_timeout = default_timeout

def _normalize_timeout(self, timeout):
if timeout is None:
timeout = self.default_timeout
return timeout

def get(self, key):
return None

def delete(self, key):
return True

def set(self, key, value, timeout=None):
return True


class NullCache(BaseCache):
pass


class SimpleCache(BaseCache):

"""Simple memory cache for single process environments. This class exists
mainly for the development server and is not 100% thread safe. It tries
to use as many atomic operations as possible and no locks for simplicity
but it could happen under heavy load that keys are added multiple times.
:param threshold: the maximum number of items the cache stores before
it starts deleting some.
:param default_timeout: the default timeout that is used if no timeout is
specified on :meth:`~BaseCache.set`. A timeout of
0 indicates that the cache never expires.
"""

serializer = SimpleSerializer()

def __init__(
self,
threshold: int = 500,
default_timeout: int = 300,
):
BaseCache.__init__(self, default_timeout)
self._cache = {}
self._threshold = threshold or 500 # threshold = 0

def _over_threshold(self):
return len(self._cache) > self._threshold

def _remove_expired(self, now):
toremove = [k for k, (expires, _) in self._cache.items() if expires < now]
for k in toremove:
self._cache.pop(k, None)

def _remove_older(self):
k_ordered = (
k
for k, v in sorted(
self._cache.items(), key=lambda item: item[1][0]
)
)
for k in k_ordered:
self._cache.pop(k, None)
if not self._over_threshold():
break

def _prune(self):
if self._over_threshold():
now = time()
self._remove_expired(now)
# remove older items if still over threshold
if self._over_threshold():
self._remove_older()

def _normalize_timeout(self, timeout):
timeout = BaseCache._normalize_timeout(self, timeout)
if timeout > 0:
timeout = int(time()) + timeout
return timeout

def get(self, key):
try:
expires, value = self._cache[key]
if expires == 0 or expires > time():
return self.serializer.loads(value)
except KeyError:
return None

def set(self, key, value, timeout=None):
expires = self._normalize_timeout(timeout)
self._prune()
self._cache[key] = (expires, self.serializer.dumps(value))
return True

def delete(self, key):
return self._cache.pop(key, None) is not None
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from setuptools import setup, find_packages

requires = ['itsdangerous', 'Jinja2', 'misaka>=2.0,<3.0', 'html5lib',
'werkzeug>=1.0', 'bleach', 'Flask-Caching>=1.9', 'Flask']
'werkzeug>=1.0', 'bleach']
tests_require = ['pytest', 'pytest-cov']

# https://packaging.python.org/en/latest/guides/making-a-pypi-friendly-readme/
Expand Down

0 comments on commit 8dfd661

Please sign in to comment.