From 986f082b592257a38d5b7fdba923c0ad980f480b Mon Sep 17 00:00:00 2001 From: "Jens L." Date: Thu, 2 Oct 2025 16:01:28 +0200 Subject: [PATCH] packages/django-postgres-cache: Initial implementation of postgres cache (#16653) * start db cache Signed-off-by: Jens Langhammer * update codeowners Signed-off-by: Jens Langhammer * handle db error in keys Signed-off-by: Jens Langhammer * implement rest of the methods Signed-off-by: Jens Langhammer * fix unrelated warning on startup for cache Signed-off-by: Jens Langhammer * fix migrations? Signed-off-by: Jens Langhammer * add readme Signed-off-by: Jens Langhammer * dynamic dependency...? Signed-off-by: Jens Langhammer * types Signed-off-by: Jens Langhammer * rip out django_redis Signed-off-by: Jens Langhammer * format Signed-off-by: Jens Langhammer * fix tests? Signed-off-by: Jens Langhammer * fix get default Signed-off-by: Jens Langhammer * some cleanup Signed-off-by: Jens Langhammer * simplify to use ORM Signed-off-by: Jens Langhammer * remove old migrations that use cache instead of doing dynamic things Signed-off-by: Jens Langhammer * fix migration Signed-off-by: Jens Langhammer * Update packages/django-postgres-cache/django_postgres_cache/models.py Signed-off-by: Marc 'risson' Schmitt * Update packages/django-postgres-cache/django_postgres_cache/migrations/0001_initial.py Signed-off-by: Marc 'risson' Schmitt * fix redis imports Signed-off-by: Marc 'risson' Schmitt * more redis removal Signed-off-by: Marc 'risson' Schmitt * lint Signed-off-by: Marc 'risson' Schmitt --------- Signed-off-by: Jens Langhammer Signed-off-by: Marc 'risson' Schmitt Co-authored-by: Marc 'risson' Schmitt --- CODEOWNERS | 1 + authentik/admin/signals.py | 9 ++++ authentik/admin/tasks.py | 3 -- ...0_1345_squashed_0028_alter_token_intent.py | 11 ---- .../core/migrations/0046_session_and_more.py | 30 ----------- authentik/core/tasks.py | 2 + authentik/lib/sentry.py | 12 ----- authentik/root/monitoring.py | 20 +++----- authentik/root/settings.py | 14 ++--- authentik/tasks/middleware.py | 6 +-- .../system_migrations/to_0_13_authentik.py | 16 ------ lifecycle/wait_for_db.py | 24 +-------- .../django_dramatiq_postgres/apps.py | 7 ++- packages/django-postgres-cache/README.md | 12 +++++ .../django_postgres_cache/__init__.py | 0 .../django_postgres_cache/apps.py | 6 +++ .../django_postgres_cache/backend.py | 51 +++++++++++++++++++ .../migrations/0001_initial.py | 24 +++++++++ .../migrations/__init__.py | 0 .../django_postgres_cache/models.py | 14 +++++ .../django_postgres_cache/tasks.py | 11 ++++ packages/django-postgres-cache/pyproject.toml | 46 +++++++++++++++++ pyproject.toml | 8 ++- uv.lock | 29 +++++------ 24 files changed, 215 insertions(+), 141 deletions(-) create mode 100644 authentik/admin/signals.py create mode 100644 packages/django-postgres-cache/README.md create mode 100644 packages/django-postgres-cache/django_postgres_cache/__init__.py create mode 100644 packages/django-postgres-cache/django_postgres_cache/apps.py create mode 100644 packages/django-postgres-cache/django_postgres_cache/backend.py create mode 100644 packages/django-postgres-cache/django_postgres_cache/migrations/0001_initial.py create mode 100644 packages/django-postgres-cache/django_postgres_cache/migrations/__init__.py create mode 100644 packages/django-postgres-cache/django_postgres_cache/models.py create mode 100644 packages/django-postgres-cache/django_postgres_cache/tasks.py create mode 100644 packages/django-postgres-cache/pyproject.toml diff --git a/CODEOWNERS b/CODEOWNERS index 03f37329e5..92f5a888d8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -24,6 +24,7 @@ Makefile @goauthentik/infrastructure .editorconfig @goauthentik/infrastructure CODEOWNERS @goauthentik/infrastructure # Backend packages +packages/django-postgres-cache @goauthentik/backend packages/django-dramatiq-postgres @goauthentik/backend # Web packages packages/docusaurus-config @goauthentik/frontend diff --git a/authentik/admin/signals.py b/authentik/admin/signals.py new file mode 100644 index 0000000000..a5c76a4b85 --- /dev/null +++ b/authentik/admin/signals.py @@ -0,0 +1,9 @@ +from django.dispatch import receiver + +from authentik.admin.tasks import _set_prom_info +from authentik.root.signals import post_startup + + +@receiver(post_startup) +def post_startup_admin_metrics(sender, **_): + _set_prom_info() diff --git a/authentik/admin/tasks.py b/authentik/admin/tasks.py index 4d96f07b4a..8b6f2b42dc 100644 --- a/authentik/admin/tasks.py +++ b/authentik/admin/tasks.py @@ -71,6 +71,3 @@ def update_latest_version(): except (RequestException, IndexError) as exc: cache.set(VERSION_CACHE_KEY, VERSION_NULL, VERSION_CACHE_TIMEOUT) raise exc - - -_set_prom_info() diff --git a/authentik/core/migrations/0018_auto_20210330_1345_squashed_0028_alter_token_intent.py b/authentik/core/migrations/0018_auto_20210330_1345_squashed_0028_alter_token_intent.py index 353aefa563..16bd255bf6 100644 --- a/authentik/core/migrations/0018_auto_20210330_1345_squashed_0028_alter_token_intent.py +++ b/authentik/core/migrations/0018_auto_20210330_1345_squashed_0028_alter_token_intent.py @@ -13,14 +13,6 @@ import authentik.core.models import authentik.lib.models -def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): - from django.contrib.sessions.backends.cache import KEY_PREFIX - from django.core.cache import cache - - session_keys = cache.keys(KEY_PREFIX + "*") - cache.delete_many(session_keys) - - def fix_duplicates(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): db_alias = schema_editor.connection.alias Token = apps.get_model("authentik_core", "token") @@ -151,9 +143,6 @@ class Migration(migrations.Migration): "abstract": False, }, ), - migrations.RunPython( - code=migrate_sessions, - ), migrations.AlterField( model_name="application", name="meta_launch_url", diff --git a/authentik/core/migrations/0046_session_and_more.py b/authentik/core/migrations/0046_session_and_more.py index 1b52b1d65f..bffb065254 100644 --- a/authentik/core/migrations/0046_session_and_more.py +++ b/authentik/core/migrations/0046_session_and_more.py @@ -7,15 +7,10 @@ from django.contrib.auth import BACKEND_SESSION_KEY, HASH_SESSION_KEY, SESSION_K from django.db import migrations, models import django.db.models.deletion from django.conf import settings -from django.contrib.sessions.backends.cache import KEY_PREFIX -from django.utils.timezone import now, timedelta from authentik.lib.migrations import progress_bar from authentik.root.middleware import ClientIPMiddleware -SESSION_CACHE_ALIAS = "default" - - class PickleSerializer: """ Simple wrapper around pickle to be used in signing.dumps()/loads() and @@ -83,27 +78,6 @@ def _migrate_session( ) -def migrate_redis_sessions(apps, schema_editor): - from django.core.cache import caches - - db_alias = schema_editor.connection.alias - cache = caches[SESSION_CACHE_ALIAS] - - # Not a redis cache, skipping - if not hasattr(cache, "keys"): - return - - print("\nMigrating Redis sessions to database, this might take a couple of minutes...") - for key, session_data in progress_bar(cache.get_many(cache.keys(f"{KEY_PREFIX}*")).items()): - _migrate_session( - apps=apps, - db_alias=db_alias, - session_key=key.removeprefix(KEY_PREFIX), - session_data=session_data, - expires=now() + timedelta(seconds=cache.ttl(key)), - ) - - def migrate_database_sessions(apps, schema_editor): DjangoSession = apps.get_model("sessions", "Session") db_alias = schema_editor.connection.alias @@ -231,10 +205,6 @@ class Migration(migrations.Migration): "verbose_name_plural": "Authenticated Sessions", }, ), - migrations.RunPython( - code=migrate_redis_sessions, - reverse_code=migrations.RunPython.noop, - ), migrations.RunPython( code=migrate_database_sessions, reverse_code=migrations.RunPython.noop, diff --git a/authentik/core/tasks.py b/authentik/core/tasks.py index 91cf5313ad..833822cf49 100644 --- a/authentik/core/tasks.py +++ b/authentik/core/tasks.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ +from django_postgres_cache.tasks import clear_expired_cache from dramatiq.actor import actor from structlog.stdlib import get_logger @@ -32,6 +33,7 @@ def clean_expired_models(): obj.expire_action() LOGGER.debug("Expired models", model=cls, amount=amount) self.info(f"Expired {amount} {cls._meta.verbose_name_plural}") + clear_expired_cache() @actor(description=_("Remove temporary users created by SAML Sources.")) diff --git a/authentik/lib/sentry.py b/authentik/lib/sentry.py index 3b7f93c7a9..17b8959421 100644 --- a/authentik/lib/sentry.py +++ b/authentik/lib/sentry.py @@ -7,14 +7,11 @@ from django.conf import settings from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation, ValidationError from django.db import DatabaseError, InternalError, OperationalError, ProgrammingError from django.http.response import Http404 -from django_redis.exceptions import ConnectionInterrupted from docker.errors import DockerException from dramatiq.errors import Retry from h11 import LocalProtocolError from ldap3.core.exceptions import LDAPException from psycopg.errors import Error -from redis.exceptions import ConnectionError as RedisConnectionError -from redis.exceptions import RedisError, ResponseError from rest_framework.exceptions import APIException from sentry_sdk import HttpTransport, get_current_scope from sentry_sdk import init as sentry_sdk_init @@ -22,7 +19,6 @@ from sentry_sdk.api import set_tag from sentry_sdk.integrations.argv import ArgvIntegration from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.dramatiq import DramatiqIntegration -from sentry_sdk.integrations.redis import RedisIntegration from sentry_sdk.integrations.socket import SocketIntegration from sentry_sdk.integrations.stdlib import StdlibIntegration from sentry_sdk.integrations.threading import ThreadingIntegration @@ -58,11 +54,6 @@ ignored_classes = ( ProgrammingError, SuspiciousOperation, ValidationError, - # Redis errors - RedisConnectionError, - ConnectionInterrupted, - RedisError, - ResponseError, # websocket errors WebSocketException, LocalProtocolError, @@ -110,7 +101,6 @@ def sentry_init(**sentry_init_kwargs): ArgvIntegration(), DjangoIntegration(transaction_style="function_name", cache_spans=True), DramatiqIntegration(), - RedisIntegration(), SocketIntegration(), StdlibIntegration(), ThreadingIntegration(propagate_hub=True), @@ -157,9 +147,7 @@ def before_send(event: dict, hint: dict) -> dict | None: if event["logger"] in [ "asyncio", "multiprocessing", - "django_redis", "django.security.DisallowedHost", - "django_redis.cache", "paramiko.transport", ]: return None diff --git a/authentik/root/monitoring.py b/authentik/root/monitoring.py index f038778b73..890e3d6061 100644 --- a/authentik/root/monitoring.py +++ b/authentik/root/monitoring.py @@ -11,8 +11,6 @@ from django.dispatch import Signal from django.http import HttpRequest, HttpResponse from django.views import View from django_prometheus.exports import ExportToDjangoView -from django_redis import get_redis_connection -from redis.exceptions import RedisError monitoring_set = Signal() @@ -44,19 +42,17 @@ class LiveView(View): class ReadyView(View): - """View for readiness probe, always returns Http 200, unless sql or redis is down""" + """View for readiness probe, always returns Http 200, unless sql is down""" + + def check_db(self): + for db_conn in connections.all(): + # Force connection reload + db_conn.connect() + _ = db_conn.cursor() def dispatch(self, request: HttpRequest) -> HttpResponse: try: - for db_conn in connections.all(): - # Force connection reload - db_conn.connect() - _ = db_conn.cursor() + self.check_db() except OperationalError: # pragma: no cover return HttpResponse(status=503) - try: - redis_conn = get_redis_connection() - redis_conn.ping() - except RedisError: # pragma: no cover - return HttpResponse(status=503) return HttpResponse(status=200) diff --git a/authentik/root/settings.py b/authentik/root/settings.py index 28ffe141d7..080001f9e6 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -10,7 +10,7 @@ from sentry_sdk import set_tag from xmlsec import enable_debug_trace from authentik import authentik_version -from authentik.lib.config import CONFIG, django_db_config, redis_url +from authentik.lib.config import CONFIG, django_db_config from authentik.lib.logging import get_logger_config, structlog_configure from authentik.lib.sentry import sentry_init from authentik.lib.utils.reflection import get_env @@ -73,6 +73,7 @@ TENANT_APPS = [ "django.contrib.contenttypes", "django.contrib.sessions", "pgtrigger", + "django_postgres_cache", "authentik.admin", "authentik.api", "authentik.core", @@ -227,20 +228,11 @@ REST_FRAMEWORK = { CACHES = { "default": { - "BACKEND": "django_redis.cache.RedisCache", - "LOCATION": CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db")), - "TIMEOUT": CONFIG.get_int("cache.timeout", 300), - "OPTIONS": { - "CLIENT_CLASS": "django_redis.client.DefaultClient", - }, - "KEY_PREFIX": "authentik_cache", + "BACKEND": "django_postgres_cache.backend.DatabaseCache", "KEY_FUNCTION": "django_tenants.cache.make_key", "REVERSE_KEY_FUNCTION": "django_tenants.cache.reverse_key", } } -DJANGO_REDIS_SCAN_ITERSIZE = 1000 -DJANGO_REDIS_IGNORE_EXCEPTIONS = True -DJANGO_REDIS_LOG_IGNORED_EXCEPTIONS = True SESSION_ENGINE = "authentik.core.sessions" # Configured via custom SessionMiddleware # SESSION_COOKIE_SAMESITE = "None" diff --git a/authentik/tasks/middleware.py b/authentik/tasks/middleware.py index 0d6e3bd98a..87ab3cb498 100644 --- a/authentik/tasks/middleware.py +++ b/authentik/tasks/middleware.py @@ -13,12 +13,10 @@ from django_dramatiq_postgres.middleware import HTTPServer from django_dramatiq_postgres.middleware import ( MetricsMiddleware as BaseMetricsMiddleware, ) -from django_redis import get_redis_connection from dramatiq.broker import Broker from dramatiq.message import Message from dramatiq.middleware import Middleware from psycopg.errors import Error -from redis.exceptions import RedisError from structlog.stdlib import get_logger from authentik import authentik_full_version @@ -31,7 +29,7 @@ from authentik.tenants.utils import get_current_tenant LOGGER = get_logger() HEALTHCHECK_LOGGER = get_logger("authentik.worker").bind() -DB_ERRORS = (OperationalError, Error, RedisError) +DB_ERRORS = (OperationalError, Error) class CurrentTask(BaseCurrentTask): @@ -188,8 +186,6 @@ class _healthcheck_handler(BaseHTTPRequestHandler): # Force connection reload db_conn.connect() _ = db_conn.cursor() - redis_conn = get_redis_connection() - redis_conn.ping() self.send_response(200) except DB_ERRORS: # pragma: no cover self.send_response(503) diff --git a/lifecycle/system_migrations/to_0_13_authentik.py b/lifecycle/system_migrations/to_0_13_authentik.py index c56d3b2d1f..1e368ec4a2 100644 --- a/lifecycle/system_migrations/to_0_13_authentik.py +++ b/lifecycle/system_migrations/to_0_13_authentik.py @@ -1,7 +1,5 @@ # flake8: noqa -from redis import Redis -from authentik.lib.config import CONFIG from lifecycle.migrate import BaseMigration SQL_STATEMENT = """BEGIN TRANSACTION; @@ -106,17 +104,3 @@ class Migration(BaseMigration): def run(self): with self.con.transaction(): self.cur.execute(SQL_STATEMENT) - # We also need to clean the cache to make sure no pickeled objects still exist - for db in [ - CONFIG.get("redis.message_queue_db"), - CONFIG.get("redis.cache_db"), - CONFIG.get("redis.ws_db"), - ]: - redis = Redis( - host=CONFIG.get("redis.host"), - port=6379, - db=db, - username=CONFIG.get("redis.username"), - password=CONFIG.get("redis.password"), - ) - redis.flushall() diff --git a/lifecycle/wait_for_db.py b/lifecycle/wait_for_db.py index 2c193c24c4..e0d8c1860e 100755 --- a/lifecycle/wait_for_db.py +++ b/lifecycle/wait_for_db.py @@ -1,14 +1,13 @@ #!/usr/bin/env python """This file needs to be run from the root of the project to correctly import authentik. This is done by the dockerfile.""" + from sys import exit as sysexit from time import sleep from psycopg import OperationalError, connect -from redis import Redis -from redis.exceptions import RedisError -from authentik.lib.config import CONFIG, redis_url +from authentik.lib.config import CONFIG CHECK_THRESHOLD = 30 @@ -40,24 +39,6 @@ def check_postgres(): CONFIG.log("info", "PostgreSQL connection successful") -def check_redis(): - url = CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db")) - attempt = 0 - while True: - if attempt >= CHECK_THRESHOLD: - sysexit(1) - try: - redis = Redis.from_url(url) - redis.ping() - break - except RedisError as exc: - sleep(1) - CONFIG.log("info", f"Redis Connection failed, retrying... ({exc})") - finally: - attempt += 1 - CONFIG.log("info", "Redis Connection successful") - - def wait_for_db(): CONFIG.log("info", "Starting authentik bootstrap") # Sanity check, ensure SECRET_KEY is set before we even check for database connectivity @@ -69,7 +50,6 @@ def wait_for_db(): CONFIG.log("info", "----------------------------------------------------------------------") sysexit(1) check_postgres() - check_redis() CONFIG.log("info", "Finished authentik bootstrap") diff --git a/packages/django-dramatiq-postgres/django_dramatiq_postgres/apps.py b/packages/django-dramatiq-postgres/django_dramatiq_postgres/apps.py index 7e7277a8f3..24a9ddffd0 100644 --- a/packages/django-dramatiq-postgres/django_dramatiq_postgres/apps.py +++ b/packages/django-dramatiq-postgres/django_dramatiq_postgres/apps.py @@ -12,9 +12,12 @@ class DjangoDramatiqPostgres(AppConfig): verbose_name = "Django Dramatiq postgres" def ready(self) -> None: - old_broker = dramatiq.get_broker() + try: + old_broker = dramatiq.get_broker() + except ModuleNotFoundError: + old_broker = None - if len(old_broker.actors) != 0: + if old_broker is not None and len(old_broker.actors) != 0: raise ImproperlyConfigured( "Actors were previously registered. " "Make sure your actors are not imported too early." diff --git a/packages/django-postgres-cache/README.md b/packages/django-postgres-cache/README.md new file mode 100644 index 0000000000..68abe12fe1 --- /dev/null +++ b/packages/django-postgres-cache/README.md @@ -0,0 +1,12 @@ +# django-postgres-cache + +### Use in migrations + +Migrations that use the cache with this installed need to depend on the migration to create the cache entry table: + +```python + dependencies = [ + # ...other requirements + ("django_postgres_cache", "0001_initial"), + ] +``` diff --git a/packages/django-postgres-cache/django_postgres_cache/__init__.py b/packages/django-postgres-cache/django_postgres_cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/django-postgres-cache/django_postgres_cache/apps.py b/packages/django-postgres-cache/django_postgres_cache/apps.py new file mode 100644 index 0000000000..c0be47b92a --- /dev/null +++ b/packages/django-postgres-cache/django_postgres_cache/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DjangoPostgresCache(AppConfig): + name = "django_postgres_cache" + verbose_name = "Django Postgres cache" diff --git a/packages/django-postgres-cache/django_postgres_cache/backend.py b/packages/django-postgres-cache/django_postgres_cache/backend.py new file mode 100644 index 0000000000..11a269d647 --- /dev/null +++ b/packages/django-postgres-cache/django_postgres_cache/backend.py @@ -0,0 +1,51 @@ +from typing import Any + +from django.core.cache.backends.db import DatabaseCache as BaseDatabaseCache +from django.db.utils import ProgrammingError +from django.utils.module_loading import import_string +from django.utils.timezone import now + +from django_postgres_cache.models import CacheEntry + + +class DatabaseCache(BaseDatabaseCache): + + def __init__(self, table: str, params: dict[str, Any]) -> None: + super().__init__(table, params) + self.reverse_key_func = import_string(params["REVERSE_KEY_FUNCTION"]) + self._table = CacheEntry._meta.db_table + self.cache_model_class = CacheEntry + + def _cull(self, *args: Any, **kwargs: Any) -> None: + """Stubbed out cull method as we cull in a background task""" + pass + + def get(self, key: str, default: Any | None = None, version: int | None = None) -> Any: + try: + return super().get(key, default=default, version=version) + except ProgrammingError: + return default + + def keys(self, keys_pattern: str, version: int | None = None) -> list[str]: + try: + return self._keys(keys_pattern, version=version) + except ProgrammingError: + return [] + + def _keys(self, keys_pattern: str, version: int | None = None) -> list[str]: + keys_pattern = self.make_key(keys_pattern.replace("*", ".*"), version=version) + + return [ + self.reverse_key_func(key) + for key in CacheEntry.objects.filter(cache_key__regex=keys_pattern).values_list( + "cache_key", flat=True + ) + ] + + def ttl(self, key: str, version: int | None = None) -> int | None: + """Get TTL left for a given key and version""" + key = self.make_and_validate_key(key, version=version) + entry = CacheEntry.objects.filter(cache_key=key).first() + if not entry: + return None + return int((entry.expires - now()).total_seconds()) diff --git a/packages/django-postgres-cache/django_postgres_cache/migrations/0001_initial.py b/packages/django-postgres-cache/django_postgres_cache/migrations/0001_initial.py new file mode 100644 index 0000000000..e2fb2a8a14 --- /dev/null +++ b/packages/django-postgres-cache/django_postgres_cache/migrations/0001_initial.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.12 on 2025-09-06 16:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="CacheEntry", + fields=[ + ("cache_key", models.TextField(primary_key=True, serialize=False)), + ("value", models.TextField()), + ("expires", models.DateTimeField(db_index=True)), + ], + options={ + "default_permissions": [], + }, + ), + ] diff --git a/packages/django-postgres-cache/django_postgres_cache/migrations/__init__.py b/packages/django-postgres-cache/django_postgres_cache/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/django-postgres-cache/django_postgres_cache/models.py b/packages/django-postgres-cache/django_postgres_cache/models.py new file mode 100644 index 0000000000..caefe2555c --- /dev/null +++ b/packages/django-postgres-cache/django_postgres_cache/models.py @@ -0,0 +1,14 @@ +from django.db import models + + +class CacheEntry(models.Model): + + cache_key = models.TextField(primary_key=True) + value = models.TextField() + expires = models.DateTimeField(db_index=True) + + class Meta: + default_permissions = [] + + def __str__(self) -> str: + return f"Cache entry '{self.cache_key}'" diff --git a/packages/django-postgres-cache/django_postgres_cache/tasks.py b/packages/django-postgres-cache/django_postgres_cache/tasks.py new file mode 100644 index 0000000000..ecfa9a238a --- /dev/null +++ b/packages/django-postgres-cache/django_postgres_cache/tasks.py @@ -0,0 +1,11 @@ +from django.utils.timezone import now + +from authentik.lib.utils.db import chunked_queryset +from django_postgres_cache.models import CacheEntry + + +def clear_expired_cache() -> None: + # FIXME: this is currently imported from the main project, + # do we copy it here to make it independent? + for obj in chunked_queryset(CacheEntry.objects.filter(expires__lt=now())): + obj.delete() diff --git a/packages/django-postgres-cache/pyproject.toml b/packages/django-postgres-cache/pyproject.toml new file mode 100644 index 0000000000..71e8f65ef3 --- /dev/null +++ b/packages/django-postgres-cache/pyproject.toml @@ -0,0 +1,46 @@ +[project] +name = "django-postgres-cache" +version = "0.1.0" +description = "Improved Django Postgres Cache" +requires-python = ">=3.9,<3.14" +readme = "README.md" +license = "MIT" +authors = [{ name = "Authentik Security Inc.", email = "hello@goauthentik.io" }] +keywords = ["django", "cache", "postgres"] + +classifiers = [ + "Development Status :: 3 - Alpha", + "Environment :: Web Environment", + "Framework :: Django", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", + "Framework :: Django :: 5.2", + "Intended Audience :: Developers", + "Operating System :: MacOS", + "Operating System :: POSIX", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", +] + +dependencies = [ + "django >=4.2,<6.0", +] + +[project.urls] +Homepage = "https://github.com/goauthentik/authentik/tree/main/packages/django-postgres-cache" +Documentation = "https://github.com/goauthentik/authentik/tree/main/packages/django-postgres-cache" +Repository = "https://github.com/goauthentik/authentik/tree/main/packages/django-postgres-cache" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.setuptools.packages] +find = {} diff --git a/pyproject.toml b/pyproject.toml index c03681f0a7..341da4cc27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,13 +16,13 @@ dependencies = [ "django-countries==7.6.1", "django-cte==2.0.0", "django-dramatiq-postgres", + "django-postgres-cache", "django-filter==25.1", "django-guardian==3.0.3", "django-model-utils==5.0.0", "django-pglock==1.7.2", "django-pgtrigger==4.15.2", "django-prometheus==2.4.1", - "django-redis==6.0.0", "django-storages[s3]==1.14.6", "django-tenants==3.8.0", "djangoql==0.18.1", @@ -120,11 +120,15 @@ no-binary-package = [ [tool.uv.sources] djangorestframework = { git = "https://github.com/goauthentik/django-rest-framework", rev = "896722bab969fabc74a08b827da59409cf9f1a4e" } django-dramatiq-postgres = { workspace = true } +django-postgres-cache = { workspace = true } opencontainers = { git = "https://github.com/vsoch/oci-python", rev = "ceb4fcc090851717a3069d78e85ceb1e86c2740c" } channels-postgres = { git = "https://github.com/rissson/channels_postgres", rev = "93ed24e3c5317d7ccf7e8b1ce913c0f365d1728f" } [tool.uv.workspace] -members = ["packages/django-dramatiq-postgres"] +members = [ + "packages/django-dramatiq-postgres", + "packages/django-postgres-cache", +] [project.scripts] ak = "lifecycle.ak:main" diff --git a/uv.lock b/uv.lock index 3abd6e849c..1307b10341 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,7 @@ requires-python = "==3.13.*" members = [ "authentik", "django-dramatiq-postgres", + "django-postgres-cache", ] [[package]] @@ -178,8 +179,8 @@ dependencies = [ { name = "django-model-utils" }, { name = "django-pglock" }, { name = "django-pgtrigger" }, + { name = "django-postgres-cache" }, { name = "django-prometheus" }, - { name = "django-redis" }, { name = "django-storages", extra = ["s3"] }, { name = "django-tenants" }, { name = "djangoql" }, @@ -280,8 +281,8 @@ requires-dist = [ { name = "django-model-utils", specifier = "==5.0.0" }, { name = "django-pglock", specifier = "==1.7.2" }, { name = "django-pgtrigger", specifier = "==4.15.2" }, + { name = "django-postgres-cache", editable = "packages/django-postgres-cache" }, { name = "django-prometheus", specifier = "==2.4.1" }, - { name = "django-redis", specifier = "==6.0.0" }, { name = "django-storages", extras = ["s3"], specifier = "==1.14.6" }, { name = "django-tenants", specifier = "==3.8.0" }, { name = "djangoql", specifier = "==0.18.1" }, @@ -1041,6 +1042,17 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/97/81a4779e73f09d347d006b866b85b4be7774543d9f6eb6fe49a1303802af/django_pgtrigger-4.15.2-py3-none-any.whl", hash = "sha256:77ac6bd44fe5df0307e2630e3294cc68dcb25c9ec1a6bb523667546d28c80bea", size = 36434, upload-time = "2025-04-29T20:38:20.7Z" }, ] +[[package]] +name = "django-postgres-cache" +version = "0.1.0" +source = { editable = "packages/django-postgres-cache" } +dependencies = [ + { name = "django" }, +] + +[package.metadata] +requires-dist = [{ name = "django", specifier = ">=4.2,<6.0" }] + [[package]] name = "django-prometheus" version = "2.4.1" @@ -1054,19 +1066,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/50/9c5e022fa92574e5d20606687f15a2aa255e10512a17d11a8216fa117f72/django_prometheus-2.4.1-py2.py3-none-any.whl", hash = "sha256:7fe5af7f7c9ad9cd8a429fe0f3f1bf651f0e244f77162147869eab7ec09cc5e7", size = 29541, upload-time = "2025-06-25T15:45:35.433Z" }, ] -[[package]] -name = "django-redis" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "django" }, - { name = "redis" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/08/53/dbcfa1e528e0d6c39947092625b2c89274b5d88f14d357cee53c4d6dbbd4/django_redis-6.0.0.tar.gz", hash = "sha256:2d9cb12a20424a4c4dde082c6122f486628bae2d9c2bee4c0126a4de7fda00dd", size = 56904, upload-time = "2025-06-17T18:15:46.376Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/79/055dfcc508cfe9f439d9f453741188d633efa9eab90fc78a67b0ab50b137/django_redis-6.0.0-py3-none-any.whl", hash = "sha256:20bf0063a8abee567eb5f77f375143c32810c8700c0674ced34737f8de4e36c0", size = 33687, upload-time = "2025-06-17T18:15:34.165Z" }, -] - [[package]] name = "django-storages" version = "1.14.6"