Current section
Files
Jump to
Current section
Files
lib/oban/web/timing.ex
defmodule Oban.Web.Timing do
@moduledoc false
@empty_time "-"
@doc """
Snap unix time to an even value for a given increment.
iex> Oban.Web.Timing.snap(1685707957, 1)
1685707957
iex> Oban.Web.Timing.snap(1685707957, 5)
1685707960
iex> Oban.Web.Timing.snap(1685707957, 30)
1685707980
"""
def snap(unix, step) when rem(unix, step) == 0, do: unix
def snap(unix, step), do: snap(unix + 1, step)
@doc """
Format ellapsed seconds into a timer format of "MM:SS" or "HH:MM:SS".
iex> Oban.Web.Timing.to_duration(0)
"00:00"
iex> Oban.Web.Timing.to_duration(5)
"00:05"
iex> Oban.Web.Timing.to_duration(-5)
"00:05"
iex> Oban.Web.Timing.to_duration(60)
"01:00"
iex> Oban.Web.Timing.to_duration(65)
"01:05"
iex> Oban.Web.Timing.to_duration(7199)
"01:59:59"
"""
def to_duration(ellapsed) when is_integer(ellapsed) do
ellapsed = abs(ellapsed)
seconds = Integer.mod(ellapsed, 60)
minutes = ellapsed |> Integer.mod(3_600) |> div(60)
hours = div(ellapsed, 3_600)
parts = [minutes, seconds]
parts = if hours > 0, do: [hours | parts], else: parts
Enum.map_join(parts, ":", &pad/1)
end
@doc """
Format elapsed seconds with trailing milliseconds.
iex> Oban.Web.Timing.to_duration(0, :millisecond)
"00:00.000"
iex> Oban.Web.Timing.to_duration(5, :millisecond)
"00:00.005"
iex> Oban.Web.Timing.to_duration(-5, :millisecond)
"00:00.005"
iex> Oban.Web.Timing.to_duration(61030, :millisecond)
"01:01.030"
iex> Oban.Web.Timing.to_duration(61930, :millisecond)
"01:01.930"
"""
def to_duration(ellapsed, :millisecond) do
milliseconds =
ellapsed
|> abs()
|> Integer.mod(1000)
|> pad(3)
ellapsed
|> div(1000)
|> to_duration()
|> Kernel.<>(".#{milliseconds}")
end
@doc """
Format ellapsed seconds into a wordy format, based on "distance of time in words".
iex> Oban.Web.Timing.to_words(0)
"now"
iex> Oban.Web.Timing.to_words(1)
"in 1s"
iex> Oban.Web.Timing.to_words(-1)
"1s ago"
iex> Oban.Web.Timing.to_words(-5)
"5s ago"
iex> Oban.Web.Timing.to_words(60)
"in 1m"
iex> Oban.Web.Timing.to_words(121)
"in 2m"
iex> Oban.Web.Timing.to_words(-60 * 60)
"1h ago"
iex> Oban.Web.Timing.to_words(60 * 60)
"in 1h"
iex> Oban.Web.Timing.to_words((60 * 60 * 24) - 1)
"in 23h"
iex> Oban.Web.Timing.to_words(60 * 60 * 24)
"in 1d"
iex> Oban.Web.Timing.to_words(60 * 60 * 24 * 5)
"in 5d"
iex> Oban.Web.Timing.to_words(60 * 60 * 24 * 30)
"in 1mo"
iex> Oban.Web.Timing.to_words(60 * 60 * 24 * 30 * 5)
"in 5mo"
iex> Oban.Web.Timing.to_words(60 * 60 * 24 * 365)
"in 1yr"
iex> Oban.Web.Timing.to_words(60 * 60 * 24 * 365 * 2)
"in 2yr"
The `:relative` option controls whether an "in" or "ago" modifier is added:
iex> Oban.Web.Timing.to_words(1, relative: false)
"1s"
iex> Oban.Web.Timing.to_words(-1, relative: false)
"1s"
"""
def to_words(ellapsed, opts \\ [relative: true]) when is_integer(ellapsed) do
distance =
case abs(ellapsed) do
0 -> "now"
n when n in 1..59 -> "#{n}s"
n when n in 60..3_599 -> "#{div(n, 60)}m"
n when n in 3_600..86_399 -> "#{div(n, 3_600)}h"
n when n in 86_400..2_591_999 -> "#{div(n, 86_400)}d"
n when n in 2_592_000..31_535_999 -> "#{div(n, 2_592_000)}mo"
n -> "#{div(n, 31_536_000)}yr"
end
cond do
not opts[:relative] -> distance
ellapsed < 0 -> "#{distance} ago"
ellapsed > 0 -> "in #{distance}"
true -> distance
end
end
@doc """
Calculate the amount of time a job waited between availability and execution.
iex> Oban.Web.Timing.queue_time(%{attempted_at: nil})
"-"
iex> at = DateTime.utc_now()
...> Oban.Web.Timing.queue_time(%{attempted_at: at, inserted_at: at, scheduled_at: at})
"00:00.000"
iex> at_at = DateTime.utc_now()
...> sc_at = DateTime.add(at_at, -60)
...> Oban.Web.Timing.queue_time(%{attempted_at: at_at, inserted_at: sc_at, scheduled_at: sc_at})
"01:00.000"
iex> at_at = DateTime.utc_now()
...> sc_at = DateTime.add(at_at, -60)
...> Oban.Web.Timing.queue_time(%{attempted_at: at_at, inserted_at: sc_at, scheduled_at: sc_at})
"01:00.000"
iex> at_at = DateTime.utc_now()
...> in_at = DateTime.add(at_at, -60)
...> sc_at = DateTime.add(at_at, -90)
...> Oban.Web.Timing.queue_time(%{attempted_at: at_at, inserted_at: in_at, scheduled_at: sc_at})
"01:00.000"
"""
def queue_time(%{attempted_at: nil}), do: @empty_time
def queue_time(job) do
scheduled_or_inserted =
if DateTime.compare(job.scheduled_at, job.inserted_at) == :gt do
job.scheduled_at
else
job.inserted_at
end
job.attempted_at
|> DateTime.diff(scheduled_or_inserted, :millisecond)
|> to_duration(:millisecond)
end
@doc """
Calculate the amount of time a job executed before completing or discarded.
iex> Oban.Web.Timing.run_time(%{attempted_at: nil})
"-"
iex> Oban.Web.Timing.run_time(%{completed_at: nil, discarded_at: nil})
"-"
iex> at = DateTime.utc_now()
...> Oban.Web.Timing.run_time(%{attempted_at: at, completed_at: at})
"00:00.000"
iex> at_at = DateTime.utc_now()
...> co_at = DateTime.add(at_at, -60)
...> Oban.Web.Timing.run_time(%{attempted_at: at_at, completed_at: co_at})
"01:00.000"
iex> at_at = DateTime.utc_now()
...> di_at = DateTime.add(at_at, -60)
...> Oban.Web.Timing.run_time(%{attempted_at: at_at, completed_at: nil, discarded_at: di_at})
"01:00.000"
"""
def run_time(%{attempted_at: nil}), do: @empty_time
def run_time(%{completed_at: nil, discarded_at: nil}), do: @empty_time
def run_time(job) do
finished_at = job.completed_at || job.discarded_at
job.attempted_at
|> DateTime.diff(finished_at, :millisecond)
|> to_duration(:millisecond)
end
@doc """
Select an absolute timestamp appropriate for the provided state and format it.
"""
@spec absolute_time(String.t(), Job.t()) :: String.t()
def absolute_time(state, job) do
case state do
"available" -> "Available At: #{truncate_sec(job.scheduled_at)}"
"cancelled" -> "Cancelled At: #{truncate_sec(job.cancelled_at)}"
"completed" -> "Completed At: #{truncate_sec(job.completed_at)}"
"discarded" -> "Discarded At: #{truncate_sec(job.discarded_at)}"
"executing" -> "Attempted At: #{truncate_sec(job.attempted_at)}"
"retryable" -> "Retryable At: #{truncate_sec(job.scheduled_at)}"
"scheduled" -> "Scheduled At: #{truncate_sec(job.scheduled_at)}"
end
end
defp truncate_sec(datetime), do: NaiveDateTime.truncate(datetime, :second)
@doc """
Convert a stringified timestamp (i.e. from an error) into a relative time.
"""
def iso8601_to_words(iso8601, now \\ NaiveDateTime.utc_now()) do
{:ok, datetime} = NaiveDateTime.from_iso8601(iso8601)
datetime
|> NaiveDateTime.diff(now)
|> to_words()
end
defp pad(time, places \\ 2) do
time
|> to_string()
|> String.pad_leading(places, "0")
end
end