core: survive the empty-queryset race in chunked_queryset (#21666)

This commit is contained in:
Sai Asish Y
2026-04-23 06:21:57 -07:00
committed by GitHub
parent 0f8d8c81d7
commit 92a2d26c86

View File

@@ -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]