-
Notifications
You must be signed in to change notification settings - Fork 23
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
New result entries table #1092
New result entries table #1092
Changes from 20 commits
70549d4
6b697dd
576c23b
36328f7
f8256ff
cc5de77
65fc4f1
b4f0f20
397f8aa
7f44e4a
f5691bc
a6c38b0
0a3e32b
0da9965
73d23be
8a58683
65e95ca
878883f
38187ee
d2a4d1c
2022a7b
33e32e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ use Readonly; | |
use Try::Tiny; | ||
|
||
use Zonemaster::Backend::Errors; | ||
use Zonemaster::Engine::Logger::Entry; | ||
|
||
requires qw( | ||
add_batch_job | ||
|
@@ -371,6 +372,34 @@ sub test_state { | |
} | ||
} | ||
|
||
sub set_test_completed { | ||
my ( $self, $test_id ) = @_; | ||
|
||
my $current_state = $self->test_state( $test_id ); | ||
|
||
if ( $current_state ne $TEST_RUNNING ) { | ||
die Zonemaster::Backend::Error::Internal->new( reason => 'illegal transition to COMPLETED' ); | ||
} | ||
|
||
my $rows_affected = $self->dbh->do( | ||
q[ | ||
UPDATE test_results | ||
SET progress = 100, | ||
ended_at = ? | ||
WHERE hash_id = ? | ||
AND 0 < progress | ||
AND progress < 100 | ||
], | ||
undef, | ||
$self->format_time( time() ), | ||
$test_id, | ||
); | ||
|
||
if ( $rows_affected == 0 ) { | ||
die Zonemaster::Backend::Error::Internal->new( reason => "job not found or illegal transition" ); | ||
} | ||
} | ||
|
||
sub select_test_results { | ||
my ( $self, $test_id ) = @_; | ||
|
||
|
@@ -379,8 +408,7 @@ sub select_test_results { | |
SELECT | ||
hash_id, | ||
created_at, | ||
params, | ||
results | ||
params | ||
FROM test_results | ||
WHERE hash_id = ? | ||
], | ||
|
@@ -431,14 +459,35 @@ sub test_results { | |
|
||
my $result = $self->select_test_results( $test_id ); | ||
|
||
my @result_entries = $self->dbh->selectall_array( | ||
q[ | ||
SELECT | ||
l.level, | ||
r.module, | ||
r.testcase, | ||
r.tag, | ||
r.timestamp, | ||
r.args | ||
FROM result_entries r | ||
INNER JOIN log_level l | ||
ON r.level = l.value | ||
WHERE hash_id = ? | ||
], | ||
{ Slice => {} }, | ||
$test_id | ||
); | ||
|
||
eval { | ||
$result->{params} = decode_json( $result->{params} ); | ||
|
||
if (defined $result->{results}) { | ||
$result->{results} = decode_json( $result->{results} ); | ||
} else { | ||
$result->{results} = []; | ||
} | ||
@result_entries = map { | ||
{ | ||
%$_, | ||
args => decode_json( $_->{args} ), | ||
} | ||
} @result_entries; | ||
|
||
$result->{results} = \@result_entries; | ||
}; | ||
|
||
die Zonemaster::Backend::Error::JsonError->new( reason => "$@", data => { test_id => $test_id } ) | ||
|
@@ -462,11 +511,13 @@ sub get_test_history { | |
my @results; | ||
my $query = q[ | ||
SELECT | ||
(SELECT count(*) FROM result_entries WHERE result_entries.hash_id = test_results.hash_id AND level = ?) AS nb_critical, | ||
(SELECT count(*) FROM result_entries WHERE result_entries.hash_id = test_results.hash_id AND level = ?) AS nb_error, | ||
(SELECT count(*) FROM result_entries WHERE result_entries.hash_id = test_results.hash_id AND level = ?) AS nb_warning, | ||
id, | ||
hash_id, | ||
created_at, | ||
undelegated, | ||
results | ||
undelegated | ||
FROM test_results | ||
WHERE progress = 100 AND domain = ? AND ( ? IS NULL OR undelegated = ? ) | ||
ORDER BY id DESC | ||
|
@@ -475,33 +526,37 @@ sub get_test_history { | |
|
||
my $sth = $dbh->prepare( $query ); | ||
|
||
$sth->bind_param( 1, _normalize_domain( $p->{frontend_params}{domain} ) ); | ||
$sth->bind_param( 2, $undelegated, SQL_INTEGER ); | ||
$sth->bind_param( 3, $undelegated, SQL_INTEGER ); | ||
$sth->bind_param( 4, $p->{limit} ); | ||
$sth->bind_param( 5, $p->{offset} ); | ||
my %levels = Zonemaster::Engine::Logger::Entry->levels(); | ||
$sth->bind_param( 1, $levels{CRITICAL} ); | ||
$sth->bind_param( 2, $levels{ERROR} ); | ||
$sth->bind_param( 3, $levels{WARNING} ); | ||
$sth->bind_param( 4, _normalize_domain( $p->{frontend_params}{domain} ) ); | ||
$sth->bind_param( 5, $undelegated, SQL_INTEGER ); | ||
$sth->bind_param( 6, $undelegated, SQL_INTEGER ); | ||
$sth->bind_param( 7, $p->{limit} ); | ||
$sth->bind_param( 8, $p->{offset} ); | ||
|
||
$sth->execute(); | ||
|
||
while ( my $h = $sth->fetchrow_hashref ) { | ||
$h->{results} = decode_json($h->{results}) if $h->{results}; | ||
my $critical = ( grep { $_->{level} eq 'CRITICAL' } @{ $h->{results} } ); | ||
my $error = ( grep { $_->{level} eq 'ERROR' } @{ $h->{results} } ); | ||
my $warning = ( grep { $_->{level} eq 'WARNING' } @{ $h->{results} } ); | ||
|
||
# More important overwrites | ||
my $overall = 'ok'; | ||
$overall = 'warning' if $warning; | ||
$overall = 'error' if $error; | ||
$overall = 'critical' if $critical; | ||
my $overall_result = 'ok'; | ||
if ( $h->{nb_critical} ) { | ||
$overall_result = 'critical'; | ||
} | ||
elsif ( $h->{nb_error} ) { | ||
$overall_result = 'error'; | ||
} | ||
elsif ( $h->{nb_warning} ) { | ||
$overall_result = 'warning'; | ||
} | ||
|
||
push( | ||
@results, | ||
{ | ||
id => $h->{hash_id}, | ||
created_at => $self->to_iso8601( $h->{created_at} ), | ||
undelegated => $h->{undelegated}, | ||
overall_result => $overall, | ||
overall_result => $overall_result, | ||
} | ||
); | ||
} | ||
|
@@ -725,15 +780,18 @@ sub process_unfinished_tests { | |
$test_run_timeout, | ||
); | ||
|
||
my $msg = { | ||
"level" => "CRITICAL", | ||
"module" => "BACKEND_TEST_AGENT", | ||
"tag" => "UNABLE_TO_FINISH_TEST", | ||
"args" => { max_execution_time => $test_run_timeout }, | ||
"timestamp" => $test_run_timeout | ||
}; | ||
my $msg = Zonemaster::Engine::Logger::Entry->new( | ||
{ | ||
level => "CRITICAL", | ||
module => "BACKEND_TEST_AGENT", | ||
testcase => "", | ||
tag => "UNABLE_TO_FINISH_TEST", | ||
args => { max_execution_time => $test_run_timeout }, | ||
timestamp => $test_run_timeout | ||
} | ||
); | ||
while ( my $h = $sth1->fetchrow_hashref ) { | ||
$self->force_end_test($h->{hash_id}, $h->{results}, $msg); | ||
$self->force_end_test($h->{hash_id}, $msg); | ||
} | ||
} | ||
|
||
|
@@ -775,44 +833,41 @@ sub select_unfinished_tests { | |
} | ||
} | ||
|
||
=head2 force_end_test($hash_id, $results, $msg) | ||
=head2 force_end_test($hash_id, $msg) | ||
|
||
Append the $msg log entry to the $results arrayref and store the results into | ||
the database. | ||
Store the L<Zonemaster::Engine::Logger::Entry> $msg log entry into the database | ||
and mark test with $hash_id as COMPLETED. | ||
|
||
=cut | ||
|
||
sub force_end_test { | ||
my ( $self, $hash_id, $results, $msg ) = @_; | ||
my $result; | ||
if ( defined $results && $results =~ /^\[/ ) { | ||
$result = decode_json( $results ); | ||
} | ||
else { | ||
$result = []; | ||
} | ||
push @$result, $msg; | ||
my ( $self, $hash_id, $msg ) = @_; | ||
|
||
$self->store_results( $hash_id, encode_json($result) ); | ||
$self->add_result_entries( $hash_id, $msg ); | ||
$self->set_test_completed( $hash_id ); | ||
} | ||
|
||
=head2 process_dead_test($hash_id) | ||
|
||
Append a new log entry C<BACKEND_TEST_AGENT:TEST_DIED> to the test with $hash_id. | ||
Then store the results in database. | ||
Store a new log entry C<BACKEND_TEST_AGENT:TEST_DIED> in database for the test | ||
with $hash_id. | ||
|
||
=cut | ||
|
||
sub process_dead_test { | ||
my ( $self, $hash_id ) = @_; | ||
my ( $results ) = $self->dbh->selectrow_array("SELECT results FROM test_results WHERE hash_id = ?", undef, $hash_id); | ||
my $msg = { | ||
"level" => "CRITICAL", | ||
"module" => "BACKEND_TEST_AGENT", | ||
"tag" => "TEST_DIED", | ||
"timestamp" => $self->get_relative_start_time($hash_id) | ||
}; | ||
$self->force_end_test($hash_id, $results, $msg); | ||
my $msg = Zonemaster::Engine::Logger::Entry->new( | ||
{ | ||
level => "CRITICAL", | ||
module => "BACKEND_TEST_AGENT", | ||
testcase => "", | ||
tag => "TEST_DIED", | ||
args => {}, | ||
timestamp => $self->get_relative_start_time($hash_id) | ||
} | ||
); | ||
#$msg = bless( $msg, "Zonemaster::Engine::Logger::Entry" ); | ||
hannaeko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$self->force_end_test($hash_id, $msg); | ||
} | ||
|
||
# Converts the domain to lowercase and if the domain is not the root ('.') | ||
|
@@ -945,6 +1000,33 @@ sub to_iso8601 { | |
return $time; | ||
} | ||
|
||
sub add_result_entries { | ||
my ( $self, $hash_id, @entries ) = @_; | ||
my @records; | ||
|
||
my $json = JSON::PP->new->allow_blessed->convert_blessed->canonical; | ||
|
||
my %levels = Zonemaster::Engine::Logger::Entry->levels(); | ||
|
||
foreach my $e ( @entries ) { | ||
my $r = [ | ||
$hash_id, | ||
$levels{ $e->level }, | ||
$e->module, | ||
$e->testcase, | ||
$e->tag, | ||
$e->timestamp, | ||
$json->encode( $e->args // {} ), | ||
]; | ||
|
||
push @records, $r; | ||
} | ||
my $query_values = join ", ", ("(?, ?, ?, ?, ?, ?, ?)") x @records; | ||
my $query = "INSERT INTO result_entries (hash_id, level, module, testcase, tag, timestamp, args) VALUES $query_values"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be super-nice if we could verify that the job is "running" when performing the insert and throw an error otherwise. I'm open to leaving this as a future exercise though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea. Just a question on how to do this. I was thinking to add something like: if ( $current_state ne $TEST_RUNNING ) {
die Zonemaster::Backend::Error::Internal->new( reason => 'cannot store results to a non-running test' );
} but I think it won't be sufficient. What if the test's state is updated by another process between the check and the insert? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, exactly. I made a proof of concept for how to make it atomic. But I'm not sure about its scalability. Maybe it's great, I don't know. And I also haven't tried it in the other DB engines. #!/usr/bin/env perl
use strict;
use warnings;
use 5.014;
use utf8;
use DBI;
my $dbh = DBI->connect( "dbi:SQLite:dbname=:memory:", "", "" );
sub setup {
$dbh->do( '
CREATE TABLE test_results (
id INTEGER,
progress INTEGER
)
' );
$dbh->do( '
CREATE TABLE result_entries (
test_id INTEGER,
text VARCHAR(20)
)
' );
$dbh->do( 'INSERT INTO test_results VALUES (1234, 0)' );
$dbh->do( 'INSERT INTO test_results VALUES (2345, 50)' );
$dbh->do( 'INSERT INTO test_results VALUES (3456, 100)' );
}
sub insert {
my ( $id, @messages ) = @_;
my $prefix = 'INSERT INTO result_entries ';
my $separator = ' UNION ';
my $select = 'SELECT id, ? FROM test_results WHERE id = ? AND progress > 0 AND progress < 100';
my $query = $prefix . join $separator, ( $select ) x scalar @messages;
my $sth = $dbh->prepare( $query );
my @params;
for my $message ( @messages ) {
push @params, $message, $id;
}
my $rv = $sth->execute( @params );
if ( $rv && $rv == 0 ) {
die "illegal state transition";
}
}
sub show {
my ( $table ) = @_;
my $rows = $dbh->selectall_arrayref( "SELECT * FROM $table" );
say "Dump of $table:";
for my $row ( @$rows ) {
say join ', ', @{$row};
}
}
my $id = shift @ARGV;
my @messages = @ARGV;
setup();
show( 'test_results' );
say 'Inserting...';
insert( $id, @messages );
show( 'result_entries' ); $ ./poc 1234 aaa bbb ccc
Dump of test_results:
1234, 0
2345, 50
3456, 100
Inserting...
illegal state transition at ./poc line 45. $ ./poc 2345 aaa bbb ccc
Dump of test_results:
1234, 0
2345, 50
3456, 100
Inserting...
Dump of result_entries:
2345, aaa
2345, bbb
2345, ccc There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the proof of concept. I think I'll go with your suggestion to "leav[e] this as a future exercise". I've open an issue for that, see #1135. |
||
my $sth = $self->dbh->prepare($query); | ||
$sth = $sth->execute(map { @$_ } @records); | ||
} | ||
|
||
no Moose::Role; | ||
|
||
1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if performance is an issue for test_results, but if it is I have a hunch we're spending a lot of cycles on this decoding and re-encoding of all the JSON-blobs.