I am trying to work out how to allocate exactly N available objects, in a way which is safe against concurrent activity. Is there a way to write a queryset that will fail if it cannot return at least N objects?
This is probably an excess of caution for my particular application because the chances of a race are infinitesimal, but I'm curious
What I know won't work is
available = Foo.objects.filter( status=Foo.AVAILABLE).count()
if available < N:
#let the user know there aren't enough left
# but now something concurrent may grab them!
foo_qs = Foo.objects.select_for_update().filter(status=Foo.AVAILABLE)[:N]
with transaction.atomic():
for foo in foo_qs:
... # quite a bit. In RL I will have locked related objects as well.
foo.status=Foo.RESERVED
foo.save()
because slicing a queryset guarantees only no more than N objects.
Removing the slice might lock a large number of rows that I don't need to lock. Is this inefficient? The whole Foo table won't be locked for long because I update only N objects and then exit the transaction.
Is the only answer to grab the objects one at a time inside the transaction, or to get all of them as a list and re-check its length?
with transaction.atomic():
foos = list( foo_qs)
if len(foos) < N:
raise ... # fail transaction
for foo in foos:
... # as before