diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c389ca8c9cbbe142bd7fa67ffa37642a1ff18e76..9e27918ef1db7075daefd321dba9d175d83206c8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,12 @@ Changed * [Dev] The (undocumented) setting PDF_CONTEXT_PROCESSORS is now named NON_REQUEST_CONTEXT_PROCESSORS * [Docker] Cache is now cleared if migrations are applied +Fixed +~~~~~ + +* Celery progress could be inaccurate if recording progress during a transaction + + `2.7.1`_ - 2022-01-28 --------------------- diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index b380f0dd86f1e0b4df2839191bfb23612f1d9412..6d1302e831a0d75f70f55818c8fa43156a8184d9 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -227,6 +227,13 @@ DATABASES = { } } +# Duplicate default database for out-of-transaction updates +DATABASES["default_oot"] = DATABASES["default"].copy() +DATABASE_ROUTERS = [ + "aleksis.core.util.core_helpers.OOTRouter", +] +DATABASE_OOT_LABELS = ["django_celery_results"] + merge_app_settings("DATABASES", DATABASES, False) REDIS_HOST = _settings.get("redis.host", "localhost") diff --git a/aleksis/core/util/core_helpers.py b/aleksis/core/util/core_helpers.py index 55085d91031a1c591f71e4b8712b1cc7f3178cef..fc83b605032013b7d0dbee06ce0b1ef8193c177d 100644 --- a/aleksis/core/util/core_helpers.py +++ b/aleksis/core/util/core_helpers.py @@ -390,3 +390,54 @@ def create_default_celery_schedule(): PeriodicTask.objects.create( name=f"{name} (default schedule)", task=name, **{attr: db_schedule} ) + + +class OOTRouter: + """Database router for operations that should run out of transaction. + + This router routes database operations for certain apps through + the separate default_oot connection, to ensure that data get + updated immediately even during atomic transactions. + """ + + default_db = "default" + oot_db = "default_oot" + + @property + def oot_labels(self): + return settings.DATABASE_OOT_LABELS + + @property + def default_dbs(self): + return set((self.default_db, self.oot_db)) + + def is_same_db(self, db1: str, db2: str): + return set((db1, db2)).issubset(self.default_dbs) + + def db_for_read(self, model: Model, **hints) -> Optional[str]: + if model._meta.app_label in self.oot_labels: + return self.oot_db + + return None + + def db_for_write(self, model: Model, **hints) -> Optional[str]: + return self.db_for_read(model, **hints) + + def allow_relation(self, obj1: Model, obj2: Model, **hints) -> Optional[bool]: + # Allow relations between default database and OOT connection + # They are the same database + if self.is_same_db(obj1._state.db, obj2._state.db): + return True + + return None + + def allow_migrate( + self, db: str, app_label: str, model_name: Optional[str] = None, **hints + ) -> Optional[bool]: + # Never allow any migrations on the default_oot database + # It connects to the same database as default, so everything + # migrated there + if db == self.oot_db: + return False + + return None