Skip to content

Commit 131df4d

Browse files
authored
Merge pull request #6850 from Martchus/overview-limit
Fix possibly excessive memory use when computer test result overview
2 parents e3b9e48 + c018f21 commit 131df4d

File tree

10 files changed

+152
-124
lines changed

10 files changed

+152
-124
lines changed

lib/OpenQA/Schema/Result/Jobs.pm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ sub settings_hash ($self, $prefetched = undef) {
418418
$settings->{$_->key} = $_->value;
419419
}
420420
}
421-
for my $column (qw(DISTRI VERSION FLAVOR MACHINE ARCH BUILD TEST)) {
421+
for my $column (MAIN_SETTINGS) {
422422
if (my $value = $self->$column) { $settings->{$column} = $value }
423423
}
424424
$settings->{NAME} = sprintf '%08d-%s', $self->id, $self->name;

lib/OpenQA/Schema/ResultSet/Jobs.pm

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ use Mojolicious::Validator;
2323
use Mojolicious::Validator::Validation;
2424
use Time::HiRes 'time';
2525
use DateTime;
26+
use List::Util qw(any);
2627
use Scalar::Util qw(looks_like_number);
2728

2829
=head2 latest_build
@@ -55,20 +56,16 @@ sub latest_build ($self, %args) {
5556
$attrs{order_by} = {-desc => 'me.id'}; # More reliable for tests than t_created
5657
$attrs{columns} = qw(BUILD);
5758

59+
my $job_settings = $schema->resultset('JobSettings');
5860
foreach my $key (keys %args) {
61+
my $key_uc = uc $key;
5962
my $value = $args{$key};
60-
61-
if (grep { $key eq $_ } qw(distri version flavor machine arch build test)) {
62-
push(@conds, {'me.' . uc($key) => $value});
63+
if (any { $key_uc eq $_ } OpenQA::Schema::Result::Jobs::MAIN_SETTINGS) {
64+
push @conds, {'me.' . $key_uc => $value};
6365
}
6466
else {
65-
66-
my $subquery = $schema->resultset('JobSettings')->search(
67-
{
68-
key => uc($key),
69-
value => $value
70-
});
71-
push(@conds, {'me.id' => {-in => $subquery->get_column('job_id')->as_query}});
67+
my $subquery = $job_settings->search({key => $key_uc, value => $value});
68+
push @conds, {'me.id' => {-in => $subquery->get_column('job_id')->as_query}};
7269
}
7370
}
7471

@@ -97,16 +94,8 @@ sub latest_jobs ($self, $until = undef) {
9794
my @latest;
9895
my %seen;
9996
foreach my $job (@jobs) {
100-
my $test = $job->TEST;
101-
my $distri = $job->DISTRI;
102-
my $version = $job->VERSION;
103-
my $build = $job->BUILD;
104-
my $flavor = $job->FLAVOR;
105-
my $arch = $job->ARCH;
106-
my $machine = $job->MACHINE // '';
107-
my $key = "$distri-$version-$build-$test-$flavor-$arch-$machine";
108-
next if $seen{$key}++;
109-
push(@latest, $job);
97+
my $key = join('-', map { $job->$_ // '' } OpenQA::Schema::Result::Jobs::MAIN_SETTINGS);
98+
push @latest, $job unless $seen{$key}++;
11099
}
111100

112101
return @latest;
@@ -321,7 +310,7 @@ sub _prepare_complex_query_search_args ($self, $args) {
321310
else {
322311
my %js_settings;
323312
# Check if the settings are between the arguments passed via query url
324-
# they come in lowercase, so mace sure $key is lc'ed
313+
# they come in lowercase, so make sure $key is lc'ed
325314
for my $key (qw(ISO HDD_1 WORKER_CLASS)) {
326315
$js_settings{$key} = $args->{lc $key} if defined $args->{lc $key};
327316
}
@@ -353,16 +342,53 @@ sub _prepare_complex_query_search_args ($self, $args) {
353342
return (\@conds, \%attrs);
354343
}
355344

356-
sub complex_query ($self, %args) {
357-
# For args where we accept a list of values, allow passing either an
358-
# array ref or a comma-separated list
345+
sub _accept_comma_separated_arg_values ($args) {
359346
for my $arg (qw(state ids result modules modules_result)) {
360-
next unless $args{$arg};
361-
$args{$arg} = [split(',', $args{$arg})] unless (ref($args{$arg}) eq 'ARRAY');
347+
next unless my $value = $args->{$arg};
348+
$args->{$arg} = [split ',', $value] unless ref $value eq 'ARRAY';
362349
}
350+
}
351+
352+
sub complex_query ($self, %args) {
353+
_accept_comma_separated_arg_values(\%args);
363354
my ($conds, $attrs) = $self->_prepare_complex_query_search_args(\%args);
364-
my $jobs = $self->search({-and => $conds}, $attrs);
365-
return $jobs;
355+
return $self->search({-and => $conds}, $attrs);
356+
}
357+
358+
sub complex_query_latest_ids ($self, %args) {
359+
# prepare basic search conditions and attributes
360+
_accept_comma_separated_arg_values(\%args);
361+
my ($conds, $attrs) = $self->_prepare_complex_query_search_args(\%args);
362+
my $filters = $args{filters};
363+
my $has_filters = $filters && @$filters > 0;
364+
my $rows = $has_filters ? delete $attrs->{rows} : undef; # when filtering, limit rows only in outer/filtering query
365+
if (my $until = $args{until}) { push @$conds, {'me.t_created' => {'<=' => $until}} }
366+
367+
# set attributes to return only the latest job IDs for a certain combination of TEST, DISTRI, VERSION, …
368+
$attrs->{order_by} = \['max(me.id) DESC'];
369+
$attrs->{select} = ['max(me.id)'];
370+
$attrs->{as} = ['id'];
371+
$attrs->{group_by} = [OpenQA::Schema::Result::Jobs::MAIN_SETTINGS];
372+
373+
# execute the search; use a sub query if filtering is enabled
374+
my $search = $self->search({-and => $conds}, $attrs);
375+
if ($has_filters) {
376+
# add another layer of querying for filters
377+
# note: The filtering cannot be applied in the same query we do the grouping to return only the latest job IDs.
378+
# Otherwise adding filter parameters would lead to old jobs showing up. That is not wanted (and we have
379+
# therefore the test "filtering does not reveal old jobs" in `10-tests_overview.t` to test this).
380+
my %filter_attrs = %$attrs;
381+
$filter_attrs{rows} = $rows;
382+
push @$filters, {'me.id' => {-in => $search->as_query}};
383+
$search = $self->search({-and => $filters}, \%filter_attrs);
384+
}
385+
return [map { $_->id } $search->all];
386+
}
387+
388+
sub latest_jobs_from_ids ($self, $latest_job_ids, $limit_from_initial_search) {
389+
my %search_args = (id => {-in => $latest_job_ids});
390+
my %options = (order_by => {-desc => 'id'}, rows => $limit_from_initial_search - 1);
391+
return $self->search(\%search_args, \%options);
366392
}
367393

368394
sub cancel_by_settings (
@@ -380,7 +406,7 @@ sub cancel_by_settings (
380406
my %precond = %{$settings};
381407
my %cond;
382408

383-
for my $key (qw(DISTRI VERSION FLAVOR MACHINE ARCH BUILD TEST)) {
409+
for my $key (OpenQA::Schema::Result::Jobs::MAIN_SETTINGS) {
384410
$cond{$key} = delete $precond{$key} if defined $precond{$key};
385411
}
386412
if (keys %precond) {

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

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -226,29 +226,15 @@ So this works in the same way as the test results overview in the GUI.
226226
sub overview ($self) {
227227
my ($search_args, $groups) = $self->compose_job_overview_search_args;
228228
my $failed_modules = $self->param_hash('failed_modules');
229-
my $states = $self->param_hash('state');
230-
my $results = $self->param_hash('result');
231-
my $archs = $self->param_hash('arch');
232-
my $machines = $self->param_hash('machine');
233-
234-
my @jobs = $self->schema->resultset('Jobs')->complex_query(%$search_args)->latest_jobs;
229+
my $jobs_rs = $self->schema->resultset('Jobs');
230+
my $latest_job_ids = $jobs_rs->complex_query_latest_ids(%$search_args);
231+
my @jobs = $jobs_rs->latest_jobs_from_ids($latest_job_ids, $search_args->{limit})->all;
235232

236233
my @job_hashes;
237234
for my $job (@jobs) {
238-
next if $states && !$states->{$job->state};
239-
next if $results && !$results->{$job->result};
240-
next if $archs && !$archs->{$job->ARCH};
241-
next if $machines && !$machines->{$job->MACHINE};
242-
if ($failed_modules) {
243-
next if $job->result ne OpenQA::Jobs::Constants::FAILED;
244-
next unless OpenQA::Utils::any_array_item_contained_by_hash($job->failed_modules, $failed_modules);
245-
}
246-
push(
247-
@job_hashes,
248-
{
249-
id => $job->id,
250-
name => $job->name,
251-
});
235+
next
236+
if $failed_modules && !OpenQA::Utils::any_array_item_contained_by_hash($job->failed_modules, $failed_modules);
237+
push @job_hashes, {id => $job->id, name => $job->name};
252238
}
253239
$self->render(json => \@job_hashes);
254240
}

lib/OpenQA/WebAPI/Controller/Test.pm

Lines changed: 40 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -685,10 +685,9 @@ sub _calculate_preferred_machines ($jobs) {
685685
}
686686

687687
# Take an job objects arrayref and prepare data structures for 'overview'
688-
sub _prepare_job_results ($self, $all_jobs, $limit) {
688+
sub _prepare_job_results ($self, $jobs, $job_ids) {
689689
my %archs;
690690
my %results;
691-
my @job_ids;
692691
my $aggregated = {
693692
none => 0,
694693
passed => 0,
@@ -699,25 +698,8 @@ sub _prepare_job_results ($self, $all_jobs, $limit) {
699698
running => 0,
700699
unknown => 0
701700
};
702-
my $preferred_machines = _calculate_preferred_machines($all_jobs);
703-
704-
# read parameter for additional filtering
705-
my $failed_modules = $self->param_hash('failed_modules');
706-
my $states = $self->param_hash('state');
707-
my $results = $self->param_hash('result');
708-
my $archs = $self->param_hash('arch');
709-
my $machines = $self->param_hash('machine');
710-
711-
my @jobs = grep {
712-
(not $states or $states->{$_->state})
713-
and (not $results or $results->{$_->result})
714-
and (not $archs or $archs->{$_->ARCH})
715-
and (not $machines or $machines->{$_->MACHINE})
716-
and (not $failed_modules or $_->result eq OpenQA::Jobs::Constants::FAILED)
717-
} @$all_jobs;
718-
my $limit_exceeded = @jobs >= $limit;
719-
@jobs = @jobs[0 .. ($limit - 1)] if $limit_exceeded;
720-
my @jobids = map { $_->id } @jobs;
701+
my @jobs = $jobs->all;
702+
my $preferred_machines = _calculate_preferred_machines(\@jobs);
721703

722704
# prefetch the number of available labels for those jobs
723705
my $schema = $self->schema;
@@ -726,7 +708,7 @@ sub _prepare_job_results ($self, $all_jobs, $limit) {
726708
# prefetch test suite names from job settings
727709
my $job_settings
728710
= $schema->resultset('JobSettings')
729-
->search({job_id => {-in => [map { $_->id } @jobs]}, key => {-in => [qw(JOB_DESCRIPTION TEST_SUITE_NAME)]}});
711+
->search({job_id => {-in => $job_ids}, key => {-in => [qw(JOB_DESCRIPTION TEST_SUITE_NAME)]}});
730712
my %settings_by_job_id;
731713
for my $js ($job_settings->all) {
732714
$settings_by_job_id{$js->job_id}->{$js->key} = $js->value;
@@ -740,12 +722,13 @@ sub _prepare_job_results ($self, $all_jobs, $limit) {
740722
= $schema->resultset('TestSuites')->search({name => \%desc_args}, {columns => [qw(name description)]});
741723
my %descriptions = map { $_->name => $_->description } @descriptions;
742724

743-
my $failed_modules_by_job = $self->_fetch_failed_modules_by_jobs(\@jobids);
744-
my ($children_by_job, $parents_by_job) = $self->_fetch_dependencies_by_jobs(\@jobids);
725+
my $failed_modules_by_job = $self->_fetch_failed_modules_by_jobs($job_ids);
726+
my ($children_by_job, $parents_by_job) = $self->_fetch_dependencies_by_jobs($job_ids);
745727
foreach my $job (@jobs) {
746728
my $id = $job->id;
747729
my $result = $job->overview_result(
748-
$comment_data, $aggregated, $failed_modules,
730+
$comment_data, $aggregated,
731+
$self->param_hash('failed_modules'),
749732
$failed_modules_by_job->{$id} || [],
750733
$self->param('todo')) or next;
751734
my $test = $job->TEST;
@@ -780,14 +763,11 @@ sub _prepare_job_results ($self, $all_jobs, $limit) {
780763
$results{$distri}{$version}{$flavor}{$test} //= {};
781764
$results{$distri}{$version}{$flavor}{$test}{$arch} = $result;
782765

783-
# populate job IDs for adding multiple comments
784-
push @job_ids, $id;
785-
786766
# add description
787767
my $description = $settings_by_job_id{$id}->{JOB_DESCRIPTION} // $descriptions{$test_suite_names{$id}};
788768
$results{$distri}{$version}{$flavor}{$test}{description} //= $description;
789769
}
790-
return ($limit_exceeded, \%archs, \%results, \@job_ids, $aggregated);
770+
return (\%archs, \%results, $aggregated);
791771
}
792772

793773
sub _prepare_groupids ($self) {
@@ -857,32 +837,26 @@ sub overview ($self) {
857837
my $validation = $self->validation;
858838
$validation->optional('t')->datetime;
859839
my $until = $validation->param('t');
860-
my %stash = (
861-
# build, version, distri are not mandatory and therefore not
862-
# necessarily come from the search args so they can be undefined.
863-
build => ref $search_args->{build} eq 'ARRAY' ? join(',', @{$search_args->{build}}) : $search_args->{build},
864-
version => $search_args->{version},
865-
distri => $search_args->{distri},
866-
groups => $groups,
867-
until => $until,
868-
parallel_children_collapsable_results_sel => $config->{global}->{parallel_children_collapsable_results_sel},
869-
);
870-
my @jobs = $self->schema->resultset('Jobs')->complex_query(%$search_args)->latest_jobs($until);
871-
872-
my $limit = $config->{misc_limits}->{tests_overview_max_jobs};
873-
(my $limit_exceeded, $stash{archs}, $stash{results}, $stash{job_ids}, $stash{aggregated})
874-
= $self->_prepare_job_results(\@jobs, $limit);
840+
$search_args->{until} = $until;
841+
my $distri = $search_args->{distri};
842+
my $version = $search_args->{version};
843+
my $jobs_rs = $self->schema->resultset('Jobs');
844+
my $latest_job_ids = $jobs_rs->complex_query_latest_ids(%$search_args);
845+
my $limit = $search_args->{limit}; # one more than actual limit so we can check whether the limit was exceeded
846+
my $exceeded_limit = @$latest_job_ids >= $limit ? $limit - 1 : 0;
847+
my $jobs = $jobs_rs->latest_jobs_from_ids($latest_job_ids, $limit);
848+
my ($archs, $results, $aggregated) = $self->_prepare_job_results($jobs, $latest_job_ids);
875849

876850
# determine distri/version from job results if not explicitly specified via search args
877-
my @distris = keys %{$stash{results}};
851+
my @distris = keys %$results;
878852
my $formatted_distri;
879853
my $formatted_version;
880854
my $only_distri = scalar @distris == 1;
881-
if (!defined $stash{distri} && $only_distri) {
855+
if (!defined $distri && $only_distri) {
882856
$formatted_distri = $distris[0];
883-
if (!defined $stash{version}) {
884-
my @versions = keys %{$stash{results}->{$formatted_distri}};
885-
$formatted_version = $versions[0] if (scalar @versions == 1);
857+
if (!defined $version) {
858+
my @versions = keys %{$results->{$formatted_distri}};
859+
$formatted_version = $versions[0] if scalar @versions == 1;
886860
}
887861
}
888862

@@ -899,18 +873,28 @@ sub overview ($self) {
899873
_add_distri_and_version_to_summary(\@summary_parts, $formatted_distri, $formatted_version, 1);
900874

901875
# add distri and version from query parameters as regular strings
902-
_add_distri_and_version_to_summary(\@summary_parts, $stash{distri}, $stash{version}, 0);
876+
_add_distri_and_version_to_summary(\@summary_parts, $distri, $version, 0);
903877
}
904878

905-
$self->stash(
906-
%stash,
879+
my %stash = (
880+
# build, version, distri are not mandatory and therefore not
881+
# necessarily come from the search args so they can be undefined.
882+
build => ref $search_args->{build} eq 'ARRAY' ? join(',', @{$search_args->{build}}) : $search_args->{build},
883+
distri => $distri,
884+
version => $version,
885+
groups => $groups,
886+
archs => $archs,
887+
results => $results,
888+
aggregated => $aggregated,
889+
job_ids => $latest_job_ids,
890+
until => $until,
891+
parallel_children_collapsable_results_sel => $config->{global}->{parallel_children_collapsable_results_sel},
907892
summary_parts => \@summary_parts,
908893
only_distri => $only_distri,
909-
limit_exceeded => $limit_exceeded ? $limit : undef
894+
limit_exceeded => $exceeded_limit,
910895
);
911-
$self->respond_to(
912-
json => {json => \%stash},
913-
html => {template => 'test/overview'});
896+
$self->stash(\%stash);
897+
$self->respond_to(json => {json => \%stash}, html => {template => 'test/overview'});
914898
}
915899

916900
sub _get_latest_job ($self) {

0 commit comments

Comments
 (0)