From 08115c9e902bfa67283f08fffae917907f0129fb Mon Sep 17 00:00:00 2001 From: Daniel Finke Date: Wed, 17 Jun 2020 01:58:14 +0000 Subject: [PATCH] [4.3] acdc ignore/cancel/exit overhaul (#6588) - PISTON-179: re-implement exit key support - PISTON-191: clear queue fsm when ignoring members - PISTON-588: allow external member_call_cancels --- applications/acdc/src/acdc_agent_fsm.erl | 24 +- applications/acdc/src/acdc_queue_fsm.erl | 331 +++++++++--------- applications/acdc/src/acdc_queue_handler.erl | 6 + applications/acdc/src/acdc_queue_listener.erl | 278 +++++++-------- applications/acdc/src/acdc_queue_manager.erl | 57 ++- applications/acdc/src/cf_acdc_member.erl | 1 - applications/acdc/src/kapi_acdc_agent.erl | 2 +- applications/crossbar/priv/api/swagger.json | 3 - .../kapi.acdc_agent.member_connect_win.json | 3 - 9 files changed, 341 insertions(+), 364 deletions(-) diff --git a/applications/acdc/src/acdc_agent_fsm.erl b/applications/acdc/src/acdc_agent_fsm.erl index 8173b571e2a..283169c8e74 100644 --- a/applications/acdc/src/acdc_agent_fsm.erl +++ b/applications/acdc/src/acdc_agent_fsm.erl @@ -107,7 +107,6 @@ ,member_call_id :: kz_term:api_binary() ,member_call_queue_id :: kz_term:api_binary() ,member_call_start :: kz_time:now() | 'undefined' - ,caller_exit_key = <<"#">> :: kz_term:ne_binary() ,queue_notifications :: kz_term:api_object() ,agent_call_id :: kz_term:api_binary() @@ -184,8 +183,6 @@ call_event(ServerRef, <<"call_event">>, <<"LEG_DESTROYED">>, JObj) -> gen_statem:cast(ServerRef, {'leg_destroyed', call_id(JObj)}); call_event(ServerRef, <<"call_event">>, <<"CHANNEL_ANSWER">>, JObj) -> gen_statem:cast(ServerRef, {'channel_answered', JObj}); -call_event(ServerRef, <<"call_event">>, <<"DTMF">>, EvtJObj) -> - gen_statem:cast(ServerRef, {'dtmf_pressed', kz_json:get_value(<<"DTMF-Digit">>, EvtJObj)}); call_event(ServerRef, <<"call_event">>, <<"CHANNEL_EXECUTE_COMPLETE">>, JObj) -> maybe_send_execute_complete(ServerRef, kz_json:get_value(<<"Application-Name">>, JObj), JObj); call_event(ServerRef, <<"error">>, <<"dialplan">>, JObj) -> @@ -201,6 +198,7 @@ call_event(ServerRef, <<"call_event">>, <<"CHANNEL_TRANSFEREE">>, JObj) -> Transferor = kz_call_event:other_leg_call_id(JObj), Transferee = kz_call_event:call_id(JObj), gen_statem:cast(ServerRef, {'channel_transferee', Transferor, Transferee}); +call_event(_, <<"call_event">>, <<"DTMF">>, _) -> 'ok'; call_event(_, _C, _E, _) -> lager:info("Unhandled combo: ~s/~s", [_C, _E]). @@ -570,7 +568,6 @@ ready('cast', {'member_connect_win', JObj, 'same_node'}, #state{agent_listener=A kz_util:put_callid(CallId), WrapupTimer = kz_json:get_integer_value(<<"Wrapup-Timeout">>, JObj, 0), - CallerExitKey = kz_json:get_value(<<"Caller-Exit-Key">>, JObj, <<"#">>), QueueId = kz_json:get_value(<<"Queue-ID">>, JObj), CDRUrl = cdr_url(JObj), @@ -604,7 +601,6 @@ ready('cast', {'member_connect_win', JObj, 'same_node'}, #state{agent_listener=A ,member_call_id=CallId ,member_call_start=kz_time:now() ,member_call_queue_id=QueueId - ,caller_exit_key=CallerExitKey ,endpoints=UpdatedEPs ,queue_notifications=kz_json:get_value(<<"Notifications">>, JObj) }} @@ -620,7 +616,6 @@ ready('cast', {'member_connect_win', JObj, 'different_node'}, #state{agent_liste kz_util:put_callid(CallId), WrapupTimer = kz_json:get_integer_value(<<"Wrapup-Timeout">>, JObj, 0), - CallerExitKey = kz_json:get_value(<<"Caller-Exit-Key">>, JObj, <<"#">>), QueueId = kz_json:get_value(<<"Queue-ID">>, JObj), RecordingUrl = recording_url(JObj), @@ -645,7 +640,6 @@ ready('cast', {'member_connect_win', JObj, 'different_node'}, #state{agent_liste ,member_call_id=CallId ,member_call_start=kz_time:now() ,member_call_queue_id=QueueId - ,caller_exit_key=CallerExitKey ,endpoints=UpdatedEPs ,queue_notifications=kz_json:get_value(<<"Notifications">>, JObj) ,monitoring='true' @@ -682,8 +676,6 @@ ready('cast', {'channel_unbridged', CallId}, #state{agent_listener=_AgentListene ready('cast', {'leg_destroyed', CallId}, #state{agent_listener=_AgentListener}=State) -> lager:debug("channel unbridged: ~s", [CallId]), {'next_state', 'ready', State}; -ready('cast', {'dtmf_pressed', _}, State) -> - {'next_state', 'ready', State}; ready('cast', {'originate_failed', _E}, State) -> {'next_state', 'ready', State}; ready('cast', Evt, State) -> @@ -818,19 +810,6 @@ ringing('cast', {'channel_bridged', MemberCallId}, #state{member_call_id=MemberC {'next_state', 'answered', State#state{connect_failures=0}}; ringing('cast', {'channel_bridged', _CallId}, State) -> {'next_state', 'ringing', State}; -ringing('cast', {'dtmf_pressed', DTMF}, #state{caller_exit_key=DTMF - ,agent_listener=AgentListener - ,agent_call_id=AgentCallId - }=State) when is_binary(DTMF) -> - lager:debug("caller exit key pressed: ~s", [DTMF]), - acdc_agent_listener:channel_hungup(AgentListener, AgentCallId), - - acdc_agent_listener:presence_update(AgentListener, ?PRESENCE_GREEN), - - apply_state_updates(clear_call(State, 'ready')); -ringing('cast', {'dtmf_pressed', DTMF}, #state{caller_exit_key=_ExitKey}=State) -> - lager:debug("caller pressed ~s, exit key is ~s", [DTMF, _ExitKey]), - {'next_state', 'ringing', State}; ringing('cast', {'channel_answered', JObj}, #state{member_call_id=MemberCallId ,agent_listener=AgentListener ,outbound_call_ids=OutboundCallIds @@ -1637,7 +1616,6 @@ clear_call(#state{statem_call_id=StateMCallId ,member_call_start = 'undefined' ,member_call_queue_id = 'undefined' ,agent_call_id = 'undefined' - ,caller_exit_key = <<"#">> ,queue_notifications = 'undefined' ,monitoring = 'false' }. diff --git a/applications/acdc/src/acdc_queue_fsm.erl b/applications/acdc/src/acdc_queue_fsm.erl index 57a5bdf871a..89a997be636 100644 --- a/applications/acdc/src/acdc_queue_fsm.erl +++ b/applications/acdc/src/acdc_queue_fsm.erl @@ -13,6 +13,7 @@ %% Event injectors -export([member_call/3 + ,member_call_cancel/2 ,member_connect_resp/2 ,member_accepted/2 ,member_connect_retry/2 @@ -20,7 +21,6 @@ ,refresh/2 ,current_call/1 ,status/1 - ,finish_member_call/1 %% Accessors ,cdr_url/1 @@ -119,6 +119,14 @@ refresh(ServerRef, QueueJObj) -> member_call(ServerRef, CallJObj, Delivery) -> gen_statem:cast(ServerRef, {'member_call', CallJObj, Delivery}). +%%------------------------------------------------------------------------------ +%% @doc +%% @end +%%------------------------------------------------------------------------------ +-spec member_call_cancel(pid(), kz_json:object()) -> 'ok'. +member_call_cancel(ServerRef, JObj) -> + gen_statem:cast(ServerRef, {'member_call_cancel', JObj}). + %%------------------------------------------------------------------------------ %% @doc %% @end @@ -152,18 +160,9 @@ member_connect_retry(ServerRef, RetryJObj) -> -spec call_event(pid(), kz_term:ne_binary(), kz_term:ne_binary(), kz_json:object()) -> 'ok'. call_event(ServerRef, <<"call_event">>, <<"CHANNEL_DESTROY">>, EvtJObj) -> gen_statem:cast(ServerRef, {'member_hungup', EvtJObj}); -call_event(ServerRef, <<"call_event">>, <<"DTMF">>, EvtJObj) -> - gen_statem:cast(ServerRef, {'dtmf_pressed', kz_json:get_value(<<"DTMF-Digit">>, EvtJObj)}); call_event(ServerRef, <<"call_event">>, <<"CHANNEL_BRIDGE">>, EvtJObj) -> gen_statem:cast(ServerRef, {'channel_bridged', EvtJObj}); call_event(_, _E, _N, _J) -> 'ok'. -%% lager:debug("unhandled event: ~s: ~s (~s)" -%% ,[_E, _N, kz_json:get_value(<<"Application-Name">>, _J)] -%% ). - --spec finish_member_call(pid()) -> 'ok'. -finish_member_call(ServerRef) -> - gen_statem:cast(ServerRef, {'member_finished'}). -spec current_call(pid()) -> kz_term:api_object(). current_call(ServerRef) -> @@ -241,21 +240,32 @@ ready('cast', {'get_listener_proc', WorkerSup}, State) -> ListenerSrv = acdc_queue_worker_sup:listener(WorkerSup), lager:debug("got listener proc: ~p", [ListenerSrv]), {'next_state', 'ready', State#state{listener_proc=ListenerSrv}}; -ready('cast', {'member_call', CallJObj, Delivery}, #state{listener_proc=ListenerSrv - ,manager_proc=MgrSrv - }=State) -> +ready('cast', {'member_call', CallJObj, Delivery}, #state{listener_proc=ListenerSrv}=State) -> Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, CallJObj)), CallId = kapps_call:call_id(Call), kz_util:put_callid(CallId), + acdc_queue_listener:member_call(ListenerSrv, CallJObj, Delivery), + + ready('cast', {'check_if_next', CallJObj, Delivery}, State#state{member_call=Call}); +ready('cast', {'check_if_next', CallJObj, Delivery}, #state{listener_proc=ListenerSrv + ,manager_proc=MgrSrv + ,member_call=Call + }=State) -> case acdc_queue_manager:should_ignore_member_call(MgrSrv, Call, CallJObj) of 'false' -> - maybe_delay_connect_req(Call, CallJObj, Delivery, State); + maybe_abort_connect_req(fun maybe_delay_connect_req/3 + ,[CallJObj, Delivery] + ,State + ); 'true' -> - lager:debug("queue mgr said to ignore this call: ~s", [CallId]), + lager:debug("queue mgr said to ignore this call: ~s", [kapps_call:call_id(Call)]), acdc_queue_listener:ignore_member_call(ListenerSrv, Call, Delivery), - {'next_state', 'ready', State} + {'next_state', 'ready', clear_member_call(State)} end; +ready('cast', {'member_call_cancel', _}, State) -> + %% Let check_if_next handle this call being cancelled + {'next_state', 'ready', State}; ready('cast', {'agent_resp', _Resp}, State) -> lager:debug("someone jumped the gun, or was slow on the draw"), {'next_state', 'ready', State}; @@ -266,13 +276,6 @@ ready('cast', {'retry', _RetryJObj}, State) -> lager:debug("weird to receive a retry when we're just hanging here"), {'next_state', 'ready', State}; ready('cast', {'member_hungup', _CallEvt}, State) -> - lager:debug("member hungup from previous call: ~p", [_CallEvt]), - {'next_state', 'ready', State}; -ready('cast', {'member_finished'}, State) -> - lager:debug("member finished while in 'ready', ignore"), - {'next_state', 'ready', State}; -ready('cast', {'dtmf_pressed', _DTMF}, State) -> - lager:debug("DTMF(~s) for old call", [_DTMF]), {'next_state', 'ready', State}; ready('cast', Event, State) -> handle_event(Event, ready, State); @@ -287,7 +290,10 @@ ready({'call', From}, 'status', #state{cdr_url=Url ready({'call', From}, 'current_call', State) -> {'next_state', 'ready', State, {'reply', From, 'undefined'}}; ready({'call', From}, Event, State) -> - handle_sync_event(Event, From, ready, State). + handle_sync_event(Event, From, ready, State); + +ready('info', {'timeout', _, ?COLLECT_RESP_MESSAGE}, State) -> + {'next_state', 'ready', State}. %%------------------------------------------------------------------------------ %% @doc @@ -301,17 +307,19 @@ connect_req('cast', {'member_call', CallJObj, Delivery}, #state{listener_proc=Li acdc_queue_listener:cancel_member_call(ListenerSrv, CallJObj, Delivery), {'next_state', 'connect_req', State}; +connect_req('cast', {'member_call_cancel', JObj}, State) -> + handle_member_call_cancel(JObj, 'connect_req', State); + connect_req('cast', {'agent_resp', Resp}, #state{connect_resps=CRs ,manager_proc=MgrSrv }=State) -> Agents = acdc_queue_manager:current_agents(MgrSrv), Resps = [Resp | CRs], - {NextState, State1} = - case have_agents_responded(Resps, Agents) of - 'true' -> handle_agent_responses(State#state{connect_resps=Resps}); - 'false' -> {'connect_req', State#state{connect_resps=Resps}} - end, - {'next_state', NextState, State1}; + State1 = State#state{connect_resps=Resps}, + case have_agents_responded(Resps, Agents) of + 'true' -> handle_agent_responses(State1); + 'false' -> {'next_state', 'connect_req', State1} + end; connect_req('cast', {'accepted', AcceptJObj}=Accept, #state{member_call=Call}=State) -> case accept_is_for_call(AcceptJObj, Call) of @@ -348,30 +356,6 @@ connect_req('cast', {'member_hungup', JObj}, #state{listener_proc=ListenerSrv {'next_state', 'connect_req', State} end; -connect_req('cast', {'member_finished'}, #state{member_call=Call}=State) -> - case catch kapps_call:call_id(Call) of - CallId when is_binary(CallId) -> - lager:debug("member finished while in connect_req: ~s", [CallId]), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finished - forced">>); - _E-> - lager:debug("member finished, but callid became ~p", [_E]) - end, - {'next_state', 'ready', clear_member_call(State), 'hibernate'}; - -connect_req('cast', {'dtmf_pressed', DTMF}, #state{caller_exit_key=DTMF - ,listener_proc=ListenerSrv - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - }=State) when is_binary(DTMF) -> - lager:debug("member pressed the exit key (~s)", [DTMF]), - CallId = kapps_call:call_id(Call), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - DTMF">>), - - acdc_queue_listener:exit_member_call(ListenerSrv), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_EXIT), - {'next_state', 'ready', clear_member_call(State), 'hibernate'}; - connect_req('cast', Event, State) -> handle_event(Event, connect_req, State); @@ -416,26 +400,14 @@ connect_req('info', {'timeout', Ref, ?COLLECT_RESP_MESSAGE}, #state{collect_ref= 'true' -> lager:debug("queue mgr said to ignore this call: ~s, not retrying agents", [kapps_call:call_id(Call)]), acdc_queue_listener:finish_member_call(ListenerSrv), - {'next_state', 'ready', State}; + {'next_state', 'ready', clear_member_call(State), 'hibernate'}; 'false' -> - maybe_connect_re_req(MgrSrv, ListenerSrv, State) + maybe_abort_connect_req(fun maybe_delay_connect_re_req/1, [], State) end; connect_req('info', {'timeout', Ref, ?COLLECT_RESP_MESSAGE}, #state{collect_ref=Ref}=State) -> - {NextState, State1} = handle_agent_responses(State), - {'next_state', NextState, State1}; -connect_req('info', {'timeout', ConnRef, ?CONNECTION_TIMEOUT_MESSAGE}, #state{listener_proc=ListenerSrv - ,connection_timer_ref=ConnRef - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - }=State) -> - lager:debug("connection timeout occurred, bounce the caller out of the queue"), - CallId = kapps_call:call_id(Call), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - timeout">>), - - acdc_queue_listener:timeout_member_call(ListenerSrv), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_TIMEOUT), - {'next_state', 'ready', clear_member_call(State), 'hibernate'}. + handle_agent_responses(State); +connect_req('info', {'timeout', ConnRef, ?CONNECTION_TIMEOUT_MESSAGE}, State) -> + handle_connection_timeout(ConnRef, State). %%------------------------------------------------------------------------------ %% @doc @@ -447,6 +419,9 @@ connecting('cast', {'member_call', CallJObj, Delivery}, #state{listener_proc=Lis acdc_queue_listener:cancel_member_call(ListenerSrv, CallJObj, Delivery), {'next_state', 'connecting', State}; +connecting('cast', {'member_call_cancel', JObj}, State) -> + handle_member_call_cancel(JObj, 'connecting', State); + connecting('cast', {'agent_resp', _Resp}, State) -> lager:debug("agent resp must have just missed cutoff"), {'next_state', 'connecting', State}; @@ -462,7 +437,7 @@ connecting('cast', {'accepted', AcceptJObj}, #state{listener_proc=ListenerSrv CallId = kapps_call:call_id(Call), webseq:evt(?WSD_ID, self(), CallId, <<"member call - agent acceptance">>), - acdc_queue_listener:finish_member_call(ListenerSrv, AcceptJObj), + acdc_queue_listener:finish_member_call(ListenerSrv), acdc_stats:call_handled(AccountId, QueueId, CallId ,kz_json:get_value(<<"Agent-ID">>, AcceptJObj) ), @@ -483,7 +458,6 @@ connecting('cast', {'retry', RetryJObj}, #state{agent_ring_timer_ref=AgentRef {RetryAgentId, RetryProcId} -> lager:debug("recv retry from our winning agent ~s(~s)", [RetryAgentId, RetryProcId]), - lager:debug("but wait, we have others who wanted to try"), erlang:send(self(), {'timeout', 'undefined', ?COLLECT_RESP_MESSAGE}), maybe_stop_timer(CollectRef), @@ -517,32 +491,6 @@ connecting('cast', {'member_hungup', CallEvt}, #state{listener_proc=ListenerSrv {'next_state', 'ready', clear_member_call(State), 'hibernate'}; -connecting('cast', {'member_finished'}, #state{member_call=Call}=State) -> - case catch kapps_call:call_id(Call) of - CallId when is_binary(CallId) -> - lager:debug("member finished while in connecting: ~s", [CallId]), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finished - forced">>); - _E-> - lager:debug("member finished, but callid became ~p", [_E]) - end, - {'next_state', 'ready', clear_member_call(State), 'hibernate'}; -connecting('cast', {'dtmf_pressed', DTMF}, #state{caller_exit_key=DTMF - ,listener_proc=ListenerSrv - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - }=State) when is_binary(DTMF) -> - lager:debug("member pressed the exit key (~s)", [DTMF]), - acdc_queue_listener:exit_member_call(ListenerSrv), - CallId = kapps_call:call_id(Call), - webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - DTMF">>), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_EXIT), - {'next_state', 'ready', clear_member_call(State), 'hibernate'}; - -connecting('cast', {'dtmf_pressed', _DTMF}, State) -> - lager:debug("caller pressed ~s, ignoring", [_DTMF]), - {'next_state', 'connecting', State}; - connecting('cast', Event, State) -> handle_event(Event, connecting, State); @@ -592,22 +540,8 @@ connecting('info', {'timeout', AgentRef, ?AGENT_RING_TIMEOUT_MESSAGE}, #state{ag connecting('info', {'timeout', _OtherAgentRef, ?AGENT_RING_TIMEOUT_MESSAGE}, #state{agent_ring_timer_ref=_AgentRef}=State) -> lager:debug("unknown agent ref: ~p known: ~p", [_OtherAgentRef, _AgentRef]), {'next_state', 'connect_req', State}; -connecting('info', {'timeout', ConnRef, ?CONNECTION_TIMEOUT_MESSAGE}, #state{listener_proc=ListenerSrv - ,connection_timer_ref=ConnRef - ,account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - ,member_call_winner=Winner - }=State) -> - lager:debug("connection timeout occurred, bounce the caller out of the queue"), - - maybe_timeout_winner(ListenerSrv, Winner), - CallId = kapps_call:call_id(Call), - acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_TIMEOUT), - - webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - timeout">>), - - {'next_state', 'ready', clear_member_call(State), 'hibernate'}. +connecting('info', {'timeout', ConnRef, ?CONNECTION_TIMEOUT_MESSAGE}, State) -> + handle_connection_timeout(ConnRef, State). %%------------------------------------------------------------------------------ %% @doc @@ -663,6 +597,65 @@ code_change(_OldVsn, StateName, State, _Extra) -> %%% Internal functions %%%============================================================================= +%%------------------------------------------------------------------------------ +%% @doc Handle a member_call_cancel event. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_member_call_cancel(kz_json:object(), atom(), state()) -> kz_types:handle_fsm_ret(state()). +handle_member_call_cancel(JObj, StateName, State) -> + case kz_json:get_ne_binary_value(<<"Reason">>, JObj) of + <<"dtmf_exit">> -> handle_member_call_cancel_dtmf_exit(JObj, StateName, State); + _ -> {'next_state', StateName, State} + end. + +%%------------------------------------------------------------------------------ +%% @doc Handle a member_call_cancel event as a result of the caller pressing the +%% caller_exit_key. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_member_call_cancel_dtmf_exit(kz_json:object(), atom(), state()) -> kz_types:handle_fsm_ret(state()). +handle_member_call_cancel_dtmf_exit(JObj, StateName, #state{listener_proc=ListenerSrv + ,account_id=AccountId + ,queue_id=QueueId + ,member_call=MemberCall + ,member_call_winner=Winner + ,caller_exit_key=DTMF + }=State) -> + CallId = kz_json:get_ne_binary_value(<<"Call-ID">>, JObj), + MemberCallId = kapps_call:call_id(MemberCall), + case CallId of + MemberCallId -> + lager:debug("member pressed the exit key (~s)", [DTMF]), + + webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - DTMF">>), + + acdc_queue_listener:exit_member_call(ListenerSrv, Winner), + acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_EXIT), + {'next_state', 'ready', clear_member_call(State), 'hibernate'}; + _ -> {'next_state', StateName, State} + end. + +%%------------------------------------------------------------------------------ +%% @doc Handle a connection timeout event as a result of the caller reaching the +%% max wait time in the queue. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_connection_timeout(reference(), state()) -> kz_types:handle_fsm_ret(state()). +handle_connection_timeout(ConnRef, #state{listener_proc=ListenerSrv + ,connection_timer_ref=ConnRef + ,account_id=AccountId + ,queue_id=QueueId + ,member_call=Call + ,member_call_winner=Winner + }=State) -> + lager:debug("connection timeout occurred, bounce the caller out of the queue"), + CallId = kapps_call:call_id(Call), + webseq:evt(?WSD_ID, self(), CallId, <<"member call finish - timeout">>), + + acdc_queue_listener:timeout_member_call(ListenerSrv, Winner), + acdc_stats:call_abandoned(AccountId, QueueId, CallId, ?ABANDON_TIMEOUT), + {'next_state', 'ready', clear_member_call(State), 'hibernate'}. + %%------------------------------------------------------------------------------ %% @doc %% @end @@ -692,12 +685,6 @@ maybe_stop_timer(ConnRef) -> _ = erlang:cancel_timer(ConnRef), 'ok'. --spec maybe_timeout_winner(pid(), kz_term:api_object()) -> 'ok'. -maybe_timeout_winner(Srv, 'undefined') -> - acdc_queue_listener:timeout_member_call(Srv); -maybe_timeout_winner(Srv, Winner) -> - acdc_queue_listener:timeout_member_call(Srv, Winner). - -spec clear_member_call(state()) -> state(). clear_member_call(#state{connection_timer_ref=ConnRef ,agent_ring_timer_ref=AgentRef @@ -757,20 +744,43 @@ elapsed(Ref) when is_reference(Ref) -> end; elapsed(Time) -> kz_time:elapsed_s(Time). +%%------------------------------------------------------------------------------ +%% @doc Abort a queue call if agents have left the building +%% @end +%%------------------------------------------------------------------------------ +-type on_continue_callback() :: fun((...) -> kz_types:handle_fsm_ret(state())). + +-spec maybe_abort_connect_req(on_continue_callback(), [term()], state()) -> kz_types:handle_fsm_ret(state()). +maybe_abort_connect_req(OnContinue, CallbackArgs, #state{listener_proc=ListenerSrv + ,manager_proc=MgrSrv + ,account_id=AccountId + ,queue_id=QueueId + ,member_call=Call + }=State) -> + case acdc_queue_manager:are_agents_available(MgrSrv) of + 'true' -> apply(OnContinue, CallbackArgs ++ [State]); + 'false' -> + lager:debug("all agents have left the queue, failing call"), + webseq:note(?WSD_ID, self(), 'right', <<"all agents have left the queue, failing call">>), + acdc_queue_listener:exit_member_call_empty(ListenerSrv), + acdc_stats:call_abandoned(AccountId, QueueId, kapps_call:call_id(Call), ?ABANDON_EMPTY), + {'next_state', 'ready', clear_member_call(State), 'hibernate'} + end. + %%------------------------------------------------------------------------------ %% @doc If some agents are busy, the manager will tell us to delay our %% connect reqs %% %% @end %%------------------------------------------------------------------------------ --spec maybe_delay_connect_req(kapps_call:call(), kz_json:object(), gen_listener:basic_deliver(), state()) -> +-spec maybe_delay_connect_req(kz_json:object(), gen_listener:basic_deliver(), state()) -> {'next_state', 'ready' | 'connect_req', state()}. -maybe_delay_connect_req(Call, CallJObj, Delivery, #state{listener_proc=ListenerSrv - ,manager_proc=MgrSrv - ,connection_timeout=ConnTimeout - ,connection_timer_ref=ConnRef - ,cdr_url=Url - }=State) -> +maybe_delay_connect_req(CallJObj, Delivery, #state{listener_proc=ListenerSrv + ,manager_proc=MgrSrv + ,connection_timeout=ConnTimeout + ,connection_timer_ref=ConnRef + ,member_call=Call + }=State) -> CallId = kapps_call:call_id(Call), case acdc_queue_manager:up_next(MgrSrv, CallId) of 'true' -> @@ -779,52 +789,31 @@ maybe_delay_connect_req(Call, CallJObj, Delivery, #state{listener_proc=ListenerS webseq:note(?WSD_ID, self(), 'right', [CallId, <<": member call">>]), webseq:evt(?WSD_ID, CallId, self(), <<"member call received">>), - acdc_queue_listener:member_connect_req(ListenerSrv, CallJObj, Delivery, Url), + acdc_queue_listener:member_connect_req(ListenerSrv), maybe_stop_timer(ConnRef), % stop the old one, maybe {'next_state', 'connect_req', State#state{collect_ref=start_collect_timer() - ,member_call=Call ,member_call_start=kz_time:now_s() ,connection_timer_ref=start_connection_timer(ConnTimeout) }}; 'false' -> lager:debug("connect_req delayed (not up next)"), - _ = timer:apply_after(1000, 'gen_statem', 'cast', [self(), {'member_call', CallJObj, Delivery}]), + _ = timer:apply_after(1000, 'gen_statem', 'cast', [self(), {'check_if_next', CallJObj, Delivery}]), {'next_state', 'ready', State} end. -%%------------------------------------------------------------------------------ -%% @doc Abort a queue call between connect_reqs if agents have left the -%% building -%% -%% @end -%%------------------------------------------------------------------------------ --spec maybe_connect_re_req(pid(), pid(), state()) -> kz_types:handle_fsm_ret(state()). -maybe_connect_re_req(MgrSrv, ListenerSrv, #state{account_id=AccountId - ,queue_id=QueueId - ,member_call=Call - }=State) -> - case acdc_queue_manager:are_agents_available(MgrSrv) of - 'true' -> - maybe_delay_connect_re_req(MgrSrv, ListenerSrv, State); - 'false' -> - lager:debug("all agents have left the queue, failing call"), - webseq:note(?WSD_ID, self(), 'right', <<"all agents have left the queue, failing call">>), - acdc_queue_listener:exit_member_call_empty(ListenerSrv), - acdc_stats:call_abandoned(AccountId, QueueId, kapps_call:call_id(Call), ?ABANDON_EMPTY), - {'next_state', 'ready', clear_member_call(State), 'hibernate'} - end. - --spec maybe_delay_connect_re_req(pid(), pid(), state()) -> - {'next_state', 'connect_req', state()}. -maybe_delay_connect_re_req(MgrSrv, ListenerSrv, #state{member_call=Call}=State) -> +-spec maybe_delay_connect_re_req(state()) -> {'next_state', 'connect_req', state()}. +maybe_delay_connect_re_req(#state{listener_proc=ListenerSrv + ,manager_proc=MgrSrv + ,member_call=Call + }=State) -> CallId = kapps_call:call_id(Call), case acdc_queue_manager:up_next(MgrSrv, CallId) of 'true' -> lager:debug("done waiting, no agents responded, let's ask again"), webseq:note(?WSD_ID, self(), 'right', <<"no agents responded, trying again">>), - acdc_queue_listener:member_connect_re_req(ListenerSrv), + acdc_queue_listener:member_connect_req(ListenerSrv), {'next_state', 'connect_req', State#state{collect_ref=start_collect_timer()}}; 'false' -> lager:debug("connect_re_req delayed (not up next)"), @@ -840,7 +829,7 @@ accept_is_for_call(AcceptJObj, Call) -> update_agent(Agent, Winner) -> kz_json:set_value(<<"Agent-Process-ID">>, kz_json:get_value(<<"Process-ID">>, Winner), Agent). --spec handle_agent_responses(state()) -> {atom(), state()}. +-spec handle_agent_responses(state()) -> kz_types:handle_fsm_ret(state()). handle_agent_responses(#state{collect_ref=Ref ,manager_proc=MgrSrv ,listener_proc=ListenerSrv @@ -853,52 +842,46 @@ handle_agent_responses(#state{collect_ref=Ref 'true' -> lager:debug("queue mgr said to ignore this call: ~s, not connecting to agents", [kapps_call:call_id(Call)]), acdc_queue_listener:finish_member_call(ListenerSrv), - {'ready', State}; + {'next_state', 'ready', clear_member_call(State)}; 'false' -> lager:debug("done waiting for agents to respond, picking a winner"), maybe_pick_winner(State) end. --spec maybe_pick_winner(state()) -> {atom(), state()}. +-spec maybe_pick_winner(state()) -> kz_types:handle_fsm_ret(state()). maybe_pick_winner(#state{connect_resps=CRs ,listener_proc=ListenerSrv ,manager_proc=Mgr ,agent_ring_timeout=RingTimeout ,agent_wrapup_time=AgentWrapup - ,caller_exit_key=CallerExitKey ,cdr_url=CDRUrl ,record_caller=ShouldRecord ,recording_url=RecordUrl ,notifications=Notifications }=State) -> case acdc_queue_manager:pick_winner(Mgr, CRs) of - {[Winner|_]=Agents, Rest} -> + {[Winner|_], _} -> QueueOpts = [{<<"Ring-Timeout">>, RingTimeout} ,{<<"Wrapup-Timeout">>, AgentWrapup} - ,{<<"Caller-Exit-Key">>, CallerExitKey} ,{<<"CDR-Url">>, CDRUrl} ,{<<"Record-Caller">>, ShouldRecord} ,{<<"Recording-URL">>, RecordUrl} ,{<<"Notifications">>, Notifications} ], - _ = [acdc_queue_listener:member_connect_win(ListenerSrv, update_agent(Agent, Winner), QueueOpts) - || Agent <- Agents - ], + acdc_queue_listener:member_connect_win(ListenerSrv, update_agent(Winner, Winner), props:filter_undefined(QueueOpts)), lager:debug("sending win to ~s(~s)", [kz_json:get_value(<<"Agent-ID">>, Winner) ,kz_json:get_value(<<"Process-ID">>, Winner) ]), - {'connecting', State#state{connect_resps=Rest - ,collect_ref='undefined' - ,agent_ring_timer_ref=start_agent_ring_timer(RingTimeout) - ,member_call_winner=Winner - }}; + {'next_state', 'connecting', State#state{connect_resps=[] + ,collect_ref='undefined' + ,agent_ring_timer_ref=start_agent_ring_timer(RingTimeout) + ,member_call_winner=Winner + }}; 'undefined' -> lager:debug("no more responses to choose from"), - - acdc_queue_listener:cancel_member_call(ListenerSrv), - {'ready', clear_member_call(State)} + maybe_abort_connect_req(fun maybe_delay_connect_re_req/1, [], State#state{connect_resps=[]}) end. -spec have_agents_responded(kz_json:objects(), kz_term:ne_binaries()) -> boolean(). diff --git a/applications/acdc/src/acdc_queue_handler.erl b/applications/acdc/src/acdc_queue_handler.erl index 3256d3b2735..93de490dcf0 100644 --- a/applications/acdc/src/acdc_queue_handler.erl +++ b/applications/acdc/src/acdc_queue_handler.erl @@ -8,6 +8,7 @@ -export([handle_call_event/2 ,handle_member_call/3 + ,handle_member_call_cancel/2 ,handle_member_resp/2 ,handle_member_accepted/2 ,handle_member_retry/2 @@ -52,6 +53,11 @@ handle_member_call(JObj, Props, Delivery) -> acdc_queue_fsm:member_call(props:get_value('fsm_pid', Props), JObj, Delivery), gen_listener:cast(props:get_value('server', Props), {'delivery', Delivery}). +-spec handle_member_call_cancel(kz_json:object(), kz_term:proplist()) -> 'ok'. +handle_member_call_cancel(JObj, Props) -> + 'true' = kapi_acdc_queue:member_call_cancel_v(JObj), + acdc_queue_fsm:member_call_cancel(props:get_value('fsm_pid', Props), JObj). + -spec handle_member_resp(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_member_resp(JObj, Props) -> 'true' = kapi_acdc_queue:member_connect_resp_v(JObj), diff --git a/applications/acdc/src/acdc_queue_listener.erl b/applications/acdc/src/acdc_queue_listener.erl index 781dcb7d796..8fb60b69b63 100644 --- a/applications/acdc/src/acdc_queue_listener.erl +++ b/applications/acdc/src/acdc_queue_listener.erl @@ -19,17 +19,16 @@ %% API -export([start_link/4 - ,accept_member_calls/1 - ,member_connect_req/4 - ,member_connect_re_req/1 + ,member_call/3 + ,member_connect_req/1 ,member_connect_win/3 - ,timeout_member_call/1, timeout_member_call/2 + ,timeout_member_call/2 ,timeout_agent/2 - ,exit_member_call/1 + ,exit_member_call/2 ,exit_member_call_empty/1 - ,finish_member_call/1, finish_member_call/2 + ,finish_member_call/1 ,ignore_member_call/3 - ,cancel_member_call/1, cancel_member_call/2 ,cancel_member_call/3 + ,cancel_member_call/2, cancel_member_call/3 ,send_sync_req/2 ,config/1 ,send_sync_resp/4 @@ -77,6 +76,9 @@ ,{{'acdc_queue_handler', 'handle_call_event'} ,[{<<"error">>, <<"*">>}] } + ,{{'acdc_queue_handler', 'handle_member_call_cancel'} + ,[{<<"member">>, <<"call_cancel">>}] + } ,{{'acdc_queue_handler', 'handle_member_resp'} ,[{<<"member">>, <<"connect_resp">>}] } @@ -113,17 +115,13 @@ start_link(WorkerSup, MgrPid, AccountId, QueueId) -> ,[WorkerSup, MgrPid, AccountId, QueueId] ). --spec accept_member_calls(pid()) -> 'ok'. -accept_member_calls(Srv) -> - gen_listener:cast(Srv, {'accept_member_calls'}). - --spec member_connect_req(pid(), kz_json:object(), any(), kz_term:api_binary()) -> 'ok'. -member_connect_req(Srv, MemberCallJObj, Delivery, Url) -> - gen_listener:cast(Srv, {'member_connect_req', MemberCallJObj, Delivery, Url}). +-spec member_call(pid(), kz_json:object(), any()) -> 'ok'. +member_call(Srv, MemberCallJObj, Delivery) -> + gen_listener:cast(Srv, {'member_call', MemberCallJObj, Delivery}). --spec member_connect_re_req(pid()) -> 'ok'. -member_connect_re_req(Srv) -> - gen_listener:cast(Srv, {'member_connect_re_req'}). +-spec member_connect_req(pid()) -> 'ok'. +member_connect_req(Srv) -> + gen_listener:cast(Srv, {'member_connect_req'}). -spec member_connect_win(pid(), kz_json:object(), kz_term:proplist()) -> 'ok'. member_connect_win(Srv, RespJObj, QueueOpts) -> @@ -133,17 +131,13 @@ member_connect_win(Srv, RespJObj, QueueOpts) -> timeout_agent(Srv, RespJObj) -> gen_listener:cast(Srv, {'timeout_agent', RespJObj}). --spec timeout_member_call(pid()) -> 'ok'. -timeout_member_call(Srv) -> - timeout_member_call(Srv, 'undefined'). - -spec timeout_member_call(pid(), kz_term:api_object()) -> 'ok'. -timeout_member_call(Srv, JObj) -> - gen_listener:cast(Srv, {'timeout_member_call', JObj}). +timeout_member_call(Srv, WinnerJObj) -> + gen_listener:cast(Srv, {'timeout_member_call', WinnerJObj}). --spec exit_member_call(pid()) -> 'ok'. -exit_member_call(Srv) -> - gen_listener:cast(Srv, {'exit_member_call'}). +-spec exit_member_call(pid(), kz_term:api_object()) -> 'ok'. +exit_member_call(Srv, WinnerJObj) -> + gen_listener:cast(Srv, {'exit_member_call', WinnerJObj}). -spec exit_member_call_empty(pid()) -> 'ok'. exit_member_call_empty(Srv) -> @@ -153,14 +147,6 @@ exit_member_call_empty(Srv) -> finish_member_call(Srv) -> gen_listener:cast(Srv, {'finish_member_call'}). --spec finish_member_call(pid(), kz_json:object()) -> 'ok'. -finish_member_call(Srv, AcceptJObj) -> - gen_listener:cast(Srv, {'finish_member_call', AcceptJObj}). - --spec cancel_member_call(pid()) -> 'ok'. -cancel_member_call(Srv) -> - gen_listener:cast(Srv, {'cancel_member_call'}). - -spec cancel_member_call(pid(), kz_json:object()) -> 'ok'. cancel_member_call(Srv, RejectJObj) -> gen_listener:cast(Srv, {'cancel_member_call', RejectJObj}). @@ -240,33 +226,38 @@ handle_cast({'get_friends', WorkerSup}, State) -> handle_cast({'gen_listener', {'created_queue', Q}}, #state{my_q='undefined'}=State) -> {'noreply', State#state{my_q=Q}, 'hibernate'}; -handle_cast({'member_connect_req', MemberCallJObj, Delivery, _Url} - ,#state{my_q=MyQ - ,my_id=MyId - ,account_id=AccountId - ,queue_id=QueueId - }=State) -> +handle_cast({'member_call', MemberCallJObj, Delivery}, #state{queue_id=QueueId + ,account_id=AccountId + }=State) -> Call = kapps_call:from_json(kz_json:get_value(<<"Call">>, MemberCallJObj)), + CallId = kapps_call:call_id(Call), - kz_util:put_callid(kapps_call:call_id(Call)), + kz_util:put_callid(CallId), acdc_util:bind_to_call_events(Call), - lager:debug("bound to call events for ~s", [kapps_call:call_id(Call)]), - send_member_connect_req(kapps_call:call_id(Call), AccountId, QueueId, MyQ, MyId), + lager:debug("bound to call events for ~s", [CallId]), + + %% Be ready in case a cancel comes in while queue_listener is handling call + gen_listener:add_binding(self(), 'acdc_queue', [{'restrict_to', ['member_call_result']} + ,{'account_id', AccountId} + ,{'queue_id', QueueId} + ,{'callid', CallId} + ]), {'noreply', State#state{call=Call ,delivery=Delivery ,member_call_queue=kz_json:get_value(<<"Server-ID">>, MemberCallJObj) - } - ,'hibernate'}; -handle_cast({'member_connect_re_req'}, #state{my_q=MyQ - ,my_id=MyId - ,account_id=AccountId - ,queue_id=QueueId - ,call=Call - }=State) -> + }}; + +handle_cast({'member_connect_req'}, #state{queue_id=QueueId + ,account_id=AccountId + ,my_id=MyId + ,my_q=MyQ + ,call=Call + }=State) -> send_member_connect_req(kapps_call:call_id(Call), AccountId, QueueId, MyQ, MyId), {'noreply', State}; + handle_cast({'member_connect_win', RespJObj, QueueOpts}, #state{my_q=MyQ ,my_id=MyId ,call=Call @@ -282,115 +273,43 @@ handle_cast({'timeout_agent', RespJObj}, #state{queue_id=QueueId lager:debug("timing out winning agent"), send_agent_timeout(RespJObj, Call, QueueId), {'noreply', State#state{agent_id='undefined'}, 'hibernate'}; -handle_cast({'timeout_member_call', JObj}, #state{delivery=Delivery - ,call=Call - ,shared_pid=Pid - ,member_call_queue=Q - ,account_id=AccountId - ,queue_id=QueueId - ,my_id=MyId - ,agent_id=AgentId - }=State) -> +handle_cast({'timeout_member_call', WinnerJObj}, #state{call=Call + ,queue_id=QueueId + ,agent_id=AgentId + }=State) -> lager:debug("member call has timed out, we're done"), - acdc_util:unbind_from_call_events(Call), - lager:debug("unbound from call events for ~s", [kapps_call:call_id(Call)]), - - maybe_timeout_agent(AgentId, QueueId, Call, JObj), - - publish_queue_member_remove(AccountId, QueueId, kapps_call:call_id(Call)), - acdc_queue_shared:ack(Pid, Delivery), - send_member_call_failure(Q, AccountId, QueueId, kapps_call:call_id(Call), MyId, AgentId), + maybe_timeout_agent(AgentId, QueueId, Call, WinnerJObj), + handle_call_failure(State), {'noreply', clear_call_state(State), 'hibernate'}; -handle_cast({'ignore_member_call', Call, Delivery}, #state{shared_pid=Pid}=State) -> +handle_cast({'ignore_member_call', Call, Delivery}, #state{shared_pid=SharedPid}=State) -> lager:debug("ignoring member call ~s, moving on", [kapps_call:call_id(Call)]), - acdc_util:unbind_from_call_events(Call), - lager:debug("unbound from call events for ~s", [kapps_call:call_id(Call)]), - acdc_queue_shared:ack(Pid, Delivery), + ack_and_unbind(Call, SharedPid, Delivery), {'noreply', clear_call_state(State), 'hibernate'}; -handle_cast({'exit_member_call'}, #state{delivery=Delivery - ,call=Call - ,shared_pid=Pid - ,member_call_queue=Q - ,account_id=AccountId - ,queue_id=QueueId - ,my_id=MyId - ,agent_id=AgentId - }=State) -> +handle_cast({'exit_member_call', WinnerJObj}, #state{call=Call + ,queue_id=QueueId + ,agent_id=AgentId + }=State) -> lager:debug("member call has exited the queue, we're done"), - acdc_util:unbind_from_call_events(Call), - lager:debug("unbound from call events for ~s", [kapps_call:call_id(Call)]), - publish_queue_member_remove(AccountId, QueueId, kapps_call:call_id(Call)), - acdc_queue_shared:ack(Pid, Delivery), - send_member_call_failure(Q, AccountId, QueueId, kapps_call:call_id(Call), MyId, AgentId, <<"Caller exited the queue via DTMF">>), + maybe_timeout_agent(AgentId, QueueId, Call, WinnerJObj), + handle_call_failure(State, <<"Caller exited the queue via DTMF">>), {'noreply', clear_call_state(State), 'hibernate'}; -handle_cast({'exit_member_call_empty'}, #state{delivery=Delivery - ,call=Call - ,shared_pid=Pid - ,member_call_queue=Q - ,account_id=AccountId - ,queue_id=QueueId - ,my_id=MyId - ,agent_id=AgentId - }=State) -> +handle_cast({'exit_member_call_empty'}, State) -> lager:debug("no agents left in queue to handle callers, kick everyone out"), - acdc_util:unbind_from_call_events(Call), - lager:debug("unbound from call events for ~s", [kapps_call:call_id(Call)]), - publish_queue_member_remove(AccountId, QueueId, kapps_call:call_id(Call)), - acdc_queue_shared:ack(Pid, Delivery), - send_member_call_failure(Q, AccountId, QueueId, kapps_call:call_id(Call), MyId, AgentId, <<"No agents left in queue">>), + handle_call_failure(State, <<"No agents left in queue">>), {'noreply', clear_call_state(State), 'hibernate'}; handle_cast({'finish_member_call'}, #state{call='undefined'}=State) -> {'noreply', State}; -handle_cast({'finish_member_call'}, #state{delivery=Delivery - ,call=Call - ,shared_pid=Pid - ,member_call_queue=Q - ,account_id=AccountId - ,queue_id=QueueId - ,my_id=MyId - ,agent_id=AgentId - }=State) -> +handle_cast({'finish_member_call'}, State) -> lager:debug("agent has taken care of member, we're done"), - acdc_util:unbind_from_call_events(Call), - lager:debug("unbound from call events for ~s", [kapps_call:call_id(Call)]), - acdc_queue_shared:ack(Pid, Delivery), - send_member_call_success(Q, AccountId, QueueId, MyId, AgentId, kapps_call:call_id(Call)), + handle_call_success(State), - {'noreply', clear_call_state(State), 'hibernate'}; -handle_cast({'finish_member_call', _AcceptJObj}, #state{delivery=Delivery - ,call=Call - ,shared_pid=Pid - ,member_call_queue=Q - ,account_id=AccountId - ,queue_id=QueueId - ,my_id=MyId - ,agent_id=AgentId - }=State) -> - lager:debug("agent has taken care of member, we're done"), - - acdc_util:unbind_from_call_events(Call), - lager:debug("unbound from call events for ~s", [kapps_call:call_id(Call)]), - acdc_queue_shared:ack(Pid, Delivery), - send_member_call_success(Q, AccountId, QueueId, MyId, AgentId, kapps_call:call_id(Call)), - - {'noreply', clear_call_state(State), 'hibernate'}; -handle_cast({'cancel_member_call'}, #state{delivery='undefined'}=State) -> - lager:debug("empty cancel member, no delivery info"), - {'noreply', State}; -handle_cast({'cancel_member_call'}, #state{delivery=Delivery - ,call=Call - ,shared_pid=Pid - }=State) -> - lager:debug("cancel member_call"), - - _ = maybe_nack(Call, Delivery, Pid), {'noreply', clear_call_state(State), 'hibernate'}; handle_cast({'cancel_member_call', _RejectJObj}, #state{delivery='undefined'}=State) -> lager:debug("cancel a member_call that I don't have delivery info for"), @@ -467,14 +386,56 @@ code_change(_OldVsn, State, _Extra) -> %%% Internal functions %%%============================================================================= +%%------------------------------------------------------------------------------ +%% @doc Notify various listeners about success in handling a call and stop +%% tracking events for the call. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_call_success(state()) -> 'ok'. +handle_call_success(#state{queue_id=QueueId + ,account_id=AccountId + ,shared_pid=SharedPid + ,my_id=MyId + ,member_call_queue=Q + ,call=Call + ,agent_id=AgentId + ,delivery=Delivery + }) -> + ack_and_unbind(Call, SharedPid, Delivery), + send_member_call_success(Q, AccountId, QueueId, MyId, AgentId, kapps_call:call_id(Call)). + +%%------------------------------------------------------------------------------ +%% @doc Notify various listeners about a failure to handle a call and stop +%% tracking events for the call. +%% @end +%%------------------------------------------------------------------------------ +-spec handle_call_failure(state()) -> 'ok'. +handle_call_failure(State) -> + handle_call_failure(State, 'undefined'). + +-spec handle_call_failure(state(), kz_term:api_ne_binary()) -> 'ok'. +handle_call_failure(#state{queue_id=QueueId + ,account_id=AccountId + ,shared_pid=SharedPid + ,my_id=MyId + ,member_call_queue=Q + ,call=Call + ,agent_id=AgentId + ,delivery=Delivery + }, Reason) -> + CallId = kapps_call:call_id(Call), + publish_queue_member_remove(AccountId, QueueId, CallId), + ack_and_unbind(Call, SharedPid, Delivery), + send_member_call_failure(Q, AccountId, QueueId, CallId, MyId, AgentId, Reason). + %%------------------------------------------------------------------------------ %% @doc %% @end %%------------------------------------------------------------------------------ --spec maybe_timeout_agent(kz_term:api_object(), kz_term:ne_binary(), kapps_call:call(), kz_json:object()) -> 'ok'. +-spec maybe_timeout_agent(kz_term:api_object(), kz_term:ne_binary(), kapps_call:call(), kz_term:api_object()) -> 'ok'. maybe_timeout_agent('undefined', _QueueId, _Call, _JObj) -> 'ok'; +maybe_timeout_agent(_AgentId, _QueueId, _Call, 'undefined') -> 'ok'; maybe_timeout_agent(_AgentId, QueueId, Call, JObj) -> - lager:debug("timing out winning agent because they should not be able to pick up after the queue timeout"), send_agent_timeout(JObj, Call, QueueId). -spec send_member_connect_req(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. @@ -524,8 +485,6 @@ send_member_call_success(Q, AccountId, QueueId, MyId, AgentId, CallId) -> ]), publish(Q, Resp, fun kapi_acdc_queue:publish_member_call_success/2). -send_member_call_failure(Q, AccountId, QueueId, CallId, MyId, AgentId) -> - send_member_call_failure(Q, AccountId, QueueId, CallId, MyId, AgentId, 'undefined'). send_member_call_failure(Q, AccountId, QueueId, CallId, MyId, AgentId, Reason) -> Resp = props:filter_undefined( [{<<"Account-ID">>, AccountId} @@ -581,12 +540,21 @@ maybe_nack(Call, Delivery, SharedPid) -> 'true'; 'false' -> lager:debug("call is probably not active, ack it (so its gone)"), - acdc_util:unbind_from_call_events(Call), - lager:debug("unbound from call events for ~s", [kapps_call:call_id(Call)]), - acdc_queue_shared:ack(SharedPid, Delivery), + ack_and_unbind(Call, SharedPid, Delivery), 'false' end. +%%------------------------------------------------------------------------------ +%% @doc Ack the AMQP msg delivery for a queue call and unbind from call events +%% for the call. +%% @end +%%------------------------------------------------------------------------------ +-spec ack_and_unbind(kapps_call:call(), pid(), gen_listener:basic_deliver()) -> 'ok'. +ack_and_unbind(Call, SharedPid, Delivery) -> + acdc_util:unbind_from_call_events(Call), + lager:debug("unbound from call events for ~s", [kapps_call:call_id(Call)]), + acdc_queue_shared:ack(SharedPid, Delivery). + -spec is_call_alive(kapps_call:call() | kz_term:ne_binary()) -> boolean(). is_call_alive(Call) -> case kapps_call_command:b_channel_status(Call) of @@ -599,11 +567,23 @@ is_call_alive(Call) -> end. -spec clear_call_state(state()) -> state(). -clear_call_state(#state{account_id=AccountId +clear_call_state(#state{call=Call + ,account_id=AccountId ,queue_id=QueueId }=State) -> _ = acdc_util:queue_presence_update(AccountId, QueueId), + case Call of + 'undefined' -> 'ok'; + _ -> + CallId = kapps_call:call_id(Call), + gen_listener:rm_binding(self(), 'acdc_queue', [{'restrict_to', ['member_call_result']} + ,{'account_id', AccountId} + ,{'queue_id', QueueId} + ,{'callid', CallId} + ]) + end, + kz_util:put_callid(QueueId), State#state{call='undefined' ,member_call_queue='undefined' diff --git a/applications/acdc/src/acdc_queue_manager.erl b/applications/acdc/src/acdc_queue_manager.erl index e78a4c46143..29c821d4a2a 100644 --- a/applications/acdc/src/acdc_queue_manager.erl +++ b/applications/acdc/src/acdc_queue_manager.erl @@ -205,9 +205,8 @@ handle_member_call_success(JObj, Prop) -> -spec handle_member_call_cancel(kz_json:object(), kz_term:proplist()) -> 'ok'. handle_member_call_cancel(JObj, Props) -> - kz_util:put_callid(JObj), - lager:debug("cancel call ~p", [JObj]), 'true' = kapi_acdc_queue:member_call_cancel_v(JObj), + _ = kz_util:put_callid(JObj), K = make_ignore_key(kz_json:get_value(<<"Account-ID">>, JObj) ,kz_json:get_value(<<"Queue-ID">>, JObj) ,kz_json:get_value(<<"Call-ID">>, JObj) @@ -409,14 +408,27 @@ handle_cast({'update_queue_config', JObj}, #state{enter_when_empty=_EnterWhenEmp lager:debug("maybe changing ewe from ~s to ~s", [_EnterWhenEmpty, EWE]), {'noreply', State#state{enter_when_empty=EWE}, 'hibernate'}; -handle_cast({'member_call_cancel', K, JObj}, #state{ignored_member_calls=Dict}=State) -> +handle_cast({'member_call_cancel', K, JObj}, #state{ignored_member_calls=Dict + ,current_member_calls=Calls + }=State) -> AccountId = kz_json:get_value(<<"Account-ID">>, JObj), QueueId = kz_json:get_value(<<"Queue-ID">>, JObj), CallId = kz_json:get_value(<<"Call-ID">>, JObj), Reason = kz_json:get_value(<<"Reason">>, JObj), 'ok' = acdc_stats:call_abandoned(AccountId, QueueId, CallId, Reason), + + %% For cancels triggered outside of cf_acdc_member, inform cf_acdc_member + %% proc to continue + case queue_member(CallId, Calls) of + 'undefined' -> 'ok'; + Call -> + Q = kapps_call:controller_queue(Call), + publish_member_call_failure(Q, AccountId, QueueId, CallId, Reason) + end, + {'noreply', State#state{ignored_member_calls=dict:store(K, 'true', Dict)}}; + handle_cast({'monitor_call', Call}, State) -> CallId = kapps_call:call_id(Call), gen_listener:add_binding(self(), 'call', [{'callid', CallId} @@ -508,14 +520,8 @@ handle_cast({'agent_unavailable', JObj}, State) -> handle_cast({'reject_member_call', Call, JObj}, #state{account_id=AccountId ,queue_id=QueueId }=State) -> - Prop = [{<<"Call-ID">>, kapps_call:call_id(Call)} - ,{<<"Account-ID">>, AccountId} - ,{<<"Queue-ID">>, QueueId} - ,{<<"Failure-Reason">>, <<"no agents">>} - | kz_api:default_headers(?APP_NAME, ?APP_VERSION) - ], Q = kz_json:get_value(<<"Server-ID">>, JObj), - catch kapi_acdc_queue:publish_member_call_failure(Q, Prop), + publish_member_call_failure(Q, AccountId, QueueId, kapps_call:call_id(Call), <<"no agents">>), {'noreply', State}; handle_cast({'sync_with_agent', A}, #state{account_id=AccountId}=State) -> @@ -652,6 +658,27 @@ start_secondary_queue(AccountId, QueueId) -> make_ignore_key(AccountId, QueueId, CallId) -> {AccountId, QueueId, CallId}. +-spec queue_member(kz_term:ne_binary(), list()) -> kapps_call:call() | 'undefined'. +queue_member(CallId, Calls) -> + case queue_member_lookup(CallId, Calls) of + 'undefined' -> 'undefined'; + {Call, _} -> Call + end. + +-spec queue_member_lookup(kz_term:ne_binary(), list()) -> + {kapps_call:call(), pos_integer()} | 'undefined'. +queue_member_lookup(CallId, Calls) -> + queue_member_lookup(CallId, Calls, 1). + +-spec queue_member_lookup(kz_term:ne_binary(), list(), pos_integer()) -> + {kapps_call:call(), pos_integer()} | 'undefined'. +queue_member_lookup(_, [], _) -> 'undefined'; +queue_member_lookup(CallId, [Call|Calls], Position) -> + case kapps_call:call_id(Call) of + CallId -> {Call, Position}; + _ -> queue_member_lookup(CallId, Calls, Position + 1) + end. + -spec publish_queue_member_add(kz_term:ne_binary(), kz_term:ne_binary(), kapps_call:call()) -> 'ok'. publish_queue_member_add(AccountId, QueueId, Call) -> Prop = [{<<"Account-ID">>, AccountId} @@ -670,6 +697,16 @@ publish_queue_member_remove(AccountId, QueueId, CallId) -> ], kapi_acdc_queue:publish_queue_member_remove(Prop). +-spec publish_member_call_failure(kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary(), kz_term:ne_binary()) -> 'ok'. +publish_member_call_failure(Q, AccountId, QueueId, CallId, Reason) -> + Prop = [{<<"Account-ID">>, AccountId} + ,{<<"Call-ID">>, CallId} + ,{<<"Failure-Reason">>, Reason} + ,{<<"Queue-ID">>, QueueId} + | kz_api:default_headers(?APP_NAME, ?APP_VERSION) + ], + catch kapi_acdc_queue:publish_member_call_failure(Q, Prop). + %% Really sophisticated selection algorithm -spec pick_winner(pid(), kz_json:objects(), queue_strategy(), kz_term:api_binary()) -> 'undefined' | diff --git a/applications/acdc/src/cf_acdc_member.erl b/applications/acdc/src/cf_acdc_member.erl index 76cb145f330..41887a9419c 100644 --- a/applications/acdc/src/cf_acdc_member.erl +++ b/applications/acdc/src/cf_acdc_member.erl @@ -141,7 +141,6 @@ process_message(#member_call{call=Call lager:info("call failed to be processed: ~s (took ~b s)" ,[Failure, kz_time:elapsed_s(Start)] ), - cancel_member_call(Call, Failure), stop_hold_music(Call), cf_exe:continue(Call); 'false' -> diff --git a/applications/acdc/src/kapi_acdc_agent.erl b/applications/acdc/src/kapi_acdc_agent.erl index 1759f483680..94a7dde137b 100644 --- a/applications/acdc/src/kapi_acdc_agent.erl +++ b/applications/acdc/src/kapi_acdc_agent.erl @@ -475,7 +475,7 @@ shared_call_id_v(JObj) -> shared_call_id_v(kz_json:to_proplist(JObj)). %% Member Connect Win %%------------------------------------------------------------------------------ -define(MEMBER_CONNECT_WIN_HEADERS, [<<"Queue-ID">>, <<"Agent-ID">>, <<"Call">>, <<"Agent-Process-ID">>]). --define(OPTIONAL_MEMBER_CONNECT_WIN_HEADERS, [<<"Ring-Timeout">>, <<"Caller-Exit-Key">> +-define(OPTIONAL_MEMBER_CONNECT_WIN_HEADERS, [<<"Ring-Timeout">> ,<<"Wrapup-Timeout">>, <<"CDR-Url">> ,<<"Process-ID">> ,<<"Record-Caller">>, <<"Recording-URL">> diff --git a/applications/crossbar/priv/api/swagger.json b/applications/crossbar/priv/api/swagger.json index bbcb3a7c834..036ba4e0296 100644 --- a/applications/crossbar/priv/api/swagger.json +++ b/applications/crossbar/priv/api/swagger.json @@ -6863,9 +6863,6 @@ "Call": { "type": "object" }, - "Caller-Exit-Key": { - "type": "string" - }, "Event-Category": { "enum": [ "member" diff --git a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.member_connect_win.json b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.member_connect_win.json index 58b47c0041d..8498638a8f3 100644 --- a/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.member_connect_win.json +++ b/applications/crossbar/priv/couchdb/schemas/kapi.acdc_agent.member_connect_win.json @@ -15,9 +15,6 @@ "Call": { "type": "object" }, - "Caller-Exit-Key": { - "type": "string" - }, "Event-Category": { "enum": [ "member"