funbox/smppex

Q: Any recommendations on how to process submit_sm asynchronously?

elvanja opened this issue · 3 comments

Hi,

For app I'm working on, due to business requirements, the :submit_sm processing needs to perform some external services access which takes time. Therefore, we decided to try and handle those :submit_sm's asynchronously. Currently it goes something like this:

defmodule OurSession do
  use SMPPEX.Session
  
  def handle_pdu(original_pdu, state) do
    case SMPPEX.Pdu.command_name(original_pdu) do
      :submit_sm ->
        do_handle_submit_sm_async(original_pdu, state)
        {:ok, [], state}
    end
  enddefp do_handle_submit_sm_async(original_pdu, state) do
    Task.start(fn ->
      # time intensive                                
      data = fetch_data_from_external_service(original_pdu, state)# use the external data to build response and new state
      response_pdu = build_response_pdu(original_pdu, data)
      new_state = build_new_state(original_pdu, state, data)# set new state and reply to `:submit_sm`
      :ok = SMPPEX.Session.call(state.session_pid, {:set_state, new_state})
      :ok = SMPPEX.Session.send_pdu(state.session_pid, response_pdu)
    end)
  end
end

Unfortunately, due to GenServer calls in Session.call and Session.send_pdu, we must use this trick with Task.start. I tried investigating various approached, including using Session.cast to perform this, but none worked.

Do you have any suggestions on how to achieve asynchronous :submit_sm processing, but in a more elegant manner?

Thank you for your time!

Hello!

Thank you for the feedback.

One could do something like this:

defmodule OurSession do
  use SMPPEX.Session
  
  def handle_pdu(original_pdu, state) do
    case SMPPEX.Pdu.command_name(original_pdu) do
      :submit_sm ->
        Session.cast(self, {:submit_sm_async, original_pdu})
        {:ok, [], state}
    end
  end

  def handle_cast({:submit_sm_async, original_pdu}, state) do                     
      data = fetch_data_from_external_service(original_pdu, state)response_pdu = build_response_pdu(original_pdu, data)
      new_state = build_new_state(original_pdu, state, data)
      {:noreply, [response_pdu], new_state}
  end
end

But it is quite dangerous, because Session process is the very process that handles the underlying SMPP & TCP connection, and while fetch_data_from_external_service is working, the whole SMPP session does not handle anything.

That's why your approach is absolutely legit: to do the long work in a seperate process.
I can only suggest not make two consequent calls:

   :ok = SMPPEX.Session.call(state.session_pid, {:set_state, new_state})
   :ok = SMPPEX.Session.send_pdu(state.session_pid, response_pdu)

Just make one:

   :ok = SMPPEX.Session.call(state.session_pid, {:send_response, response_pdu, new_state})

And return response from handler

  def handle_call({:send_response, response_pdu, new_state}, _from, _state) do                     
      {:reply, :ok, [response_pdu], new_state}
  end

Also an advantage of the first approach (with handle_cast({:submit_sm_async, original_pdu}, state)) is that the state keeps being handled atomically.

Excellent, this is a push in the right direction definitely 👍
Thank you for your insight! 😄