TBH the question was mostly rhetorical
Just a silly lament 
I wasn’t expecting an actual answer 
That said, your suggestion certainly has legs. In theory, the reason why this should not be the explanation is that the whole operation of adding the each person to the challenge then asking how many people are in it, and deciding to start it, is wrapped in an atomic operation, so there should not be able to be two progressing “at the same time”.
details
… if you can spot the flaw, that’d be awesome…
class ChallengeJoin(views.APIView):
# Here a user is trying to join an open rengo challenge
@OGSAtomic
def put(self, request, *args, **kwargs):
if request.user.is_banned:
return Response({"error": "Account banned"}, status=status.HTTP_403_FORBIDDEN)
challenge = OpenChallenge.objects.filter(id=kwargs["pk"])
if not challenge.exists():
return Response({"error": "Challenge does not exist"}, status=status.HTTP_404_NOT_FOUND)
challenge = challenge[0]
if not request.user.is_moderator and challenge.challenger.is_game_blocked(request.user):
return Response(
{"error": _("Not allowed to accept this player's challenge")}, status=status.HTTP_403_FORBIDDEN
)
request_user_rank = floor(request.user.getBoundedRank())
if challenge.min_ranking is not None and challenge.min_ranking > request_user_rank:
return Response(
{"error": "Your rank is too low to accept this challenge"}, status=status.HTTP_403_FORBIDDEN
)
if challenge.max_ranking is not None and challenge.max_ranking < request_user_rank:
return Response(
{"error": "Your rank is too high to accept this challenge"}, status=status.HTTP_403_FORBIDDEN
)
try:
result = challenge.add_rengo_player(request.user)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)
# Dump other live game challenges from this user
if challenge.game.time_per_move <= 3600 and challenge.game.time_per_move > 0:
for C in OpenChallenge.objects.filter(
challenger=request.user, game__time_per_move__lte=3600, game__time_per_move__gt=0
):
C.delete()
if (
challenge.game.rengo_casual_mode
and challenge.rengo_auto_start
and (challenge.rengo_white_team.count() + challenge.rengo_black_team.count()) >= challenge.rengo_auto_start
):
id = challenge.id
game = startRengoChallenge(challenge) # note that this closes the challenge
if game:
# politely return an empty rengo_participants_DTO, in case the client is looking for fields
return Response(
{"challenge": id, "rengo_nominees": [], "rengo_black_team": [], "rengo_white_team": []},
status=status.HTTP_202_ACCEPTED,
)
else:
return Response(
{"error": "Unexpected result when auto-starting game"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
return Response(rengo_participants_DTO(challenge), status=status.HTTP_202_ACCEPTED)
OGSAtomic
is a thin wrapper around the django Atomic
class:
"""
Guarantee the atomic execution of a given block.
An instance can be used either as a decorator or as a context manager.
When it's used as a decorator, __call__ wraps the execution of the
decorated function in the instance itself, used as a context manager.
When it's used as a context manager, __enter__ creates a transaction or a
savepoint, depending on whether a transaction is already in progress, and
__exit__ commits the transaction or releases the savepoint on normal exit,
and rolls back the transaction or to the savepoint on exceptions.
It's possible to disable the creation of savepoints if the goal is to
ensure that some code runs within a transaction without creating overhead.
A stack of savepoints identifiers is maintained as an attribute of the
connection. None denotes the absence of a savepoint.
This allows reentrancy even if the same AtomicWrapper is reused. For
example, it's possible to define `oa = atomic('other')` and use `@oa` or
`with oa:` multiple times.
Since database connections are thread-local, this is thread-safe.
…
I’m totally open to debugging help … keep the thoughts flowing!