# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
package CrashTest::Plugin::Storage::Sql::Model::CrashGroup;
use Mojo::Base -base;
use Data::Page;
use Storable 'dclone';
use CrashTest::Model::Thread;
use CrashTest::Plugin::CrashSignatureExtractor::C_Cpp;
use CrashTest::Plugin::Storage::Sql::Utils;
has [ qw/instance app config/ ];
has [ qw/dbh sql_utils/ ];
sub new {
my $self = shift->SUPER::new(@_);
$self->dbh($self->instance->dbh);
$self->sql_utils(CrashTest::Plugin::Storage::Sql::Utils->new(@_));
return $self;
}
sub db {
my ($self) = @_;
return $self->dbh->db;
}
sub _build_query_from_search_string {
my ($self, $search_str) = @_;
return $self->sql_utils->build_query_from_search_string(
$search_str,
$self->app->config->{WebInterface}->{ExtraColumns}->{Search}, {
user_id => { name => 'crash_user.user_id' },
product => { name => 'product.name' },
version => { name => 'product.version' },
channel => { name => 'product.release_channel' },
function => { name => 'crash_group.crash_thread_signature_bt', type => "fuzzy" },
bug => { name => 'crash_reports.crash_group_id IN (SELECT crash_group_id FROM bug_links WHERE bug_key ?op? ?)', type => 'sql' },
});
}
sub index {
my ($self, $pagen, $nperpage, $search_str) = @_;
my $db = $self->db;
my $where = "";
my @values = ();
my $err;
if(defined($search_str) && $search_str ne "") {
eval {
my $q = $self->_build_query_from_search_string($search_str);
$where = "WHERE " . $q->[0];
@values = @{$q->[1]};
};
$err = $@;
}
my $count = $db->query("
SELECT count(distinct(crash_group_id)) AS total
FROM crash_reports
LEFT JOIN crash_users AS crash_user ON crash_reports.crash_user_id = crash_user.id
LEFT JOIN products AS product ON crash_reports.product_id = product.id
JOIN crash_groups AS crash_group ON crash_reports.crash_group_id = crash_group.id
$where
", @values)->hash;
my $pager = Data::Page->new();
$pager->total_entries($count->{total});
$pager->entries_per_page($nperpage);
$pager->current_page($pagen);
my @extra_cols;
foreach my $extra_col(@{$self->app->config->{WebInterface}->{ExtraColumns}->{GroupIndex}}) {
push @extra_cols, $extra_col->{db_column} . " AS " . $extra_col->{id};
}
my $extra_columns = join(",", @extra_cols);
my $results = $db->query("
SELECT crash_groups.uuid, title, group_by_count.*,
(SELECT json_agg(to_json(bug_links)) FROM bug_links WHERE bug_links.crash_group_id = crash_groups.id) AS bug_links
FROM crash_groups,
( SELECT crash_group.id AS id,
min(version) AS first_version,
max(version) AS last_version,
string_agg(distinct(name), ', ') AS product_names,
$extra_columns,
count(*) AS crash_count
FROM crash_reports
LEFT JOIN crash_users AS crash_user ON crash_reports.crash_user_id = crash_user.id
LEFT JOIN products AS product ON crash_reports.product_id = product.id
JOIN crash_groups AS crash_group ON crash_reports.crash_group_id = crash_group.id
$where
GROUP BY crash_group.id ) AS group_by_count
WHERE group_by_count.id = crash_groups.id
ORDER BY crash_count DESC, crash_groups.id DESC
OFFSET (?) ROWS
FETCH NEXT (?) ROWS ONLY
",
@values, $pager->skipped, $pager->entries_per_page
)->expand->hashes;
return ($results, $pager, $err);
}
sub get {
my ($self, $uuid) = @_;
my $db = $self->db;
my $result = $db->query("
SELECT * FROM crash_groups WHERE id = (SELECT crash_group_id FROM crash_reports WHERE uuid = ?)
",
$uuid
)->hash;
return $result;
}
sub stats_by_product_and_version {
my ($self, $group_uuid) = @_;
my $db = $self->db;
my $results = $db->query("
SELECT
name AS product_name,
version,
count(*) AS crash_count
FROM crash_reports
JOIN products ON product_id = products.id
WHERE crash_group_id = (
SELECT crash_group_id FROM crash_reports WHERE uuid = ?
)
GROUP BY product_name, version
ORDER BY crash_count DESC, product_name, version;
",
$group_uuid
)->hashes;
return $results;
}
has signature_extractor => sub {
my $self = shift;
state $sig_extract = CrashTest::Plugin::CrashSignatureExtractor::C_Cpp->new(
app => $self->app,
config => $self->app->config->{Processor}->{CrashSignatureExtractor}->{C_Cpp},
);
};
sub _find_by_sig {
my ($self, $db, $sig) = @_;
return $db->query(
"SELECT id, crash_thread_signature_bt <-> \$1 AS dist
FROM crash_groups
WHERE crash_thread_signature_bt % \$1
ORDER BY dist ASC
LIMIT 1",
$sig
)->hash;
}
sub _create_pg95 {
my ($self, $db, $uuid, $sig, $title) = @_;
my $crash_group = $db->query(
"INSERT INTO crash_groups (uuid, crash_thread_signature_bt, title)
VALUES (?, ?, ?)
ON CONFLICT DO NOTHING
RETURNING id",
$uuid, $sig, $title
)->hash;
if(!defined($crash_group)) {
$crash_group = $self->_find_by_sig($db, $sig);
warn "crash group is null after insert, reselected " . $crash_group->{id} . "\n";
}
return $crash_group;
}
sub _create_pg94 {
my ($self, $db, $uuid, $sig, $title) = @_;
# avoid race condition, no other safe way without upsert
$db->query("LOCK TABLE crash_groups IN EXCLUSIVE MODE")->finish;
# this query is NOT concurrency safe, there is a possible write race. That's why we lock the table.
my $crash_group = $db->query(
"INSERT INTO crash_groups (uuid, crash_thread_signature_bt, title)
SELECT \$1, \$2, \$3
WHERE NOT EXISTS (SELECT 1 FROM crash_groups WHERE crash_thread_signature_bt % \$2)
RETURNING id",
$uuid, $sig, $title
)->hash;
# may happen if there is an insert between the select (in _find_by_sig) and the lock
if(!defined($crash_group)) {
$crash_group = $self->_find_by_sig($db, $sig);
warn "crash group is null after insert, reselected " . $crash_group->{id} . "\n";
}
return $crash_group;
}
sub _set_similarity_limit {
my ($self, $db) = @_;
my $max_dist = $self->app->config->{Processor}->{CrashSignatureExtractor}->{C_Cpp}->{GroupMaxDistance} || 0.1;
$db->query("SELECT set_limit(?)", 1.0 - $max_dist)->finish;
}
sub find_or_create {
my ($self, $uuid, $pjson, $db) = @_;
#$db //= $self->db;
my $raw_thread = dclone $pjson->{threads}->[$pjson->{crashing_thread}->{threads_index}];
my $thread = CrashTest::Model::Thread->new($raw_thread);
my $sig = join "\n", @{$self->signature_extractor->extract_signature($thread)};
$self->_set_similarity_limit($db);
my $crash_group = $self->_find_by_sig($db, $sig);
if(!$crash_group) {
if($sig ne "") {
my $title = $self->signature_extractor->extract_signature_title($thread);
if($db->dbh->{pg_server_version} >= 90500) {
#warn "PostgreSQL 9.5, nice !";
$crash_group = $self->_create_pg95($db, $uuid, $sig, $title);
} else {
$crash_group = $self->_create_pg94($db, $uuid, $sig, $title);
}
}
}
return $crash_group;
}
sub delete_unused {
my ($self, $db) = @_;
$db->query("DELETE FROM crash_groups WHERE NOT EXISTS (SELECT 1 FROM crash_reports WHERE crash_group_id = crash_groups.id)")->finish;
}
1;