def _register_wait_page_visit(self): if otree.common_internal.USE_REDIS: lock = redis_lock.Lock(otree.common_internal.get_redis_conn(), self.get_channels_group_name(), expire=60, auto_renewal=True) else: lock = global_lock() with lock: unvisited_participants = self._tally_unvisited() if unvisited_participants: return try: if self.wait_for_all_groups: completion = CompletedSubsessionWaitPage( page_index=self._index_in_pages, session=self.session) else: completion = CompletedGroupWaitPage( page_index=self._index_in_pages, group_pk=self.group.pk, session=self.session) completion.save() return completion # if the record already exists # (enforced through unique_together) except django.db.IntegrityError: return
def _mark_completed_and_notify(self, group: Optional[BaseGroup]): # if group is not passed, then it's the whole subsession # could be 2 people creating the record at the same time # in _increment_index_in_pages, so could end up creating 2 records # but it's not a problem. base_kwargs = dict(page_index=self._index_in_pages, session_id=self._session_pk) Player = self.PlayerClass if self.wait_for_all_groups: CompletedSubsessionWaitPage.objects_create(**base_kwargs) elif self.group_by_arrival_time: db.add( CompletedGBATWaitPage( **base_kwargs, id_in_subsession=group.id_in_subsession ) ) else: db.add(CompletedGroupWaitPage(**base_kwargs, group_id=group.id)) participants = self._get_participants_for_this_waitpage() self._mark_page_completions(list(participants)) for pp in participants: pp._last_page_timestamp = time.time() # this can cause messages to get wrongly enqueued in the botworker if otree.common.USE_TIMEOUT_WORKER and not self.participant.is_browser_bot: # 2016-11-15: we used to only ensure the next page is visited # if the next page has a timeout, or if it's a wait page # but this is not reliable because next page might be skipped anyway, # and we don't know what page will actually be shown next to the user. otree.tasks.ensure_pages_visited( participant_pks=[pp.id for pp in participants], delay=10, ) if self.group_by_arrival_time: channel_utils.sync_group_send( group=channel_utils.gbat_group_name(**base_kwargs), data={'status': 'ready'}, ) else: if self.wait_for_all_groups: channels_group_name = channel_utils.subsession_wait_page_name( **base_kwargs ) else: channels_group_name = channel_utils.group_wait_page_name( **base_kwargs, group_id=group.id ) channel_utils.sync_group_send( group=channels_group_name, data={'status': 'ready'} )
def inner_dispatch_group(self): ## EARLY EXITS if CompletedGroupWaitPage.objects_exists( page_index=self._index_in_pages, group_id=self.player.group_id, session_id=self._session_pk, ): return self._response_when_ready() is_displayed = self._is_displayed() is_last, someone_waiting = self._tally_unvisited() if is_displayed and not is_last: return self._get_wait_page() elif is_last and (someone_waiting or is_displayed): self._run_aapa_and_notify(self.group) return self._response_when_ready()
def _register_wait_page_visit(self): with global_lock(): unvisited_participants = self._tally_unvisited() if unvisited_participants: return try: if self.wait_for_all_groups: completion = CompletedSubsessionWaitPage( page_index=self._index_in_pages, session=self.session) else: completion = CompletedGroupWaitPage( page_index=self._index_in_pages, group_pk=self.group.pk, session=self.session) completion.save() return completion # if the record already exists # (enforced through unique_together) except django.db.IntegrityError: return
def dispatch(self, request, *args, **kwargs): if self.wait_for_all_groups: self._group_or_subsession = self.subsession else: self._group_or_subsession = self.group if self._is_ready(): return self._response_when_ready() # take a lock because we set "waiting for" list here with lock_on_this_code_path(): unvisited_participants = self._tally_unvisited() if unvisited_participants: # only skip the wait page if there are still # unvisited participants. otherwise, you need to # mark the page as completed. # see _increment_index_in_pages, which needs to # handle this case also if not self.is_displayed(): return self._response_when_ready() self.participant.is_on_wait_page = True return self._get_wait_page() else: try: if self.wait_for_all_groups: completion = CompletedSubsessionWaitPage( page_index=self._index_in_pages, session_pk=self.session.pk) else: completion = CompletedGroupWaitPage( page_index=self._index_in_pages, group_pk=self.group.pk, session_pk=self.session.pk) completion.save() # if the record already exists # (enforced through unique_together) except django.db.IntegrityError: self.participant.is_on_wait_page = True return self._get_wait_page() try: # need to check this before deleting # reference to self.player is_displayed = self.is_displayed() # in case there is a timeout on the next page, we # should ensure the next pages are visited promptly # TODO: can we make this run only if next page is a # timeout page? # or if a player is auto playing. # we could instead make this request the current page # URL, but it's different for each player # _group_or_subsession might be deleted # in after_all_players_arrive, so calculate this first participant_pk_set = set([ p.participant.pk for p in self._group_or_subsession.player_set.all() ]) # _group_or_subsession might be deleted # in after_all_players_arrive, so calculate this first channels_group_name = self.channels_group_name() # block users from accessing self.player inside # after_all_players_arrive, because conceptually # there is no single player in this context # (method is executed once for the whole group) player = self.player del self.player # make sure we get the most up-to-date player objects # e.g. if they were queried in is_displayed(), # then they could be out of date # but don't delete the current player from cache # because we need it to be saved at the end import idmap.tls cache = getattr(idmap.tls._tls, 'idmap_cache', {}) for p in list(cache.get(self.PlayerClass, {}).values()): if p != player: self.PlayerClass.flush_cached_instance(p) # if any player can skip the wait page, # then we shouldn't run after_all_players_arrive # because if some players are able to proceed to the next page # before after_all_players_arrive is run, # then after_all_players_arrive is probably not essential. # often, there are some wait pages that all players skip, # because they should only be shown in certain rounds. # maybe the fields that after_all_players_arrive depends on # are null # something to think about: ideally, should we check if # all players skipped, or any player skipped? # as a shortcut, we just check if is_displayed is true # for the last player. if is_displayed: self.after_all_players_arrive() except Exception as e: completion.delete() raise e self.player = player if otree.common_internal.USE_REDIS: # 2015-07-27: # why not check if the next page has_timeout? otree.timeout.tasks.ensure_pages_visited.schedule(kwargs={ 'participant_pk_set': participant_pk_set, 'wait_page_index': self._index_in_pages, }, delay=10) completion.after_all_players_arrive_run = True completion.save() # send a message to the channel to move forward # this should happen at the very end, channels.Group(channels_group_name).send( {'text': json.dumps({'status': 'ready'})}) # we can assume it's ready because # even if it wasn't created, that means someone else # created it, and therefore that whole code block # finished executing (including the after_all_players_arrive) # inside the transaction return self._response_when_ready()
def completion_exists(self, **kwargs): return CompletedGroupWaitPage.objects_exists(**kwargs)