defmodule Deadline do @moduledoc "README.md" |> File.read!() |> String.split("") |> Enum.fetch!(1) @key {__MODULE__, :ctx} @doc """ Sets the deadline context. If a integer is passed it is assumed to be the desired deadline in milliseconds. This function also accepts a full deadline context. This is most commonly used when a deadline has already been set and the context needs to be propagated to another BEAM process. """ def set(nil), do: nil def set(ctx) when is_map(ctx), do: Process.put(@key, ctx) def set(deadline) when is_integer(deadline) do start = current_time() deadline = System.convert_time_unit(deadline, :millisecond, :native) ctx = %{deadline: start + deadline, start: start} Process.put(@key, ctx) end @doc """ Returns the deadline context. """ def get do Process.get(@key) end @doc """ Forces the calling process to exit if the deadline is reached. This will start a new process and that process will live as long as the calling process lives or until the deadline is reached. The extra processes should not present a problem in most cases, but it could present memory pressure in low memory environments. Accepts a callback that will be executed prior to exiting the calling process. """ def exit_after(before_exit \\ nil) do case time_remaining() do :infinity -> :ok time_remaining -> case Deadline.MonitorSupervisor.start_child(self(), time_remaining, before_exit) do {:ok, _pid} -> :ok {:error, _error} -> :error end end end @doc """ Returns the remaining time before the dealine is reached, in a given unit. Defaults to `:millisecond` units. If the deadline has been exceeded than the time remaining will be 0. """ def time_remaining(unit \\ :millisecond) do case Process.get(@key) do nil -> :infinity ctx -> # Don't allow the value to go negative max(0, to_unit(unit, ctx.deadline - current_time())) end end @doc """ Checks if a deadline has been reached or exceeded. """ def reached? do case Process.get(@key) do nil -> false ctx -> current_time() > ctx.deadline end end @doc deprecated: """ work/1 is not considered to be a safe operation. You should instead use the other primitives in Deadline, or spawn a `Task` with the specified deadline like so: Task.async(fn -> do_some_work() end) |> Task.await(Deadline.time_remaining()) """ @deprecated "Use exit_after instead." def work(f) do ctx = Process.get(@key) now = current_time() cond do ctx == nil -> f.() now > ctx.deadline -> {:error, :deadline_exceeded} true -> timeout = to_unit(:millisecond, ctx.deadline - now) {:ok, t} = :timer.exit_after(timeout, :canceled) result = f.() :timer.cancel(t) result end end defp current_time do System.monotonic_time() end defp to_unit(:native, value), do: value defp to_unit(unit, value) do System.convert_time_unit(value, :native, unit) end end