Packages

Admin scripts to keep your Oban Workers sane

Current section

Files

Jump to
oban_hydro lib oban_hydro.ex
Raw

lib/oban_hydro.ex

defmodule ObanHydro do
@moduledoc """
Helpers to administrate or oversee Oban Workers
Or, in other words, a series of functions to help you find
worrisome Oban configurations that might need a visit to
the [Oban Hydro Sanatorium].
[Oban Hydro Sanatorium]: https://en.wikipedia.org/wiki/Oban_Hydro
"""
@doc """
Find Oban Workers associated with the given queues
"""
def workers_by_queues(queue, app) when is_binary(queue) or is_atom(queue) do
queue
|> List.wrap()
|> workers_by_queues(app)
end
def workers_by_queues(queues, app) when is_list(queues) do
target_queues = Enum.map(queues, &to_string/1)
app
|> find_oban_workers()
|> Enum.group_by(
fn {worker, _attributes} ->
worker.__opts__()
|> Keyword.get(:queue, :default)
|> to_string()
end,
&elem(&1, 0)
)
|> Map.filter(fn {queue, _} -> queue in target_queues end)
end
@doc """
Find Oban Workers with _custom period_ in their `unique` option
The `unique` option is not always needed. So, I tend to review
these worker definitions to see why we are using a custom period,
when the [recommended value][Scaling Applications], after checking that the unique
feature is required, for `period` is `:infinity`.
[Scaling Applications]: https://hexdocs.pm/oban/scaling.html#uniqueness
"""
def unique_workers_with_custom_period(app) do
app
|> find_oban_workers()
|> Enum.flat_map(fn {worker, _attributes} ->
unique = Keyword.get(worker.__opts__(), :unique)
case unique && unique[:period] do
:infinity -> []
nil -> []
period -> [{worker, period}]
end
end)
|> Enum.group_by(&elem(&1, 1), &elem(&1, 0))
end
@doc """
Find unique workers without the `keys` option
As suggested in the [Scaling Applications] guide, first make
sure you require the unique jobs feature, and if you do,
always set the `keys` option so that uniqueness isn't based on
the complete `args` or `meta`
[Scaling Applications]: https://hexdocs.pm/oban/scaling.html#uniqueness
"""
def unique_workers_without_keys_option(app) do
app
|> find_oban_workers()
|> Enum.flat_map(fn {worker, _attributes} ->
unique = Keyword.get(worker.__opts__(), :unique)
if unique && unique[:keys] != [], do: [worker], else: []
end)
end
@doc """
Classify workers based on "Unique State Groups"
These Unique State Groups are available since Oban [v2.20.0].
If one or more workers don't fall into the following unique groups:
`all`, `incomplete`, `scheduled`, or `successful`, you should deeply
review of your worker module definition.
[v2.20.0]: https://github.com/oban-bg/oban/releases/tag/v2.20.0
"""
def workers_by_unique_state_groups(app) do
all = MapSet.new(~w(available scheduled executing retryable completed cancelled discarded)a)
successful = MapSet.new(~w(available scheduled executing retryable completed)a)
incomplete = MapSet.new(~w(available scheduled executing retryable)a)
scheduled = MapSet.new(~w(scheduled)a)
app
|> find_oban_workers()
|> Enum.flat_map(fn {worker, _attributes} ->
unique = Keyword.get(worker.__opts__(), :unique)
case unique && unique[:states] do
states when is_list(states) ->
states = MapSet.new(states)
states =
cond do
MapSet.equal?(states, all) -> :all
MapSet.equal?(states, successful) -> :successful
MapSet.equal?(states, incomplete) -> :incomplete
MapSet.equal?(states, scheduled) -> :scheduled
true -> states |> MapSet.to_list() |> Enum.sort()
end
[{worker, states}]
_ ->
[]
end
end)
|> Enum.group_by(&elem(&1, 1), &elem(&1, 0))
end
@doc """
Find workers without function wrappers
## Options
* `names` - specifies the name of the wrappers. Default: `["enqueue"]`
"""
def workers_without_wrappers(app, opts \\ []) do
wrapper_names =
opts
|> Keyword.get(:names, "enqueue")
|> List.wrap()
|> MapSet.new()
app
|> find_oban_workers()
|> Enum.flat_map(fn {worker, attributes} ->
function_names =
attributes
|> Keyword.fetch!(:functions)
|> MapSet.new(&(&1 |> elem(0) |> Atom.to_string()))
intersection = MapSet.intersection(wrapper_names, function_names)
if MapSet.equal?(intersection, wrapper_names) do
[]
else
non_implemented =
wrapper_names
|> MapSet.difference(intersection)
|> MapSet.to_list()
[{worker, non_implemented}]
end
end)
|> Enum.group_by(&elem(&1, 1), &elem(&1, 0))
end
@doc """
List all the Oban Workers for the given BEAM directory
"""
def find_oban_workers(beam_dir) do
[
beam_dir,
Path.join(beam_dir, "../../oban/ebin"),
Path.join(beam_dir, "../../oban_pro/ebin")
]
|> Enum.map(&Path.expand/1)
|> Enum.filter(&File.exists?/1)
|> Code.prepend_paths()
"*.beam"
|> Path.expand(beam_dir)
|> Path.wildcard()
|> Enum.map(&(&1 |> Path.basename(".beam") |> String.to_atom()))
|> Enum.flat_map(fn module ->
with {^module, binary, _file} <- :code.get_object_code(module),
{:ok, {^module, chunk_data}} <- :beam_lib.chunks(binary, [:attributes, :exports]),
true <-
chunk_data
|> get_in([:attributes, :behaviour])
|> List.wrap()
|> Enum.any?(&(&1 == Oban.Worker)) do
[{module, functions: Keyword.fetch!(chunk_data, :exports)}]
else
_ -> []
end
end)
end
end