diff --git a/api/utils/keycloak_utils.py b/api/utils/keycloak_utils.py index d6f4f46..d1a6ffc 100644 --- a/api/utils/keycloak_utils.py +++ b/api/utils/keycloak_utils.py @@ -86,7 +86,11 @@ def validate_token( from authorization.models import User user = User.objects.get(id=user_id) - user.save() + # Deliberately no user.save() here. It rewrote every + # column of an unchanged row on every authenticated + # request, taking a row lock for no benefit - and since + # concurrent requests for one user contend on that single + # row, it serialized them against each other. # NOTE: Organizations are managed in DataSpace database, not Keycloak # Organization memberships should be created/managed through DataSpace's @@ -412,13 +416,35 @@ def sync_user_from_keycloak( ) if user: - # Update existing user - user.keycloak_id = keycloak_id - user.username = username - user.email = email - user.first_name = user_info.get("given_name", "") or user.first_name - user.last_name = user_info.get("family_name", "") or user.last_name - user.is_active = True + # Update existing user, but only write when something actually + # changed. The unconditional save this replaces was the cause + # of the connection exhaustion: every login rewrote the same + # row, so concurrent logins for one user queued on a row lock + # (pg_stat_activity showed "Lock: tuple" and + # "Lock: transactionid" on UPDATE "ds_user"), each holding a + # database connection while it waited. + desired = { + "keycloak_id": keycloak_id, + "username": username, + "email": email, + "first_name": user_info.get("given_name", "") or user.first_name, + "last_name": user_info.get("family_name", "") or user.last_name, + "is_active": True, + "is_staff": "admin" in roles, + "is_superuser": "admin" in roles, + } + changed = [ + field + for field, value in desired.items() + if getattr(user, field) != value + ] + if changed: + for field in changed: + setattr(user, field, desired[field]) + # update_fields keeps the UPDATE narrow instead of + # rewriting every column. + user.save(update_fields=changed) + return user else: # Create new user user = User(