diff --git a/isso/__init__.py b/isso/__init__.py index 78fc448..3a5e937 100644 --- a/isso/__init__.py +++ b/isso/__init__.py @@ -180,7 +180,7 @@ def make_app(conf): if uwsgi is not None: cacheobj = cache.uWSGICache(timeout=3600) else: - cacheobj = cache.SQLite3Cache(db.SQLite3("/dev/shm/isso"), threshold=2048) + cacheobj = cache.SQLite3Cache(dbobj, threshold=2048) jobs = queue.Jobs() jobs.register("db-purge", dbobj, conf.getint("moderation", "purge-after")) diff --git a/isso/cache/sqlite.py b/isso/cache/sqlite.py index fabe6fb..5a8e754 100644 --- a/isso/cache/sqlite.py +++ b/isso/cache/sqlite.py @@ -4,42 +4,27 @@ from __future__ import absolute_import, unicode_literals import time +from sqlalchemy.sql import select, func + from . import Base class SQLite3Cache(Base): - """Implements a shared cache using SQLite3. Works across multiple processes - and threads, concurrent writes are not supported. + """Implements cache using SQLAlchemy Core. - JSON is used to serialize python primitives in a safe way. + JSON is used for safe serialization of python primitives. """ serialize = True - def __init__(self, connection, threshold=1024, timeout=-1): + def __init__(self, db, threshold=1024, timeout=-1): super(SQLite3Cache, self).__init__(threshold, timeout) - self.connection = connection - self.connection.execute( - 'CREATE TABLE IF NOT EXISTS cache (' - ' key TEXT PRIMARY KEY,' - ' value BLOB,' - ' time FLOAT)') - - # drop trigger, in case threshold has changed - self.connection.execute('DROP TRIGGER IF EXISTS sweeper') - self.connection.execute([ - 'CREATE TRIGGER sweeper AFTER INSERT ON cache', - 'BEGIN', - ' DELETE FROM cache WHERE key NOT IN (' - ' SELECT key FROM cache', - ' ORDER BY time DESC LIMIT {0}'.format(threshold), - ' );', - 'END']) - - def _get(self, ns, key, default=None): - rv = self.connection.execute( - 'SELECT value FROM cache WHERE key = ?', - (ns + b'-' + key, )).fetchone() + self.db = db + + def _get(self, ns, key): + rv = self.db.engine.execute( + select([self.db.cache.c.value]).where( + self.db.cache.c.key == ns + b'-' + key)).fetchone() if rv is None: raise KeyError @@ -47,11 +32,36 @@ class SQLite3Cache(Base): return rv[0] def _set(self, ns, key, value): - with self.connection.transaction as con: - con.execute( - 'INSERT OR REPLACE INTO cache (key, value, time) VALUES (?, ?, ?)', - (ns + b'-' + key, value, time.time())) + with self.db.transaction: + cnt = self.db.engine.execute( + select([func.count(self.db.cache)])).fetchone()[0] + + if cnt + 1 > self.threshold: + self.db.engine.execute( + self.db.cache.delete().where( + self.db.cache.c.key.in_(select( + [self.db.cache.c.key]) + .order_by(self.db.cache.c.time) + .limit(1)))) + + try: + self._get(ns, key) + except KeyError: + insert = True + else: + insert = False + + if insert: + stmt = self.db.cache.insert().values( + key=ns + b'-' + key, value=value, time=time.time()) + else: + stmt = self.db.cache.update().values( + value=value, time=time.time()).where( + self.db.cache.c.key == ns + b'-' + key) + + self.db.engine.execute(stmt) def _delete(self, ns, key): - with self.connection.transaction as con: - con.execute('DELETE FROM cache WHERE key = ?', (ns + b'-' + key, )) + with self.db.transaction: + self.db.engine.execute( + self.db.cache.delete(self.db.cache.c.key == ns + b'-' + key)) diff --git a/isso/db/__init__.py b/isso/db/__init__.py index a039e04..c121140 100644 --- a/isso/db/__init__.py +++ b/isso/db/__init__.py @@ -50,6 +50,11 @@ class Adapter(object): Column("key", String(255), primary_key=True), Column("value", String(255))) + self.cache = Table("cache", self.metadata, + Column("key", String(255), primary_key=True), + Column("value", LargeBinary(65535)), + Column("time", Float)) + self.metadata.create_all(self.engine) self.preferences = Preferences(self.engine, preferences) diff --git a/isso/tests/test_cache.py b/isso/tests/test_cache.py index 8e4ca9a..628b157 100644 --- a/isso/tests/test_cache.py +++ b/isso/tests/test_cache.py @@ -6,7 +6,7 @@ import unittest from isso.compat import text_type as str -from isso.db import SQLite3 +from isso.db import Adapter from isso.cache import Cache, SQLite3Cache ns = "test" @@ -56,4 +56,4 @@ class TestCache(unittest.TestCase): class TestSQLite3Cache(TestCache): def setUp(self): - self.cache = SQLite3Cache(SQLite3(":memory:"), threshold=8) + self.cache = SQLite3Cache(Adapter("sqlite:///:memory:"), threshold=8)