Skip to content

Commit

Permalink
Merge pull request #64 from evilmarty/job-actions
Browse files Browse the repository at this point in the history
Add ability to retry and cancel jobs
  • Loading branch information
evilmarty authored Nov 11, 2024
2 parents db7c26b + 1d78255 commit 9a6bef1
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 27 deletions.
100 changes: 75 additions & 25 deletions lib/oban/live_dashboard.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,30 +41,40 @@ defmodule Oban.LiveDashboard do
title="Job"
return_to={live_dashboard_path(@socket, @page, params: %{})}
>
<.label_value_list>
<:elem label="ID"><%= @job.id %></:elem>
<:elem label="State"><%= @job.state %></:elem>
<:elem label="Queue"><%= @job.queue %></:elem>
<:elem label="Worker"><%= @job.worker %></:elem>
<:elem label="Args"><%= format_value(@job.args, nil) %></:elem>
<:elem :if={@job.meta != %{}} label="Meta"><%= format_value(@job.meta, nil) %></:elem>
<:elem :if={@job.tags != []} label="Tags"><%= format_value(@job.tags, nil) %></:elem>
<:elem :if={@job.errors != []} label="Errors"><%= format_errors(@job.errors) %></:elem>
<:elem label="Attempts"><%= @job.attempt %>/<%= @job.max_attempts %></:elem>
<:elem label="Priority"><%= @job.priority %></:elem>
<:elem label="Attempted at"><%= format_value(@job.attempted_at) %></:elem>
<:elem :if={@job.cancelled_at} label="Cancelled at">
<%= format_value(@job.cancelled_at) %>
</:elem>
<:elem :if={@job.completed_at} label="Completed at">
<%= format_value(@job.completed_at) %>
</:elem>
<:elem :if={@job.discarded_at} label="Discarded at">
<%= format_value(@job.discarded_at) %>
</:elem>
<:elem label="Inserted at"><%= format_value(@job.inserted_at) %></:elem>
<:elem label="Scheduled at"><%= format_value(@job.scheduled_at) %></:elem>
</.label_value_list>
<div class="mb-4 btn-toolbar" role="toolbar" aria-label="Oban Job actions">
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm mr-2" phx-click="run_job" phx-value-job={@job.id} disabled={!can_retry_job?(@job)}>Retry Job</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-primary btn-sm" phx-click="cancel_job" phx-value-job={@job.id} disabled={!can_cancel_job?(@job)}>Cancel Job</button>
</div>
</div>
<div class="tabular-info">
<.label_value_list>
<:elem label="ID"><%= @job.id %></:elem>
<:elem label="State"><%= @job.state %></:elem>
<:elem label="Queue"><%= @job.queue %></:elem>
<:elem label="Worker"><%= @job.worker %></:elem>
<:elem label="Args"><%= format_value(@job.args, nil) %></:elem>
<:elem :if={@job.meta != %{}} label="Meta"><%= format_value(@job.meta, nil) %></:elem>
<:elem :if={@job.tags != []} label="Tags"><%= format_value(@job.tags, nil) %></:elem>
<:elem :if={@job.errors != []} label="Errors"><%= format_errors(@job.errors) %></:elem>
<:elem label="Attempts"><%= @job.attempt %>/<%= @job.max_attempts %></:elem>
<:elem label="Priority"><%= @job.priority %></:elem>
<:elem label="Attempted at"><%= format_value(@job.attempted_at) %></:elem>
<:elem :if={@job.cancelled_at} label="Cancelled at">
<%= format_value(@job.cancelled_at) %>
</:elem>
<:elem :if={@job.completed_at} label="Completed at">
<%= format_value(@job.completed_at) %>
</:elem>
<:elem :if={@job.discarded_at} label="Discarded at">
<%= format_value(@job.discarded_at) %>
</:elem>
<:elem label="Inserted at"><%= format_value(@job.inserted_at) %></:elem>
<:elem label="Scheduled at"><%= format_value(@job.scheduled_at) %></:elem>
</.label_value_list>
</div>
</.live_modal>
"""
end
Expand All @@ -85,20 +95,56 @@ defmodule Oban.LiveDashboard do
{:noreply, assign(socket, job: nil)}
end

@impl true
def handle_refresh(socket) do
{:noreply,
Phoenix.Component.update(socket, :job, fn
nil -> nil
%{id: job_id} -> get_job(job_id)
end)}
end

@impl true
def handle_event("show_job", params, socket) do
to = live_dashboard_path(socket, socket.assigns.page, params: params)
{:noreply, push_patch(socket, to: to)}
end

def handle_event("run_job", %{"job" => job_id}, socket) do
with {:ok, job} <- fetch_job(job_id),
:ok <- Oban.Engine.retry_job(Oban.config(), job),
# Refresh job
{:ok, job} <- fetch_job(job.id) do
{:noreply, assign(socket, :job, job)}
else
_ ->
{:noreply, socket}
end
end

def handle_event("cancel_job", %{"job" => job_id}, socket) do
with {:ok, job} <- fetch_job(job_id),
:ok <- Oban.Engine.cancel_job(Oban.config(), job) do
to = live_dashboard_path(socket, socket.assigns.page, params: %{})
{:noreply, push_patch(socket, to: to)}
else
_ ->
{:noreply, socket}
end
end

defp get_job(id) do
Oban.Repo.get(Oban.config(), Oban.Job, id)
end

defp fetch_jobs(params, _node) do
total_jobs = Oban.Repo.aggregate(Oban.config(), Oban.Job, :count)
jobs = Oban.Repo.all(Oban.config(), jobs_query(params)) |> Enum.map(&Map.from_struct/1)
{jobs, total_jobs}
end

defp fetch_job(id) do
case Oban.Repo.get(Oban.config(), Oban.Job, id) do
case get_job(id) do
nil ->
:error

Expand All @@ -107,6 +153,10 @@ defmodule Oban.LiveDashboard do
end
end

defp can_retry_job?(%Oban.Job{state: state}), do: state not in ["available", "executing"]

defp can_cancel_job?(%Oban.Job{state: state}), do: state != "cancelled"

defp jobs_query(%{sort_by: sort_by, sort_dir: sort_dir, limit: l}) do
Oban.Job
|> limit(^l)
Expand Down
22 changes: 20 additions & 2 deletions test/oban/live_dashboard_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,26 @@ defmodule Oban.LiveDashboardTest do
refute live |> element("#modal-close") |> render_click() =~ "modal"
end

defp job_fixture(args \\ %{}) do
{:ok, job} = Oban.Job.new(args, worker: "FakeWorker") |> Oban.insert()
test "retry job from modal" do
job = job_fixture(%{something: "foobar"}, schedule_in: 1000)
{:ok, live, _rendered} = live(build_conn(), "/dashboard/oban?params[job]=#{job.id}")

assert has_element?(live, "pre", "scheduled")
element(live, "button", "Retry Job") |> render_click()
assert has_element?(live, "pre", "available")
end

test "cancel job from modal" do
job = job_fixture(%{something: "foobar"})
{:ok, live, _rendered} = live(build_conn(), "/dashboard/oban?params[job]=#{job.id}")

element(live, "button", "Cancel Job") |> render_click()
assert_patched(live, "/dashboard/oban?")
end

defp job_fixture(args \\ %{}, opts \\ []) do
opts = Keyword.put_new(opts, :worker, "FakeWorker")
{:ok, job} = Oban.Job.new(args, opts) |> Oban.insert()
job
end
end

0 comments on commit 9a6bef1

Please sign in to comment.