diff --git a/authentik/lib/utils/db.py b/authentik/lib/utils/db.py index ef72d76652..9d40bea795 100644 --- a/authentik/lib/utils/db.py +++ b/authentik/lib/utils/db.py @@ -14,7 +14,16 @@ def chunked_queryset[T: Model](queryset: QuerySet[T], chunk_size: int = 1_000) - def get_chunks(qs: QuerySet) -> Generator[QuerySet[T]]: qs = qs.order_by("pk") pks = qs.values_list("pk", flat=True) - start_pk = pks[0] + # The outer queryset.exists() guard can race with a concurrent + # transaction that deletes the last matching row (or with a + # different isolation-level snapshot), so by the time this + # generator starts iterating the queryset may be empty and + # pks[0] would raise IndexError and crash the caller. Using + # .first() returns None on an empty queryset, which we bail + # out on cleanly. See goauthentik/authentik#21643. + start_pk = pks.first() + if start_pk is None: + return while True: try: end_pk = pks.filter(pk__gte=start_pk)[chunk_size]