Skip to content

Commit

Permalink
BREAKING(cache): allow caching long running results
Browse files Browse the repository at this point in the history
```sql
CREATE TABLE `cache` (
    `key` VARCHAR(220) NOT NULL,
    `value` LONGTEXT NOT NULL,
    `expires` TIMESTAMP NOT NULL,
    PRIMARY KEY (`key`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
``
  • Loading branch information
esoadamo committed Feb 3, 2025
1 parent bfbabb4 commit 46728f0
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 4 deletions.
12 changes: 12 additions & 0 deletions endpoint/admin/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from db import session
import model
import util
from util import cache


class Task(object):
Expand Down Expand Up @@ -41,6 +42,7 @@ def on_get(self, req, resp, id):
return

req.context['result'] = {'atask': util.admin.task.admin_to_json(task, do_fetch_testers=fetch_testers)}
cache.invalidate_cache(cache.get_key(None, "org", req.context['year'], 'admin_tasks'))
except SQLAlchemyError:
session.rollback()
raise
Expand Down Expand Up @@ -86,6 +88,7 @@ def on_put(self, req, resp, id):
task.eval_comment = data['eval_comment']

session.commit()
cache.invalidate_cache(cache.get_key(None, "org", req.context['year'], 'admin_tasks'))
except SQLAlchemyError:
session.rollback()
raise
Expand Down Expand Up @@ -148,6 +151,7 @@ def on_delete(self, req, resp, id):
session.commit()

req.context['result'] = {}
cache.invalidate_cache(cache.get_key(None, "org", req.context['year'], 'admin_tasks'))
except SQLAlchemyError:
session.rollback()
raise
Expand All @@ -174,6 +178,12 @@ def on_get(self, req, resp):
resp.status = falcon.HTTP_400
return

cache_key = cache.get_key(None, "org", req.context['year'], 'admin_tasks')
cached = cache.get_record(cache_key)
if cached is not None:
req.context['result'] = cached
return

tasks = session.query(model.Task, model.Wave).\
join(model.Wave, model.Task.wave == model.Wave.id)

Expand All @@ -192,6 +202,7 @@ def on_get(self, req, resp):
for task in tasks
]
}
cache.save_cache(cache_key, req.context['result'], 3600)
except SQLAlchemyError:
session.rollback()
raise
Expand Down Expand Up @@ -290,6 +301,7 @@ def on_post(self, req, resp):
session.commit()

req.context['result'] = {'atask': util.admin.task.admin_to_json(task)}
cache.invalidate_cache(cache.get_key(None, "org", req.context['year'], 'admin_tasks'))
except SQLAlchemyError:
session.rollback()
raise
Expand Down
2 changes: 2 additions & 0 deletions endpoint/admin/taskMerge.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from db import session
import model
import util
from util import cache


class TaskMerge(object):
Expand Down Expand Up @@ -79,6 +80,7 @@ def on_post(self, req, resp, id):
session.commit()
resp.status = falcon.HTTP_200
req.context['result'] = {}
cache.invalidate_cache(cache.get_key(None, "org", req.context['year'], 'admin_tasks'))
finally:
mergeLock.release()
except SQLAlchemyError:
Expand Down
12 changes: 9 additions & 3 deletions endpoint/user.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import os
import sys
import falcon
import json
Expand All @@ -13,7 +12,7 @@
import model.user
import util
import auth
from util import config
from util import config, cache
from util.logger import audit_log


Expand Down Expand Up @@ -138,6 +137,12 @@ def on_get(self, req, resp):
usr = req.context['user']
year = req.context['year_obj']

cache_key = cache.get_key(None, 'org' if usr.is_org() else 'participant', req.context['year'], f'users_score:{filt=}:{sort=}')
cached_data = cache.get_record(cache_key)
if cached_data is not None:
req.context['result'] = cached_data
return

"""
Tady se dela spoustu magie kvuli tomu, aby se usetrily SQL dotazy
Snazime se minimalizovat pocet dotazu, ktere musi byt provedeny pro
Expand Down Expand Up @@ -310,7 +315,7 @@ def on_get(self, req, resp):
if usr_task[0].id == user.User.id
]
] if users_tasks else None,
admin_data=req.context['user'].is_org(),
admin_data=usr.is_org(),
org_seasons=[
item.year_id
for item in org_seasons if item.user_id == user.User.id
Expand Down Expand Up @@ -338,6 +343,7 @@ def on_get(self, req, resp):
req.context['result'] = {
'users': users_json
}
cache.save_cache(cache_key, req.context['result'], 30 * 60)


class ChangePassword(object):
Expand Down
2 changes: 1 addition & 1 deletion model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@
from model.feedback import Feedback
from model.user_notify import UserNotify
from model.diploma import Diploma

from model.cache import Cache
15 changes: 15 additions & 0 deletions model/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from sqlalchemy import Column, String, Text, TIMESTAMP

from . import Base


class Cache(Base):
__tablename__ = 'cache'
__table_args__ = {
'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8mb4'
}

key = Column(String(220), primary_key=True, nullable=False)
value = Column(Text(), nullable=False)
expires = Column(TIMESTAMP, nullable=False)
2 changes: 2 additions & 0 deletions util/admin/taskDeploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import model
import util
from util import cache
from util.logger import get_log
from util.task import max_points

Expand Down Expand Up @@ -148,6 +149,7 @@ def deploy(task_id: int, year_id: int, deployLock: LockFile, scoped: Callable) -
thread.title = task.title

session.commit()
cache.invalidate_cache(cache.get_key(None, "org", year_id, 'admin_tasks'))
except Exception as e:
exc_type, exc_obj, exc_tb = sys.exc_info()
log("Exception: " + traceback.format_exc(), task=task_id)
Expand Down
62 changes: 62 additions & 0 deletions util/cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from datetime import datetime, timedelta
from typing import Optional
from gzip import compress, decompress
from pickle import dumps, loads
from base64 import b64encode, b64decode

from db import session
from model.cache import Cache


def get_key(user: Optional[int], role: Optional[str], year: Optional[int], subkey: str) -> str:
"""
Get a key for a user or role
:param user: user ID
:param role: role name
:param subkey: key name
:param year: year
:return: key name
"""
year = int(year) if year is not None else None
user = int(user) if user is not None else None
return f"{user=}:{role=}:{year=}:{subkey=}"


def get_record(key: str) -> Optional[any]:
"""
Get a record from cache
:param key: key to get
:return: data
"""
data = session.query(Cache).filter(Cache.key == key).first()
if data is None:
return None
if datetime.now() > data.expires:
invalidate_cache(key)
return None
return loads(decompress(b64decode(data.value.encode('ascii'))))


def invalidate_cache(key: str) -> None:
"""
Invalidate cache
:param key: key to invalidate
"""
session.query(Cache).filter(Cache.key == key).delete()
session.commit()


def save_cache(key: str, data: any, expires_second: int) -> None:
"""
Save data to cache
:param expires_second: seconds until record is considered expired
:param key: key to save
:param data: data to save
"""
data = b64encode(compress(dumps(data))).decode('ascii')

if get_record(key) is not None:
session.query(Cache).filter(Cache.key == key).delete()
expires = datetime.now() + timedelta(seconds=expires_second)
session.add(Cache(key=key, value=data, expires=expires))
session.commit()

0 comments on commit 46728f0

Please sign in to comment.