add "API" for job tag summaries
it is not a published API, as it is accessible only to site admins, but it uses the API tooling to make frontend work easier test plan: - /api/v1/jobs/tags/running should list running jobs grouped by tag, including `first_locked_at` which can be used to compute the longest runtime for any job with the tag - /api/v1/jobs/tags/queued should list runnable jobs that are not yet running, including `min_run_at` which can be used to compute the longest time any job with the tag has spent waiting - /api/v1/jobs/tags/future should list scheduled jobs grouped by tag, along with next_run_at - /api/v1/jobs/tags/failed does failed jobs, including last_failed_at for each tag - all endpoints should require site_admin read_jobs perms - endpoints should paginate like normal API endpoints flag=jobs_v2 refs DE-1041 Change-Id: I2df4f62fe7a0580a54ef5c1f48c191c5167e5d42 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/285994 Reviewed-by: Aaron Ogata <aogata@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> QA-Review: Jeremy Stanley <jeremy@instructure.com> Product-Review: Jeremy Stanley <jeremy@instructure.com>
This commit is contained in:
parent
7d0d80bdc8
commit
ad55cdb323
|
@ -18,7 +18,7 @@
|
|||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
class JobsV2Controller < ApplicationController
|
||||
before_action :require_view_jobs, only: [:index]
|
||||
before_action :require_view_jobs
|
||||
before_action :set_site_admin_context, :set_navigation, only: [:index]
|
||||
|
||||
def require_view_jobs
|
||||
|
@ -37,8 +37,71 @@ class JobsV2Controller < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
# @{not an}API List queued jobs grouped by tag
|
||||
#
|
||||
# @argument order [String,"count"|"tag"|"min_run_at"]
|
||||
# Sort column. Default is "min_run_at"
|
||||
def queued_tags
|
||||
scope = Delayed::Job.where("run_at <= now() AND (locked_by IS NULL OR locked_by = ?)", ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY)
|
||||
.select("count(*) AS count, tag, MIN(run_at) AS min_run_at")
|
||||
.group(:tag)
|
||||
.order(tags_order_clause("min_run_at ASC"))
|
||||
tag_info = Api.paginate(scope, self, api_v1_jobs_tags_queued_url)
|
||||
render json: tag_info.map { |info| { count: info.count, tag: info.tag, min_run_at: info.min_run_at } }
|
||||
end
|
||||
|
||||
# @{not an}API List running jobs grouped by tag
|
||||
#
|
||||
# @argument order [String,"count"|"tag"|"first_locked_at"]
|
||||
# Sort column. Default is "first_locked_at"
|
||||
def running_tags
|
||||
scope = Delayed::Job.running
|
||||
.select("count(*) AS count, tag, MIN(locked_at) AS first_locked_at")
|
||||
.group(:tag)
|
||||
.order(tags_order_clause("first_locked_at ASC"))
|
||||
tag_info = Api.paginate(scope, self, api_v1_jobs_tags_running_url)
|
||||
render json: tag_info.map { |info| { count: info.count, tag: info.tag, first_locked_at: info.first_locked_at } }
|
||||
end
|
||||
|
||||
# @{not an}API List future jobs grouped by tag
|
||||
#
|
||||
# @argument order [String,"count"|"tag"|"next_run_at"]
|
||||
# Sort column. Default is "next_run_at"
|
||||
def future_tags
|
||||
scope = Delayed::Job.future
|
||||
.select("count(*) AS count, tag, MIN(run_at) AS next_run_at")
|
||||
.group(:tag)
|
||||
.order(tags_order_clause("next_run_at ASC"))
|
||||
tag_info = Api.paginate(scope, self, api_v1_jobs_tags_future_url)
|
||||
render json: tag_info.map { |info| { count: info.count, tag: info.tag, next_run_at: info.next_run_at } }
|
||||
end
|
||||
|
||||
# @{not an}API List failed jobs grouped by tag
|
||||
#
|
||||
# @argument order [String,"count"|"tag"|"last_failed_at"]
|
||||
# Sort column. Default is "last_failed_at"
|
||||
def failed_tags
|
||||
scope = Delayed::Job::Failed
|
||||
.select("count(*) AS count, tag, MAX(failed_at) AS last_failed_at")
|
||||
.group(:tag)
|
||||
.order(tags_order_clause("last_failed_at DESC"))
|
||||
tag_info = Api.paginate(scope, self, api_v1_jobs_tags_failed_url)
|
||||
render json: tag_info.map { |info| { count: info.count, tag: info.tag, last_failed_at: info.last_failed_at } }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def tags_order_clause(default_order)
|
||||
case params[:order]
|
||||
when "tag"
|
||||
"tag ASC"
|
||||
when "count"
|
||||
"count DESC"
|
||||
else
|
||||
default_order
|
||||
end
|
||||
end
|
||||
|
||||
def set_navigation
|
||||
set_active_tab "jobs_v2"
|
||||
add_crumb t("#crumbs.jobs_v2", "Jobs v2")
|
||||
|
|
|
@ -2410,6 +2410,15 @@ CanvasRails::Application.routes.draw do
|
|||
get "jobs/:id", action: :show
|
||||
post "jobs/batch_update", action: :batch_update
|
||||
end
|
||||
|
||||
# jobs_v2 actually does do regular pagination, but the comments above
|
||||
# otherwise still apply
|
||||
scope(controller: :jobs_v2) do
|
||||
get "jobs/tags/queued", action: :queued_tags, as: :jobs_tags_queued
|
||||
get "jobs/tags/running", action: :running_tags, as: :jobs_tags_running
|
||||
get "jobs/tags/future", action: :future_tags, as: :jobs_tags_future
|
||||
get "jobs/tags/failed", action: :failed_tags, as: :jobs_tags_failed
|
||||
end
|
||||
end
|
||||
|
||||
# this is not a "normal" api endpoint in the sense that it is not documented
|
||||
|
|
|
@ -0,0 +1,174 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Copyright (C) 2022 - present Instructure, Inc.
|
||||
#
|
||||
# This file is part of Canvas.
|
||||
#
|
||||
# Canvas is free software: you can redistribute it and/or modify it under
|
||||
# the terms of the GNU Affero General Public License as published by the Free
|
||||
# Software Foundation, version 3 of the License.
|
||||
#
|
||||
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along
|
||||
# with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
require_relative "../api_spec_helper"
|
||||
|
||||
describe "Jobs V2 API", type: :request do
|
||||
include Api
|
||||
|
||||
describe "#queued_tags" do
|
||||
it "requires site admin" do
|
||||
api_call(:get, "/api/v1/jobs/tags/queued",
|
||||
{ controller: "jobs_v2", action: "queued_tags", format: "json" },
|
||||
{}, {}, expected_status: 401)
|
||||
end
|
||||
|
||||
context "as site admin" do
|
||||
before :once do
|
||||
site_admin_user
|
||||
|
||||
::Kernel.delay(run_at: 1.hour.ago).sleep(1)
|
||||
::Kernel.delay(run_at: 2.hours.ago).p
|
||||
::Kernel.delay(run_at: 1.day.ago).sleep(1)
|
||||
|
||||
# fake a held job to make sure it does appear
|
||||
Delayed::Job.last.update locked_by: ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY
|
||||
|
||||
# fake a running job and a held job to be sure it doesn't appear
|
||||
::Kernel.delay(run_at: 3.hours.ago).puts
|
||||
Delayed::Job.last.update locked_by: "foo", locked_at: 1.hour.ago
|
||||
end
|
||||
|
||||
it "returns queued jobs sorted by oldest" do
|
||||
json = api_call(:get, "/api/v1/jobs/tags/queued",
|
||||
{ controller: "jobs_v2", action: "queued_tags", format: "json" },
|
||||
{}, {}, expected_status: 200)
|
||||
expect(json.size).to eq 2
|
||||
expect(json[0]["tag"]).to eq "Kernel.sleep"
|
||||
expect(json[0]["count"]).to eq 2
|
||||
expect(Time.zone.parse(json[0]["min_run_at"])).to be_within(1.minute).of(1.day.ago)
|
||||
expect(json[1]["tag"]).to eq "Kernel.p"
|
||||
expect(json[1]["count"]).to eq 1
|
||||
expect(Time.zone.parse(json[1]["min_run_at"])).to be_within(1.minute).of(2.hours.ago)
|
||||
end
|
||||
|
||||
it "sorts by tag" do
|
||||
json = api_call(:get, "/api/v1/jobs/tags/queued?order=tag",
|
||||
{ controller: "jobs_v2", action: "queued_tags", format: "json", order: "tag" },
|
||||
{}, {}, expected_status: 200)
|
||||
expect(json.size).to eq 2
|
||||
expect(json[0]["tag"]).to eq "Kernel.p"
|
||||
expect(json[1]["tag"]).to eq "Kernel.sleep"
|
||||
end
|
||||
|
||||
it "sorts by count" do
|
||||
::Kernel.delay(run_at: 2.days.ago).puts
|
||||
json = api_call(:get, "/api/v1/jobs/tags/queued?order=count",
|
||||
{ controller: "jobs_v2", action: "queued_tags", format: "json", order: "count" },
|
||||
{}, {}, expected_status: 200)
|
||||
expect(json.size).to eq 3
|
||||
expect(json[0]["tag"]).to eq "Kernel.sleep"
|
||||
end
|
||||
|
||||
it "paginates" do
|
||||
json = api_call(:get, "/api/v1/jobs/tags/queued?per_page=1",
|
||||
{ controller: "jobs_v2", action: "queued_tags", format: "json", per_page: "1" },
|
||||
{}, {}, expected_status: 200)
|
||||
expect(json.map { |e| e["tag"] }).to eq ["Kernel.sleep"]
|
||||
links = Api.parse_pagination_links(response.headers["Link"])
|
||||
next_link = links.find { |link| link[:rel] == "next" }
|
||||
json = api_call(:get, next_link[:uri].path,
|
||||
{ controller: "jobs_v2", action: "queued_tags", format: "json", per_page: "1", page: "2" },
|
||||
{}, {}, expected_status: 200)
|
||||
expect(json.map { |e| e["tag"] }).to eq ["Kernel.p"]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#running_tags" do
|
||||
before :once do
|
||||
site_admin_user
|
||||
|
||||
# fake some running jobs
|
||||
::Kernel.delay.sleep 1
|
||||
Delayed::Job.last.update locked_at: 1.hour.ago, locked_by: "me"
|
||||
::Kernel.delay.sleep 1
|
||||
Delayed::Job.last.update locked_at: 2.hours.ago, locked_by: "me"
|
||||
|
||||
::Kernel.delay.p
|
||||
Delayed::Job.last.update locked_at: 30.minutes.ago, locked_by: "foo"
|
||||
|
||||
# and a fake held job, to ensure it doesn't appear here
|
||||
::Kernel.delay.puts
|
||||
Delayed::Job.last.update locked_by: ::Delayed::Backend::Base::ON_HOLD_LOCKED_BY
|
||||
end
|
||||
|
||||
it "returns running jobs" do
|
||||
json = api_call(:get, "/api/v1/jobs/tags/running",
|
||||
{ controller: "jobs_v2", action: "running_tags", format: "json" },
|
||||
{}, {}, expected_status: 200)
|
||||
expect(json.size).to eq 2
|
||||
expect(json[0]["tag"]).to eq "Kernel.sleep"
|
||||
expect(json[0]["count"]).to eq 2
|
||||
expect(Time.zone.parse(json[0]["first_locked_at"])).to be_within(1.minute).of(2.hours.ago)
|
||||
expect(json[1]["tag"]).to eq "Kernel.p"
|
||||
expect(json[1]["count"]).to eq 1
|
||||
expect(Time.zone.parse(json[1]["first_locked_at"])).to be_within(1.minute).of(30.minutes.ago)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#future_tags" do
|
||||
before :once do
|
||||
site_admin_user
|
||||
|
||||
::Kernel.delay(run_at: 1.hour.from_now).sleep(1)
|
||||
::Kernel.delay(run_at: 1.day.from_now).p
|
||||
end
|
||||
|
||||
it "returns future tags sorted by next up" do
|
||||
json = api_call(:get, "/api/v1/jobs/tags/future",
|
||||
{ controller: "jobs_v2", action: "future_tags", format: "json" },
|
||||
{}, {}, expected_status: 200)
|
||||
expect(json.size).to eq 2
|
||||
expect(json[0]["tag"]).to eq "Kernel.sleep"
|
||||
expect(json[0]["count"]).to eq 1
|
||||
expect(Time.zone.parse(json[0]["next_run_at"])).to be_within(1.minute).of(1.hour.from_now)
|
||||
expect(json[1]["tag"]).to eq "Kernel.p"
|
||||
expect(Time.zone.parse(json[1]["next_run_at"])).to be_within(1.minute).of(1.day.from_now)
|
||||
expect(json[1]["count"]).to eq 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "#failed_tags" do
|
||||
before :once do
|
||||
site_admin_user
|
||||
|
||||
Timecop.travel(1.day.ago) do
|
||||
::Kernel.delay.raise "uh oh"
|
||||
run_jobs
|
||||
end
|
||||
|
||||
Timecop.travel(1.hour.ago) do
|
||||
::Kernel.delay.raise "oops"
|
||||
run_jobs
|
||||
end
|
||||
end
|
||||
|
||||
it "returns failed jobs sorted by most recent" do
|
||||
json = api_call(:get, "/api/v1/jobs/tags/failed",
|
||||
{ controller: "jobs_v2", action: "failed_tags", format: "json" },
|
||||
{}, {}, expected_status: 200)
|
||||
expect(json.size).to eq 1
|
||||
expect(json[0]["tag"]).to eq "Kernel.raise"
|
||||
expect(json[0]["count"]).to eq 2
|
||||
expect(Time.zone.parse(json[0]["last_failed_at"])).to be_within(1.minute).of(1.hour.ago)
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue