1 package Gruntmaster
::Data
;
5 use parent qw
/Exporter/;
6 our $VERSION = '6000.001';
7 our @EXPORT = qw
/dbinit purge db user_list user_entry problem_list problem_entry contest_list contest_entry contest_has_problem job_list job_entry create_job standings update_status rerun_job rerun_problem take_job finish_job open_problem/;
8 our @EXPORT_OK = @EXPORT;
10 use JSON
::MaybeXS qw
/decode_json/;
12 use PerlX
::Maybe qw
/maybe/;
16 use List
::Util qw
/sum/;
19 use constant PROBLEM_PUBLIC_COLUMNS
=> [qw
/id author writer level name owner private timeout olimit value/];
20 use constant JOBS_PER_PAGE
=> 50;
23 user_list_sth
=> 'SELECT * FROM user_list LIMIT 200',
24 user_entry_sth
=> 'SELECT * FROM user_data WHERE id = ?',
26 problem_status_sth
=> 'SELECT problem,solved FROM problem_status WHERE owner = ?',
27 contest_status_sth
=> 'SELECT contest,score,rank FROM contest_status WHERE owner = ?',
29 contest_list_sth
=> 'SELECT * FROM contest_entry',
30 contest_entry_sth
=> 'SELECT * FROM contest_entry WHERE id = ?',
31 contest_has_problem_sth
=> 'SELECT EXISTS(SELECT 1 FROM contest_problems WHERE contest = ? AND problem = ?)',
32 opens_sth
=> 'SELECT problem,owner,time FROM opens WHERE contest = ?',
34 problem_entry_sth
=> 'SELECT ' . (join ',', @
{PROBLEM_PUBLIC_COLUMNS
()}, 'statement', 'solution') . ' FROM problems WHERE id = ?',
35 limits_sth
=> 'SELECT format,timeout FROM limits WHERE problem = ?',
37 job_entry_sth
=> 'SELECT * FROM job_entry WHERE id = ?',
39 rerun_problem_sth
=> 'UPDATE jobs SET daemon=NULL,result=-2,result_text=NULL,results=NULL,errors=NULL WHERE problem = ?',
40 rerun_job_sth
=> 'UPDATE jobs SET daemon=NULL,result=-2,result_text=NULL,results=NULL,errors=NULL WHERE id = ?',
41 take_job_sth
=> 'UPDATE jobs SET daemon=? WHERE id = (SELECT id FROM jobs WHERE daemon IS NULL LIMIT 1 FOR UPDATE) RETURNING id',
48 $db = DBIx
::Simple
->new(@_);
49 $db->keep_statements = 100;
50 $db->dbh->do('SET search_path TO gruntmaster, public');
56 my ($stat, @extra) = @_;
57 $db->query($statements{$stat}, @extra)
60 my (%name_cache, %name_cache_time);
61 use constant NAME_CACHE_MAX_AGE
=> 5;
64 my ($table, $id) = @_;
65 $name_cache_time{$table} //= 0;
66 if (time - $name_cache_time{$table} > NAME_CACHE_MAX_AGE
) {
67 $name_cache_time{$table} = time;
68 $name_cache{$table} = {};
69 $name_cache{$table} = $db->select($table, 'id,name')->map;
72 $name_cache{$table}{$id}
76 sub _add_names
($) { ## no critic (ProhibitSubroutinePrototypes)
78 return unless defined $el;
79 if (ref $el eq 'ARRAY') {
80 &_add_names
($_) for @
$el ## no critic (ProhibitAmpersandSigils)
82 for my $object (qw
/contest owner problem/) {
83 my $table = $object eq 'owner' ?
'users' : "${object}s";
84 $el->{"${object}_name"} = _object_name
$table, $el->{$object} if defined $el->{$object}
91 sub user_list
{ scalar _query
('user_list_sth')->hashes }
95 my $ret = _query
('user_entry_sth', $id)->hash;
96 $ret->{problems
} = _add_names _query
('problem_status_sth', $id)->hashes;
97 $ret->{contests
} = _add_names _query
('contest_status_sth', $id)->hashes;
104 my @columns = @
{PROBLEM_PUBLIC_COLUMNS
()};
105 push @columns, 'solution' if $args{solution
};
107 $where{private
} = 0 unless $args{contest
} || $args{private
};
108 $where{'cp.contest'} = $args{contest
} if $args{contest
};
109 $where{owner
} = $args{owner
} if $args{owner
};
111 my $table = $args{contest
} ?
'problems JOIN contest_problems cp ON cp.problem = id' : 'problems';
112 _add_names
$db->select(\
$table, \
@columns, \
%where, 'name')->hashes
116 my ($id, $contest) = @_;
117 $contest = contest_entry
($contest) if $contest;
118 my $ret = _add_names _query
(problem_entry_sth
=> $id)->hash;
119 my $limits = _query
(limits_sth
=> $id)->hashes;
120 $ret->{limits
} = $limits if @
$limits;
123 $ret->{contest_start
} = $contest->{start
};
124 $ret->{contest_stop
} = $contest->{stop
};
125 delete $ret->{solution
}
131 sub contest_list
{ _add_names _query
('contest_list_sth')->hashes }
133 sub contest_entry
{ _add_names _query
(contest_entry_sth
=> $_[0])->hash }
135 sub contest_has_problem
{ _query
('contest_has_problem_sth', @_[0, 1])->flat }
139 $args{page
} = int ($args{page
} // 1);
141 maybe contest
=> $args{contest
},
142 maybe owner
=> $args{owner
},
143 maybe problem
=> $args{problem
},
144 maybe result
=> $args{result
},
146 $where{private
} = 0 unless $args{private
};
148 my $rows = $db->select('job_entry', 'COUNT(*)', \
%where)->list;
149 my $pages = int (($rows + JOBS_PER_PAGE
- 1) / JOBS_PER_PAGE
);
150 my ($stmt, @bind) = $db->abstract->select('job_entry', '*', \
%where, {-desc
=> 'id'});
151 my $jobs = _add_names
$db->query("$stmt LIMIT " . JOBS_PER_PAGE
. ' OFFSET ' . ($args{page
} - 1) * JOBS_PER_PAGE
, @bind)->hashes;
153 current_page
=> $args{page
},
155 ($args{page
} - 1) ?
(previous_page
=> $args{page
} - 1) : (),
156 ($args{page
} < $pages) ?
(next_page
=> $args{page
} + 1) : (),
158 wantarray ?
($jobs, $pageinfo) : $jobs;
162 my $ret = _add_names _query
(job_entry_sth
=> $_[0])->hash;
163 $ret->{results
} = decode_json
$ret->{results
} if $ret->{results
};
169 $db->update('users', {lastjob
=> time}, {id
=> $args{owner
}});
171 scalar $db->insert('jobs', \
%args, {returning
=> 'id'})->list
175 my ($mxscore, $time, $tries, $totaltime) = @_;
176 my $score = $mxscore;
177 $time = 300 if $time > $totaltime; # uncoverable branch true does not happen anymore (only possible if opens are broken)
178 $score = ($totaltime - $time) / $totaltime * $score;
179 $score -= $tries / 10 * $mxscore;
180 $score = $mxscore * 3 / 10 if $score < $mxscore * 3 / 10;
186 my @problems = sort { $a->{value
} <=> $b->{value
} } @
{problem_list contest
=> $ct};
187 my %values = map { $_->{id
} => $_->{value
} } @problems;
188 $ct = contest_entry
$ct;
190 my (%scores, %tries, %opens);
191 my $opens = _query
(opens_sth
=> $ct->{id
});
192 while ($opens->into(my ($problem, $owner, $time))) {
193 $opens{$problem, $owner} = $time;
196 # result IS NULL if job was never run
197 # result = -2 if job is being rerun
198 my %where = (contest
=> $ct->{id
}, result
=> {'>=', 0});
199 my $jobs = $db->select('job_entry', '*', \
%where, 'id');
201 while (my $job = $jobs->hash) {
202 my $open = $opens{$job->{problem
}, $job->{owner
}} // $ct->{start
};
203 my $time = $job->{date
} - $open;
204 next if $time < 0; # uncoverable branch true job sent before contest is deprecated
205 my $value = $values{$job->{problem
}};
206 my $factor = $job->{result
} ?
0 : 1;
207 $factor = $1 / 100 if $job->{result_text} =~ /^(\d
+ )/s
;
208 $scores{$job->{owner
}}{$job->{problem
}} = int ($factor * _calc_score
($value, $time, $tries{$job->{owner
}}{$job->{problem
}}++, $ct->{stop
} - $ct->{start
}));
211 my @st = sort { $b->{score
} <=> $a->{score
} or $a->{user
} cmp $b->{user
} } map { ## no critic (ProhibitReverseSortBlock)
215 user_name
=> _object_name
(users
=> $user),
216 score
=> sum
(values %{$scores{$user}}),
217 scores
=> [map { $scores{$user}{$_->{id
}} // '-'} @problems],
221 $st[0]->{rank
} = 1 if @st;
222 $st[$_]->{rank
} = $st[$_ - 1]->{rank
} + ($st[$_]->{score
} < $st[$_ - 1]->{score
}) for 1 .. $#st;
228 my $jobs = $db->select('jobs', 'id,owner,problem,result', {-not_bool
=> 'private'}, 'id');
231 while ($jobs->into(my ($id, $owner, $problem, $result))) {
232 $hash{$problem, $owner} = [$id, $result ?
0 : 1];
235 my @problem_statuses = map { [split ($;), @
{$hash{$_}} ] } keys %hash;
237 my @contest_statuses = map {
239 map { [$ct, $_->{user
}, $_->{score
}, $_->{rank
}] } @
{standings
$ct}
240 } $db->select('contests', 'id')->flat;
243 $db->delete('problem_status');
244 $db->query('INSERT INTO problem_status (problem,owner,job,solved) VALUES (??)', @
$_) for @problem_statuses;
245 $db->delete('contest_status');
246 $db->query('INSERT INTO contest_status (contest,owner,score,rank) VALUES (??)', @
$_) for @contest_statuses;
252 _query rerun_problem_sth
=> $problem;
258 _query rerun_job_sth
=> $id;
265 my $id = _query
(take_job_sth
=> $daemon)->list;
269 db
->select(jobs
=> '*', {id
=> $id})->hash
273 my ($job, $private, %args) = @_;
274 db
->update(jobs
=> \
%args, {id
=> $job->{id
}});
276 purge
'/log/' . $job->{id
};
277 purge
'/st/' . $job->{contest
} if $job->{contest
};
280 problem
=> $job->{problem
},
281 owner
=> $job->{owner
},
283 solved
=> ($args{result
} ?
0 : 1),
286 db
->insert(problem_status
=> $status)
287 } or db
->update(problem_status
=> $status, {owner
=> $job->{owner
}, problem
=> $job->{problem
}});
288 purge
'/us/' . $job->{owner
};
292 my ($contest, $problem, $owner, $time) = @_;
293 my $ct = contest_entry
($contest);
294 return unless $ct->{id
} && $time >= $ct->{start
} && $time < $ct->{stop
}; ## no critic (ProhibitNegativeExpressionsInUnlessAndUntilConditions)
295 eval { db
->insert(opens
=> { ## no critic (RequireCheckingReturnValueOfEval)
302 my @PURGE_HOSTS = exists $ENV{PURGE_HOSTS
} ?
split ' ', $ENV{PURGE_HOSTS
} : ();
303 my $ht = HTTP
::Tiny
->new;
306 $ht->request(PURGE
=> "http://$_$_[0]") for @PURGE_HOSTS;
317 Gruntmaster::Data - Gruntmaster 6000 Online Judge -- database interface and tools
324 Gruntmaster::Data is the interface to the Gruntmaster 6000 database.
326 All functions are exported by default.
330 =item B<dbinit>(I<@args>)
332 This function connects to the database. I<@args> are the arguments
333 passed to the L<DBIx::Simple> constructor.
335 =item B<purge>(I<$url_path>)
337 Purges a relative URL from the Varnish Cache by sending PURGE
338 $url_path requests to all hosts in the PURGE_HOSTS environment
343 Returns a L<DBIx::Simple> object for interacting with the database
344 directly. Use this when no other function in this module is suitable.
348 Returns an arrayref of the top 200 users.
350 =item B<user_entry>(I<$id>)
352 Returns a hashref describing the user I<$id>.
354 =item B<problem_list>([I<%args>])
356 Returns an arrayref of problems.
358 Takes the following named arguments:
364 Only show problems owned by this user
368 Only show problems in this contest
372 If true, include private problems. Always true if contest is present.
376 If true, include problem solutions
380 =item B<problem_entry>(i<$id>, [I<$contest>])
382 Returns a hashref describing the problem I<$id>. If $contest is
383 present, contest start and stop times are included, and the solution
386 =item B<contest_list>
388 Returns an arrayref of contests.
390 =item B<contest_entry>(I<$id>)
392 Returns a hashref describing the contest I<$id>.
394 =item B<contest_has_problem>(I<$contest>, I<$problem>)
396 Returns true if the contest I<$contest> includes the problem
397 I<$problem>, false otherwise.
399 =item B<job_list>([I<%args>])
401 In scalar context, returns an arrayref of jobs. In list context,
402 returns an arrayref of jobs and a hashref of information about pages.
404 Takes the following named arguments:
410 Show this page of the job log. Defaults to 1.
414 Only show jobs submitted by this user.
418 Only show jobs submitted in this contest.
422 Only show jobs submitted for this problem.
426 Only show jobs with this result (see the constants in
427 L<Gruntmaster::Daemon::Constants>).
431 If true, include private jobs. Defaults to false.
435 =item B<job_entry>(I<$id>)
437 Returns a hashref describing the job I<$id>.
439 =item B<create_job>(I<%args>)
441 Insert a new job into the database. This function also updates the
442 lastjob field for the job's owner.
444 =item B<standings>(I<$ct>)
446 Returns an arrayref of the standings of contest I<$ct>.
448 =item B<update_status>
450 Rebuilds the problem_status and contest_status tables.
452 =item B<rerun_job>(I<$id>)
454 Marks the job $id as pending and clears its results, so that it will
455 be run again by the daemon.
457 =item B<take_job>(I<$daemon>)
459 Marks a random job as being run by I<$daemon>. Returns a hashref
460 describing the job, or undef if no job was available.
462 =item B<finish_job>(I<$job>, I<$private>, I<%results>)
464 Updates the job $job with the results in %results. If $private is
465 false, also updates the problem_status table.
467 =item B<open_problem>(I<$contest>, I<$problem>, I<$owner>, I<$time>)
469 Notes that I<$owner> has opened the problem I<$problem> of contest
470 I<$contest> at time I<$time>. If the C<opens> table already contains
471 this (I<$contest>, I<$problem>, I<$owner>) triplet, this function does
478 Marius Gavrilescu E<lt>marius@ieval.roE<gt>
480 =head1 COPYRIGHT AND LICENSE
482 Copyright (C) 2014-2016 by Marius Gavrilescu
484 This library is free software; you can redistribute it and/or modify
485 it under the same terms as Perl itself, either Perl version 5.20.1 or,
486 at your option, any later version of Perl 5 you may have available.