From 038d611d9ea67f7c59c2400aaabd6c046683fb0e Mon Sep 17 00:00:00 2001 From: Dominic R Date: Wed, 7 Jan 2026 20:41:07 -0500 Subject: [PATCH] core: fix read replica routing during transactions (#19086) * core: fix transactional app creation failing with read replicas When authentik is configured with pg read replicas, the application wizard fails with "Invalid pk - object does not exist" for the provider field. The issue occurs in the blueprint validation flow: 1. Provider is created on the primary database (e.g PK 159) 2. KeyOf.resolve() returns this PK for the application's provider field 3. ApplicationSerializer.is_valid() validates the provider FK 4. DRF's PrimaryKeyRelatedField queries to verify the PK exists 5. FailoverRouter routes this read to a replica 6. Replica hasn't replicated the new provider yet --> validation fails Number 6 happens because the transaction has not been commited yet cause blueprint validation runs in transaction_rollback() The fix introduces TransactionApplicationRequestSerializer which excludes provider-related fields (provider, provider_obj, backchannel_providers, backchannel_providers_obj) from validation. This is safe because: - The provider is created in the same blueprint transaction - The KeyOf reference correctly links them during blueprint apply() - The blueprint importer handles the actual FK assignment * wip * wip * wip * wip * wip * wip --- authentik/tenants/db.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/authentik/tenants/db.py b/authentik/tenants/db.py index e6735a01def5..2a7563f58cfd 100644 --- a/authentik/tenants/db.py +++ b/authentik/tenants/db.py @@ -1,6 +1,7 @@ from random import choice from django.conf import settings +from django.db import DEFAULT_DB_ALIAS, connections class FailoverRouter: @@ -10,16 +11,22 @@ class FailoverRouter: def __init__(self) -> None: super().__init__() self.database_aliases = set(settings.DATABASES.keys()) - self.read_replica_aliases = list(self.database_aliases - {"default"}) + self.read_replica_aliases = list(self.database_aliases - {DEFAULT_DB_ALIAS}) self.replica_enabled = len(self.read_replica_aliases) > 0 def db_for_read(self, model, **hints): if not self.replica_enabled: - return "default" + return DEFAULT_DB_ALIAS + # Stay on primary for the entire transaction to maintain consistency. + # Reading from a replica mid-transaction would give a different snapshot, + # breaking transactional semantics (not just read-your-writes, but the + # entire consistent point-in-time view that a transaction provides). + if connections[DEFAULT_DB_ALIAS].in_atomic_block: + return DEFAULT_DB_ALIAS return choice(self.read_replica_aliases) # nosec def db_for_write(self, model, **hints): - return "default" + return DEFAULT_DB_ALIAS def allow_relation(self, obj1, obj2, **hints): """Relations between objects are allowed if both objects are