lib/hardhat/middleware/timeout.ex

defmodule Hardhat.Middleware.Timeout do
  @doc """
  Timeout HTTP request after X milliseconds.

  Includes:
  - automatic propagation of OpenTelemetry tracing context
  - addition of OpenTelemetry span events when the timeout is exceeded

  Options:
  - `:timeout` - (required) timeout in milliseconds
  """

  alias OpentelemetryProcessPropagator.Task

  @behaviour Tesla.Middleware

  @impl Tesla.Middleware
  def call(env = %Tesla.Env{}, next, opts) do
    opts = opts || []
    configured_timeout = Keyword.fetch!(opts, :timeout)

    {timeout, is_deadline} =
      case Deadline.time_remaining() do
        :infinity -> {configured_timeout, false}
        value -> {min(value, configured_timeout), true}
      end

    task =
      safe_async(fn ->
        if is_deadline, do: Deadline.set(timeout)
        Tesla.run(env, next)
      end)

    try do
      task
      |> Task.await(timeout)
      |> repass_error
    catch
      :exit, {:timeout, _} ->
        Task.shutdown(task, 0)

        OpenTelemetry.Tracer.add_event(:timeout_exceeded,
          module: env.__module__,
          timeout: timeout
        )

        {:error, :timeout}
    end
  end

  defp safe_async(func) do
    Task.async(fn ->
      try do
        {:ok, func.()}
      rescue
        e in _ ->
          {:exception, e, __STACKTRACE__}
      catch
        type, value ->
          {type, value}
      end
    end)
  end

  defp repass_error({:exception, error, stacktrace}), do: reraise(error, stacktrace)

  defp repass_error({:throw, value}), do: throw(value)

  defp repass_error({:exit, value}), do: exit(value)

  defp repass_error({:ok, result}), do: result
end