async def _conduit_listen(self, ctx: ConduitListenCtx) -> Optional[bytes]: _, conduit = self._get_invitation_and_conduit( ctx.organization_id, ctx.token, expected_greeter=ctx.greeter ) if ctx.is_greeter: curr_our_payload = conduit.greeter_payload curr_peer_payload = conduit.claimer_payload else: curr_our_payload = conduit.claimer_payload curr_peer_payload = conduit.greeter_payload if ctx.peer_payload is None: # We are waiting for the peer to provite it payload # Only peer payload should be allowed to change if conduit.state != ctx.state or curr_our_payload != ctx.payload: raise InvitationInvalidStateError() if curr_peer_payload is not None: # Our peer has provided it payload (hence it knows # about our payload too), we can update the conduit # to the next state conduit.state = NEXT_CONDUIT_STATE[ctx.state] conduit.greeter_payload = None conduit.claimer_payload = None await self._send_event( BackendEvent.INVITE_CONDUIT_UPDATED, organization_id=ctx.organization_id, token=ctx.token, ) return curr_peer_payload else: # We were waiting for the peer to take into account the # payload we provided. This would be done once the conduit # has switched to it next state. if conduit.state == NEXT_CONDUIT_STATE[ctx.state] and curr_our_payload is None: return ctx.peer_payload elif ( conduit.state != ctx.state or curr_our_payload != ctx.payload or curr_peer_payload != ctx.peer_payload ): # Something unexpected has changed in our back... raise InvitationInvalidStateError() # Peer hasn't answered yet, we should wait and retry later... return None
async def _conduit_talk( self, organization_id: OrganizationID, greeter: Optional[UserID], token: InvitationToken, state: ConduitState, payload: bytes, ) -> ConduitListenCtx: is_greeter = greeter is not None _, conduit = self._get_invitation_and_conduit(organization_id, token, expected_greeter=greeter) if is_greeter: curr_our_payload = conduit.greeter_payload curr_peer_payload = conduit.claimer_payload else: curr_our_payload = conduit.claimer_payload curr_peer_payload = conduit.greeter_payload if conduit.state != state or curr_our_payload is not None: # We are out of sync with the conduit: # - the conduit state has changed in our back (maybe reseted by the peer) # - we want to reset the conduit # - we have already provided a payload for the current conduit state (most # likely because a retry of a command that failed due to connection outage) if state == ConduitState.STATE_1_WAIT_PEERS: # We wait to reset the conduit conduit.state = state conduit.claimer_payload = None conduit.greeter_payload = None curr_peer_payload = None else: raise InvitationInvalidStateError() # Now update the conduit with our payload and send a signal if # the peer is already waiting for us. if is_greeter: conduit.greeter_payload = payload else: conduit.claimer_payload = payload # Note that in case of conduit reset, this signal will lure the peer into # thinking we have answered so he will wakeup and take into account the reset await self._send_event(BackendEvent.INVITE_CONDUIT_UPDATED, organization_id=organization_id, token=token) return ConduitListenCtx( organization_id=organization_id, greeter=greeter, token=token, state=state, payload=payload, peer_payload=curr_peer_payload, )
async def _conduit_listen(conn, ctx: ConduitListenCtx) -> Optional[bytes]: async with conn.transaction(): if ctx.is_greeter: row = await conn.fetchrow( *_q_conduit_greeter_info( organization_id=ctx.organization_id.str, greeter_user_id=ctx.greeter.str, # type: ignore[union-attr] token=ctx.token.uuid, ) ) else: row = await conn.fetchrow( *_q_conduit_claimer_info( organization_id=ctx.organization_id.str, token=ctx.token.uuid ) ) if not row: raise InvitationNotFoundError() row_id = row["_id"] curr_conduit_state = ConduitState(row["conduit_state"]) if row["deleted_on"]: raise InvitationAlreadyDeletedError() if ctx.is_greeter: curr_our_payload = row["conduit_greeter_payload"] curr_peer_payload = row["conduit_claimer_payload"] else: curr_our_payload = row["conduit_claimer_payload"] curr_peer_payload = row["conduit_greeter_payload"] if ctx.peer_payload is None: # We are waiting for the peer to provite it payload # Only peer payload should be allowed to change if curr_conduit_state != ctx.state or curr_our_payload != ctx.payload: raise InvitationInvalidStateError() if curr_peer_payload is not None: # Our peer has provided it payload (hence it knows # about our payload too), we can update the conduit # to the next state await conn.execute( *_q_conduit_update( row_id=row_id, conduit_state=NEXT_CONDUIT_STATE[ctx.state].value, conduit_greeter_payload=None, conduit_claimer_payload=None, ) ) await send_signal( conn, BackendEvent.INVITE_CONDUIT_UPDATED, organization_id=ctx.organization_id, token=ctx.token, ) return curr_peer_payload else: # We were waiting for the peer to take into account the # payload we provided. This would be done once the conduit # has switched to it next state. if curr_conduit_state == NEXT_CONDUIT_STATE[ctx.state] and curr_our_payload is None: return ctx.peer_payload elif ( curr_conduit_state != ctx.state or curr_our_payload != ctx.payload or curr_peer_payload != ctx.peer_payload ): # Something unexpected has changed in our back... raise InvitationInvalidStateError() # Peer hasn't answered yet, we should wait and retry later... return None
async def _conduit_talk( conn, organization_id: OrganizationID, greeter: Optional[UserID], token: InvitationToken, state: ConduitState, payload: bytes, ) -> ConduitListenCtx: is_greeter = greeter is not None async with conn.transaction(): # On top of retrieving the invitation row, this query lock the row # in the database for the duration of the transaction. # Hence concurrent request will be on hold until the end of the transaction. if is_greeter: row = await conn.fetchrow( *_q_conduit_greeter_info( organization_id=organization_id.str, greeter_user_id=greeter.str, # type: ignore[union-attr] token=token.uuid, ) ) else: row = await conn.fetchrow( *_q_conduit_claimer_info(organization_id=organization_id.str, token=token.uuid) ) if not row: raise InvitationNotFoundError(token) row_id = row["_id"] curr_conduit_state = ConduitState(row["conduit_state"]) curr_greeter_payload = row["conduit_greeter_payload"] curr_claimer_payload = row["conduit_claimer_payload"] if is_greeter: curr_our_payload = curr_greeter_payload curr_peer_payload = curr_claimer_payload else: curr_our_payload = curr_claimer_payload curr_peer_payload = curr_greeter_payload if row["deleted_on"]: raise InvitationAlreadyDeletedError(token) if curr_conduit_state != state or curr_our_payload is not None: # We are out of sync with the conduit: # - the conduit state has changed in our back (maybe reseted by the peer) # - we want to reset the conduit # - we have already provided a payload for the current conduit state (most # likely because a retry of a command that failed due to connection outage) if state == ConduitState.STATE_1_WAIT_PEERS: # We wait to reset the conduit curr_conduit_state = state curr_claimer_payload = None curr_greeter_payload = None curr_our_payload = None curr_peer_payload = None else: raise InvitationInvalidStateError() # Now update the conduit with our payload and send a signal if # the peer is already waiting for us. if is_greeter: curr_greeter_payload = payload else: curr_claimer_payload = payload await conn.execute( *_q_conduit_update( row_id=row_id, conduit_state=curr_conduit_state.value, conduit_greeter_payload=curr_greeter_payload, conduit_claimer_payload=curr_claimer_payload, ) ) # Note that in case of conduit reset, this signal will lure the peer into # thinking we have answered so he will wakeup and take into account the reset await send_signal( conn, BackendEvent.INVITE_CONDUIT_UPDATED, organization_id=organization_id, token=token ) return ConduitListenCtx( organization_id=organization_id, greeter=greeter, token=token, state=state, payload=payload, peer_payload=curr_peer_payload, )