def form_session_handler(v, text, msg): """ The form session handler will use the inbound text to answer the next question in the open SQLXformsSession for the associated contact. If no session is open, the handler passes. If multiple sessions are open, they are all closed and an error message is displayed to the user. """ with critical_section_for_smsforms_sessions(v.owner_id): if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(v.domain): channel = get_channel_for_contact(v.owner_id, v.phone_number) running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel( channel) if running_session_info.session_id: session = SQLXFormsSession.by_session_id( running_session_info.session_id) if not session.session_is_open: # This should never happen. But if it does we should set the channel free # and act like there was no available session notify_error( "The supposedly running session was not open and was released. " 'No known way for this to happen, so worth investigating.' ) XFormsSessionSynchronization.clear_stale_channel_claim( channel) session = None else: session = None else: multiple, session = get_single_open_session_or_close_multiple( v.domain, v.owner_id) if multiple: send_sms_to_verified_number( v, get_message(MSG_MULTIPLE_SESSIONS, v)) return True if session: session.phone_number = v.phone_number session.modified_time = datetime.utcnow() session.save() # Metadata to be applied to the inbound message inbound_metadata = MessageMetadata( workflow=session.workflow, reminder_id=session.reminder_id, xforms_session_couch_id=session._id, ) add_msg_tags(msg, inbound_metadata) try: answer_next_question(v, text, msg, session) except Exception: # Catch any touchforms errors log_sms_exception(msg) send_sms_to_verified_number( v, get_message(MSG_TOUCHFORMS_DOWN, v)) return True else: return False
def test_session_synchronization(): phone_number_a = _clean_up_number('15555555555') phone_number_b = _clean_up_number('15555555554') session_a_1 = FakeSession(session_id=str(uuid4()), phone_number=phone_number_a, connection_id='Alpha') session_a_2 = FakeSession(session_id=str(uuid4()), phone_number=phone_number_a, connection_id='Beta') session_b_1 = FakeSession(session_id=str(uuid4()), phone_number=phone_number_b, connection_id='Kappa') # Nothing set yet, so it can be claimed assert XFormsSessionSynchronization.channel_is_available_for_session( session_a_1) # And so can the other one assert XFormsSessionSynchronization.channel_is_available_for_session( session_a_2) # Claim succeeds assert XFormsSessionSynchronization.claim_channel_for_session(session_a_1) # Claim for same channel fails assert not XFormsSessionSynchronization.channel_is_available_for_session( session_a_2) assert not XFormsSessionSynchronization.claim_channel_for_session( session_a_2) # But same session can re-claim it assert XFormsSessionSynchronization.claim_channel_for_session(session_a_1) # And another session can claim another channel assert XFormsSessionSynchronization.claim_channel_for_session(session_b_1) # And if the first session releases the channel XFormsSessionSynchronization.release_channel_for_session(session_a_1) # Then the contact is still set assert XFormsSessionSynchronization.get_running_session_info_for_channel( SMSChannel(BACKEND_ID, phone_number_a)).contact_id == 'Alpha' # But the other session (that couldn't before) can claim it now assert XFormsSessionSynchronization.claim_channel_for_session(session_a_2) # Trying to clear the channel claim will fail because the session is still open assert not XFormsSessionSynchronization.clear_stale_channel_claim( SMSChannel(BACKEND_ID, phone_number_a)) # But if we close the session first session_a_2.close() # The session is now "stale" so we can clear that stale channel claim assert XFormsSessionSynchronization.clear_stale_channel_claim( SMSChannel(BACKEND_ID, phone_number_a)) # If we try to clear it again it'll be a no-op and return false, since it's already cleared assert not XFormsSessionSynchronization.clear_stale_channel_claim( SMSChannel(BACKEND_ID, phone_number_a))
def create_tasks(self): survey_sessions_due_for_action = self.get_survey_sessions_due_for_action( ) all_open_session_ids = self.get_open_session_ids() for domain, connection_id, session_id, current_action_due, phone_number in survey_sessions_due_for_action: if skip_domain(domain): continue if toggles.ONE_PHONE_NUMBER_MULTIPLE_CONTACTS.enabled(domain): fake_session = SQLXFormsSession( session_id=session_id, connection_id=connection_id, phone_number=phone_number, ) if not XFormsSessionSynchronization.channel_is_available_for_session( fake_session): running_session_info = XFormsSessionSynchronization.get_running_session_info_for_channel( fake_session.get_channel()) # First confirm the supposedly running session is even open # and if it's not (should be exceedingly rare) release it and act like it wasn't there if running_session_info.session_id \ and running_session_info.session_id not in all_open_session_ids: notify_error( "The supposedly running session was not open and was released. " "No known way for this to happen, so worth investigating.", details={ 'running_session_info': running_session_info }) XFormsSessionSynchronization.clear_stale_channel_claim( fake_session.get_channel()) # This is the 99% case: there's a running session for the channel # so leave this session/action in the queue for later and move on to the next one else: continue enqueue_lock = self.get_enqueue_lock(session_id, current_action_due) if enqueue_lock.acquire(blocking=False): handle_due_survey_action.delay(domain, connection_id, session_id)