Skip to content

Allow querying the state of scheduled products by distri/version/flavor #6592

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions lib/OpenQA/Schema/ResultSet/ScheduledProducts.pm
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,90 @@ sub cancel_by_webhook_id ($self, $webhook_id, $reason) {
return {jobs_cancelled => $count};
}

sub job_statistics ($self, $distri, $version, $flavor) {
my $sth = $self->result_source->schema->storage->dbh->prepare(
<<~'END_SQL'
WITH RECURSIVE
-- get the initial set of jobs in the scheduled product
initial_job_ids AS (
SELECT
jobs.id AS job_id,
jobs.scheduled_product_id AS scheduled_product_id
FROM
jobs
WHERE
jobs.scheduled_product_id in (
SELECT
max(id)
FROM
scheduled_products
WHERE
status in ('new', 'scheduling', 'scheduled') and distri = ? and version = ? and flavor = ?
GROUP BY
arch
)
),
-- find more recent jobs for each initial job recursively
latest_id_resolver AS (
-- start with each job_id from initial_job_ids
SELECT
ij.job_id,
ij.job_id AS latest_job_id,
ij.scheduled_product_id AS scheduled_product_id,
1 AS level
FROM
initial_job_ids AS ij
UNION ALL
-- find the clone_id for the current latest_job_id
SELECT
lir.job_id,
j.clone_id AS latest_job_id,
lir.scheduled_product_id AS scheduled_product_id,
lir.level + 1 AS level
FROM
jobs AS j
JOIN latest_id_resolver AS lir ON lir.latest_job_id = j.id
-- limit the recursion
WHERE
lir.level < 50
),
-- filter jobs to only get the latest
most_recent_jobs AS (
SELECT DISTINCT ON (job_id)
job_id as initial_job_id,
latest_job_id,
mrj.state as latest_job_state,
mrj.result as latest_job_result,
mrj.scheduled_product_id as scheduled_product_id,
level as chain_length
FROM
latest_id_resolver
JOIN jobs AS mrj ON mrj.id = latest_job_id
WHERE
latest_job_id IS NOT NULL
ORDER BY
job_id,
level DESC
)
SELECT
latest_job_state,
latest_job_result,
array_agg(latest_job_id) as job_ids,
array_agg(DISTINCT scheduled_product_id) as scheduled_product_ids
FROM
most_recent_jobs
WHERE
latest_job_id IS NOT NULL
GROUP BY
latest_job_state,
latest_job_result
END_SQL
);
$sth->bind_param(1, $distri);
$sth->bind_param(2, $version);
$sth->bind_param(3, $flavor);
$sth->execute;
return $sth->fetchall_hashref([qw(latest_job_state latest_job_result)]);
}

1;
1 change: 1 addition & 0 deletions lib/OpenQA/WebAPI.pm
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ sub startup ($self) {
$api_ro->post('/isos')->name('apiv1_create_iso')->to('iso#create');
$api_ra->delete('/isos/#name')->name('apiv1_destroy_iso')->to('iso#destroy');
$api_ro->post('/isos/#name/cancel')->name('apiv1_cancel_iso')->to('iso#cancel');
$api_ro->get('/isos/job_stats')->name('apiv1_scheduled_product_job_stats')->to('iso#job_statistics');

# api/v1/webhooks
$api_ro->post('/webhooks/product')->name('apiv1_evaluate_webhook_product')->to('webhook#product');
Expand Down
42 changes: 42 additions & 0 deletions lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,48 @@ sub show_scheduled_product {
$self->render(json => $scheduled_product->to_hash(@args));
}

=over 4

=item job_statistics()

Returns job statistics about the most recent scheduled products for each ARCH
matching the specified DISTRI, VERSION and FLAVOR. Only scheduled products that
are cancelling/cancelled are not considered.

This allows to determine whether all jobs that have been scheduled for a
certain purpose are done and whether the jobs have passed. If jobs have been
cloned/restarted then only the state/result of the latest job is taken into
account.

The statistics are returned as nested JSON object with one key per present state
on outer level and one key per present result on inner level:

{
done => {
failed => {job_ids => [5057], scheduled_product_ids => [330]},
incomplete => {job_ids => [5056], scheduled_product_ids => [330]},
}
}

One can check for the existence of keys in the returned JSON object to check
whether certain states/results are present. The concrete job IDs and scheduled
product IDs for each combination are mainly returned for easier retracing but
could also be used to generate a more detailed report.

=back

=cut

sub job_statistics ($self) {
my $validation = $self->validation;
my @param_keys = (qw(distri version flavor));
$validation->required($_) for @param_keys;
return $self->reply->validation_error({format => 'json'}) if $validation->has_error;
my @params = map { $validation->param($_) } @param_keys;
my $scheduled_products = $self->app->schema->resultset('ScheduledProducts');
$self->render(json => $scheduled_products->job_statistics(@params));
}

sub validate_create_parameters ($self) {
my $validation = $self->validation;
$validation->required($_) for (MANDATORY_PARAMETERS);
Expand Down
20 changes: 20 additions & 0 deletions t/api/02-iso.t
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use FindBin;
use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../../external/os-autoinst-common/lib";
use Test::Mojo;
use Test::Warnings ':report_warnings';
use OpenQA::Jobs::Constants;
use OpenQA::JobDependencies::Constants;
use OpenQA::Test::TimeLimit '300';
use OpenQA::Test::Case;
Expand Down Expand Up @@ -309,6 +310,25 @@ is($server_64->{settings}->{PRECEDENCE}, 'overridden', "precedence override (sui

lj;

subtest 'job statistics can be queried about the scheduled product' => sub {
$schema->txn_begin;
# assume some of the scheduled jobs are already done
$jobs->find(99985)->update({state => DONE, result => INCOMPLETE});
$jobs->find(99988)->update({state => DONE, result => FAILED});
$jobs->find(99993)->update({state => DONE, result => PASSED});
$jobs->find(99994)->update({state => DONE, result => PASSED});
$t->get_ok('/api/v1/isos/job_stats?distri=opensuse&version=13.1&flavor=DVD')->status_is(200);
$schema->txn_rollback;
my $json = $t->tx->res->json;
is_deeply [sort keys %$json], [DONE, SCHEDULED], 'expected states present';
is_deeply [sort keys %{$json->{done}}], [FAILED, INCOMPLETE, PASSED], 'expected results present';
is_deeply [sort @{$json->{done}->{failed}->{job_ids}}], [99988], 'failed jobs';
is_deeply [sort @{$json->{done}->{incomplete}->{job_ids}}], [99985], 'incomplete jobs';
is_deeply [sort @{$json->{done}->{passed}->{job_ids}}], [99993, 99994], 'passed jobs';
is_deeply [sort @{$json->{scheduled}->{none}->{job_ids}}], [99986, 99987, 99989, 99990, 99991, 99992],
'scheduled jobs';
};

subtest 'old tests are cancelled unless they are marked as important' => sub {
$t->get_ok('/api/v1/jobs/99927')->status_is(200);
is($t->tx->res->json->{job}->{state}, 'cancelled', 'job 99927 is cancelled');
Expand Down