Skip to content

Commit 76fd1bf

Browse files
committed
Allow querying the state of scheduled products by distri/version/flavor
This will allow us to check whether all jobs we scheduled for a certain purpose (e.g. product increment) are done and whether they have passed. The details of the lookup are explained in the added API documentation. The query for this is not super efficient as we don't have an index on the required columns on the scheduled products table. However, it seemed to be fast enough when I tested this with production data. Related ticket: https://progress.opensuse.org/issues/184690
1 parent 87a4d34 commit 76fd1bf

File tree

4 files changed

+149
-0
lines changed

4 files changed

+149
-0
lines changed

lib/OpenQA/Schema/ResultSet/ScheduledProducts.pm

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,90 @@ sub cancel_by_webhook_id ($self, $webhook_id, $reason) {
3030
return {jobs_cancelled => $count};
3131
}
3232

33+
sub job_statistics ($self, $distri, $version, $flavor) {
34+
my $sth = $self->result_source->schema->storage->dbh->prepare(
35+
<<~'END_SQL'
36+
WITH RECURSIVE
37+
-- get the initial set of jobs in the scheduled product
38+
initial_job_ids AS (
39+
SELECT
40+
jobs.id AS job_id,
41+
jobs.scheduled_product_id AS scheduled_product_id
42+
FROM
43+
jobs
44+
WHERE
45+
jobs.scheduled_product_id in (
46+
SELECT
47+
max(id)
48+
FROM
49+
scheduled_products
50+
WHERE
51+
status = 'scheduled' and distri = ? and version = ? and flavor = ?
52+
GROUP BY
53+
arch
54+
)
55+
),
56+
-- find more recent jobs for each initial job recursively
57+
latest_id_resolver AS (
58+
-- start with each job_id from initial_job_ids
59+
SELECT
60+
ij.job_id,
61+
ij.job_id AS latest_job_id,
62+
ij.scheduled_product_id AS scheduled_product_id,
63+
1 AS level
64+
FROM
65+
initial_job_ids AS ij
66+
UNION ALL
67+
-- find the clone_id for the current latest_job_id
68+
SELECT
69+
lir.job_id,
70+
j.clone_id AS latest_job_id,
71+
lir.scheduled_product_id AS scheduled_product_id,
72+
lir.level + 1 AS level
73+
FROM
74+
jobs AS j
75+
JOIN latest_id_resolver AS lir ON lir.latest_job_id = j.id
76+
-- limit the recursion
77+
WHERE
78+
lir.level < 50
79+
),
80+
-- filter jobs to only get the latest
81+
most_recent_jobs AS (
82+
SELECT DISTINCT ON (job_id)
83+
job_id as initial_job_id,
84+
latest_job_id,
85+
mrj.state as latest_job_state,
86+
mrj.result as latest_job_result,
87+
mrj.scheduled_product_id as scheduled_product_id,
88+
level as chain_length
89+
FROM
90+
latest_id_resolver
91+
JOIN jobs AS mrj ON mrj.id = latest_job_id
92+
WHERE
93+
latest_job_id IS NOT NULL
94+
ORDER BY
95+
job_id,
96+
level DESC
97+
)
98+
SELECT
99+
latest_job_state,
100+
latest_job_result,
101+
array_agg(latest_job_id) as job_ids,
102+
array_agg(DISTINCT scheduled_product_id) as scheduled_product_ids
103+
FROM
104+
most_recent_jobs
105+
WHERE
106+
latest_job_id IS NOT NULL
107+
GROUP BY
108+
latest_job_state,
109+
latest_job_result
110+
END_SQL
111+
);
112+
$sth->bind_param(1, $distri);
113+
$sth->bind_param(2, $version);
114+
$sth->bind_param(3, $flavor);
115+
$sth->execute;
116+
return $sth->fetchall_hashref([qw(latest_job_state latest_job_result)]);
117+
}
118+
33119
1;

lib/OpenQA/WebAPI.pm

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ sub startup ($self) {
379379
$api_ro->post('/isos')->name('apiv1_create_iso')->to('iso#create');
380380
$api_ra->delete('/isos/#name')->name('apiv1_destroy_iso')->to('iso#destroy');
381381
$api_ro->post('/isos/#name/cancel')->name('apiv1_cancel_iso')->to('iso#cancel');
382+
$api_ro->get('/isos/job_stats')->name('apiv1_scheduled_product_job_stats')->to('iso#job_statistics');
382383

383384
# api/v1/webhooks
384385
$api_ro->post('/webhooks/product')->name('apiv1_evaluate_webhook_product')->to('webhook#product');

lib/OpenQA/WebAPI/Controller/API/V1/Iso.pm

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,48 @@ sub show_scheduled_product {
5656
$self->render(json => $scheduled_product->to_hash(@args));
5757
}
5858

59+
=over 4
60+
61+
=item job_statistics()
62+
63+
Returns job statistics about the most recent scheduled products for each ARCH
64+
matching the specified DISTRI, VERSION and FLAVOR. Only scheduled products that
65+
are fully scheduled and not cancelled are considered.
66+
67+
This allows to determine whether all jobs that have been scheduled for a
68+
certain purpose are done and whether the jobs have passed. If jobs have been
69+
cloned/restarted then only the state/result of the latest job is taken into
70+
account.
71+
72+
The statistics are returned as nested JSON object with one key per present state
73+
on outer level and one key per present result on inner level:
74+
75+
{
76+
done => {
77+
failed => {job_ids => [5057], scheduled_product_ids => [330]},
78+
incomplete => {job_ids => [5056], scheduled_product_ids => [330]},
79+
}
80+
}
81+
82+
One can check for the existence of keys in the returned JSON object to check
83+
whether certain states/results are present. The concrete job IDs and scheduled
84+
product IDs for each combination are mainly returned for easier retracing but
85+
could also be used to generate a more detailed report.
86+
87+
=back
88+
89+
=cut
90+
91+
sub job_statistics ($self) {
92+
my $validation = $self->validation;
93+
my @param_keys = (qw(distri version flavor));
94+
$validation->required($_) for @param_keys;
95+
return $self->reply->validation_error({format => 'json'}) if $validation->has_error;
96+
my @params = map { $validation->param($_) } @param_keys;
97+
my $scheduled_products = $self->app->schema->resultset('ScheduledProducts');
98+
$self->render(json => $scheduled_products->job_statistics(@params));
99+
}
100+
59101
sub validate_create_parameters ($self) {
60102
my $validation = $self->validation;
61103
$validation->required($_) for (MANDATORY_PARAMETERS);

t/api/02-iso.t

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use FindBin;
1010
use lib "$FindBin::Bin/../lib", "$FindBin::Bin/../../external/os-autoinst-common/lib";
1111
use Test::Mojo;
1212
use Test::Warnings ':report_warnings';
13+
use OpenQA::Jobs::Constants;
1314
use OpenQA::JobDependencies::Constants;
1415
use OpenQA::Test::TimeLimit '300';
1516
use OpenQA::Test::Case;
@@ -309,6 +310,25 @@ is($server_64->{settings}->{PRECEDENCE}, 'overridden', "precedence override (sui
309310

310311
lj;
311312

313+
subtest 'job statistics can be queried about the scheduled product' => sub {
314+
$schema->txn_begin;
315+
# assume some of the scheduled jobs are already done
316+
$jobs->find(99985)->update({state => DONE, result => INCOMPLETE});
317+
$jobs->find(99988)->update({state => DONE, result => FAILED});
318+
$jobs->find(99993)->update({state => DONE, result => PASSED});
319+
$jobs->find(99994)->update({state => DONE, result => PASSED});
320+
$t->get_ok('/api/v1/isos/job_stats?distri=opensuse&version=13.1&flavor=DVD')->status_is(200);
321+
$schema->txn_rollback;
322+
my $json = $t->tx->res->json;
323+
is_deeply [sort keys %$json], [DONE, SCHEDULED], 'expected states present';
324+
is_deeply [sort keys %{$json->{done}}], [FAILED, INCOMPLETE, PASSED], 'expected results present';
325+
is_deeply [sort @{$json->{done}->{failed}->{job_ids}}], [99988], 'failed jobs';
326+
is_deeply [sort @{$json->{done}->{incomplete}->{job_ids}}], [99985], 'incomplete jobs';
327+
is_deeply [sort @{$json->{done}->{passed}->{job_ids}}], [99993, 99994], 'passed jobs';
328+
is_deeply [sort @{$json->{scheduled}->{none}->{job_ids}}], [99986, 99987, 99989, 99990, 99991, 99992],
329+
'scheduled jobs';
330+
};
331+
312332
subtest 'old tests are cancelled unless they are marked as important' => sub {
313333
$t->get_ok('/api/v1/jobs/99927')->status_is(200);
314334
is($t->tx->res->json->{job}->{state}, 'cancelled', 'job 99927 is cancelled');

0 commit comments

Comments
 (0)