How to do proper request timeouts with Phoenix?

I’m comming from a Go background and this kind of thing in Go is trivial, but i’m struggling to do the same with Phoenix. I want a middleware that for each request it track the time of execution, if its greater then a given value (1s per example) it should render immediately a json with a timeout message and stop the underneath processing.

It’s easy track the timeout and render the message to the client, the problem is i’m doing double writes at the connection:

[info] GET /resources
[debug] Processing with Flare.Resource.Handler.index/2
  Parameters: %{}
  Pipelines: [:api]
[info] Sent 408 in 501ms
[debug] QUERY OK source="resources" db=0.8ms decode=0.1ms queue=0.1ms
SELECT r0."id", r0."addresses", r0."path", r0."change", r0."inserted_at", r0."updated_at" FROM "resources" AS r0 []
[info] Sent 200 in 1003ms

The timeout middleware wait 500ms and the controller has a 1s sleep. How can i prevent the controller from writing if the middleware already writed?

Without knowing how you implemented what part of your timeout stuff, we can’t help you.

Can you perhaps create an example app which shows your problem?


Anyway, I do not think it is a good think to cancel half finished requests from the server side. If done wrong the state could end up inconsistent.

Creating rollback machanisms might be much harder than the actual timeout.

Simply processing the request from start to finish and handle timeouts at the clientside should be the way to go in my opinion.

Its a client requirement, the endpoint should have a fixed timeout, so, after a period of time, even if the request don’t complete because a server overload or database slowdown, a answer should be sent to the client informing the timeout.

As i’m not yet familiar with Elixir/Plug architecture, the double write was a side effect of a bad programmed Plug. I deleted the code, but was something like this:

defmodule Timeout do
  import Plug.Conn
  
  def init(options) do
    options
  end

  def call(conn, _opts) do
    Task.async(fn -> 
      :timer.sleep(5000)
      
      conn
      |> put_resp_content_type("text/plain")
      |> send_resp(408, "timeout")
    end)

    conn
  end
end

I know the plug is wrong, the connection is already released when the task executes. I’m gonna talk with the client to see if this can be done client side.

But i’m with this tech doubt now. This can be done without changing too much the code? Like before each render check any global struct to see if the request is already timeout. Or any kind of callback to ignore the content writed on a connection?

Tried to use the register_before_send, but no success too. Can’t override the connection write.

The important question is if only a response should be sent or if the triggered work should get canceled as well. Also you might want to look into the Task module and OTP, because you could just offload the hard work into another process and leave your request livecycle to only care about handing the timeout requirement.

1 Like

Shall this happen everywhere or only at certain places?

If at certain places is enough, you could just use a Task for the hardwork and use Task.yield/2 to wait for the result.

Roughly like this:

t = Task.async(&do_hardworkd/0)
case Task.yield(t, 1000) do # timeout of a second
  {:ok, result} -> handle_result(result)
  nil -> handle_timeout()
  {:exit, reason} -> handle_error(reason)
end
1 Like

Everywhere, it’s a middleware that should stay at the pipeline to executes in all requests. But i don’t think this is possible, seems a plug limitation, after the conn is returned from the function, we loose 100% the control of it, the only way is to instrument everywhere the connections is used to check if first it’s already writed.

But, nevermind, we gonna handle this at client side, was just curious about how to do.

Doesn’t seem too hard to do, but if requests take long then ‘those’ areas should probably have timeouts, not the conn itself (and most timeouts do exist in the system and default to 5s).

However, to do it at the plug layer I’d make a plug that creates messages a timing process it’s own pid and how long, then that other process would wait then kill that pid after the time is elapsed. Should also have an after_plug or whatever it was called function registered to send a cancel message too, just to clean things up a bit faster.

One question, it’s possible to use defdelegate to override the render inside controllers? If this is possible, i could create a module that receive the renders, check in a agent if the connection already writed or got a timeout and write the proper response to the client.

Tried sometimes, but didn’t worked.

slightly offtopic: i was looking into Context package (i believe that’s the thing that makes it possible for OPs “feature” in golang to maintain state, in this case time/out and make early returns, rollback/cancel requests) because i thought it would be cool to implement it in Elixir. i thought it was for app level use cases then i read it should be used only for server requests. then i realized it overlaps with some of plug especially plug.conn use case. so i got here looking for confirmation and seems indeed they are.

i just find it cool that both packages(context, plug) show the paradigms of the languages they were created in. whereas in Context package, it is idiomatic for users to trigger early returns and cancel concurrent computation when an error case encountered. in Plug, it is the functional way of going through the whole pipeline even when the request has already “failed” in some part of it.

1 Like