diff --git a/CTFd/cache/__init__.py b/CTFd/cache/__init__.py index e741d5f4..366ad764 100644 --- a/CTFd/cache/__init__.py +++ b/CTFd/cache/__init__.py @@ -1,9 +1,43 @@ +from functools import lru_cache, wraps +from time import monotonic_ns + from flask import request from flask_caching import Cache, make_template_fragment_key cache = Cache() +def timed_lru_cache(timeout: int = 300, maxsize: int = 64, typed: bool = False): + """ + lru_cache implementation that includes a time based expiry + + Parameters: + seconds (int): Timeout in seconds to clear the WHOLE cache, default = 5 minutes + maxsize (int): Maximum Size of the Cache + typed (bool): Same value of different type will be a different entry + + Implmentation from https://gist.github.com/Morreski/c1d08a3afa4040815eafd3891e16b945?permalink_comment_id=3437689#gistcomment-3437689 + """ + + def wrapper_cache(func): + func = lru_cache(maxsize=maxsize, typed=typed)(func) + func.delta = timeout * 10 ** 9 + func.expiration = monotonic_ns() + func.delta + + @wraps(func) + def wrapped_func(*args, **kwargs): + if monotonic_ns() >= func.expiration: + func.cache_clear() + func.expiration = monotonic_ns() + func.delta + return func(*args, **kwargs) + + wrapped_func.cache_info = func.cache_info + wrapped_func.cache_clear = func.cache_clear + return wrapped_func + + return wrapper_cache + + def make_cache_key(path=None, key_prefix="view/%s"): """ This function mostly emulates Flask-Caching's `make_cache_key` function so we can delete cached api responses. diff --git a/CTFd/utils/health/__init__.py b/CTFd/utils/health/__init__.py new file mode 100644 index 00000000..f49ae6c2 --- /dev/null +++ b/CTFd/utils/health/__init__.py @@ -0,0 +1,18 @@ +from time import monotonic_ns + +from flask import current_app +from sqlalchemy_utils import database_exists + +from CTFd.cache import cache, timed_lru_cache + + +@timed_lru_cache(timeout=30) +def check_database(): + return database_exists(current_app.config["SQLALCHEMY_DATABASE_URI"]) + + +@timed_lru_cache(timeout=30) +def check_config(): + value = monotonic_ns() + cache.set("healthcheck", value) + return cache.get("healthcheck") == value diff --git a/CTFd/views.py b/CTFd/views.py index 5ca48b70..d94cecf3 100644 --- a/CTFd/views.py +++ b/CTFd/views.py @@ -44,6 +44,7 @@ from CTFd.utils.email import ( DEFAULT_VERIFICATION_EMAIL_BODY, DEFAULT_VERIFICATION_EMAIL_SUBJECT, ) +from CTFd.utils.health import check_config, check_database from CTFd.utils.helpers import get_errors, get_infos, markup from CTFd.utils.modes import USERS_MODE from CTFd.utils.security.auth import login_user @@ -504,3 +505,12 @@ def themes_beta(theme, path): if os.path.isfile(cand_path): return send_file(cand_path) abort(404) + + +@views.route("/healthcheck") +def healthcheck(): + if check_database() is False: + return "ERR", 500 + if check_config() is False: + return "ERR", 500 + return "OK", 200