Skip to content

Commit 250c3a8

Browse files
committed
Make forking to report back result more elegant.
1 parent 276e115 commit 250c3a8

File tree

1 file changed

+82
-54
lines changed

1 file changed

+82
-54
lines changed

judge/judgedaemon.main.php

Lines changed: 82 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function __construct()
7272
{
7373
self::$instance = $this;
7474

75-
$this->options = getopt("dv:n:hVe:j:t:", ["diskspace-error"]);
75+
$this->options = getopt("dv:n:hV", ["diskspace-error"]);
7676
if ($this->options === false) {
7777
echo "Error: parsing options failed.\n";
7878
$this->usage();
@@ -133,7 +133,7 @@ public function __construct()
133133
$runuser .= '-' . $this->options['daemonid'];
134134
}
135135

136-
if ($runuser === posix_getpwuid(posix_geteuid())['name'] ||
136+
if ($runuser === posix_getpwuid(posix_geteuid())['name'] ||
137137
RUNGROUP === posix_getgrgid(posix_getegid())['name']
138138
) {
139139
error("Do not run the judgedaemon as the runuser or rungroup.");
@@ -195,39 +195,7 @@ public function __construct()
195195

196196
$this->read_credentials();
197197

198-
if (!empty($this->options['e'])) {
199-
$this->endpointID = $this->options['e'];
200-
$endpoint = $this->endpoints[$this->endpointID];
201-
$this->endpoints[$this->endpointID]['ch'] = $this->setup_curl_handle($endpoint['user'], $endpoint['pass']);
202-
$new_judging_run = (array)dj_json_decode(base64_decode(file_get_contents($this->options['j'])));
203-
$judgeTaskId = $this->options['t'];
204-
205-
$success = false;
206-
for ($i = 0; $i < 5; $i++) {
207-
if ($i > 0) {
208-
$sleep_ms = 100 + random_int(200, ($i + 1) * 1000);
209-
dj_sleep(0.001 * $sleep_ms);
210-
}
211-
$response = $this->request(
212-
sprintf('judgehosts/add-judging-run/%s/%s', $new_judging_run['hostname'],
213-
urlencode((string)$judgeTaskId)),
214-
'POST',
215-
$new_judging_run,
216-
false
217-
);
218-
if ($response !== null) {
219-
logmsg(LOG_DEBUG, "Adding judging run result for jt$judgeTaskId successful.");
220-
$success = true;
221-
break;
222-
}
223-
logmsg(LOG_WARNING, "Failed to report jt$judgeTaskId in attempt #" . ($i + 1) . ".");
224-
}
225-
if (!$success) {
226-
error("Final attempt of uploading jt$judgeTaskId was unsuccessful, giving up.");
227-
}
228-
unlink($this->options['j']);
229-
exit(0);
230-
}
198+
231199
}
232200

233201
public function run(): void
@@ -310,6 +278,12 @@ private function loop(): void
310278
if (function_exists('pcntl_signal_dispatch')) {
311279
pcntl_signal_dispatch();
312280
}
281+
if (function_exists('pcntl_waitpid')) {
282+
// Reap any finished child processes.
283+
while (pcntl_waitpid(-1, $status, WNOHANG) > 0) {
284+
// Do nothing.
285+
}
286+
}
313287
if ($this->exitsignalled) {
314288
logmsg(LOG_NOTICE, "Received signal, exiting.");
315289
$this->close_curl_handles();
@@ -1123,7 +1097,7 @@ private function registerJudgehost(): void
11231097
foreach ($unfinished as $jud) {
11241098
$workdir = $this->judging_directory($workdirpath, $jud);
11251099
@chmod($workdir, 0700);
1126-
logmsg(LOG_WARNING, "Found unfinished judging with jobid " . $jud['jobid'] .
1100+
logmsg(LOG_WARNING, "Found unfinished judging with jobid " . $jud['jobid'] .
11271101
" in my name; given back unfinished runs from me.");
11281102
}
11291103
}
@@ -1651,27 +1625,12 @@ private function run_testcase(
16511625

16521626
$ret = true;
16531627
if ($result === 'correct') {
1654-
// Post result back asynchronously. PHP is lacking multi-threading, so
1655-
// we just call ourselves again.
1656-
$tmpfile = tempnam(TMPDIR, 'judging_run_');
1657-
file_put_contents($tmpfile, base64_encode(dj_json_encode($new_judging_run)));
1658-
$judgedaemon = BINDIR . '/judgedaemon';
1659-
$cmd = $judgedaemon
1660-
. ' -e ' . $this->endpointID
1661-
. ' -t ' . $judgeTask['judgetaskid']
1662-
. ' -j ' . $tmpfile
1663-
. ' >> /dev/null & ';
1664-
shell_exec($cmd);
1628+
// Correct results get reported asynchronously, so we can continue judging in parallel.
1629+
$this->reportJudgingRun($judgeTask, $new_judging_run, asynchronous: true);
16651630
} else {
16661631
// This run was incorrect, only continue with the remaining judge tasks
16671632
// if we are told to do so.
1668-
$needsMoreWork = $this->request(
1669-
sprintf('judgehosts/add-judging-run/%s/%s', urlencode($this->myhost),
1670-
urlencode((string)$judgeTask['judgetaskid'])),
1671-
'POST',
1672-
$new_judging_run,
1673-
false
1674-
);
1633+
$needsMoreWork = $this->reportJudgingRun($judgeTask, $new_judging_run, asynchronous: false);
16751634
$ret = (bool)$needsMoreWork;
16761635
}
16771636

@@ -1685,6 +1644,75 @@ private function run_testcase(
16851644
return $ret;
16861645
}
16871646

1647+
private function reportJudgingRun(array $judgeTask, array $new_judging_run, bool $asynchronous): ?string
1648+
{
1649+
$judgeTaskId = $judgeTask['judgetaskid'];
1650+
1651+
if ($asynchronous && function_exists('pcntl_fork')) {
1652+
$pid = pcntl_fork();
1653+
if ($pid === -1) {
1654+
logmsg(LOG_WARNING, "Could not fork to report result for jt$judgeTaskId asynchronously, reporting synchronously.");
1655+
// Fallback to synchronous reporting by continuing in this process.
1656+
} elseif ($pid > 0) {
1657+
// Parent process, nothing more to do here.
1658+
logmsg(LOG_DEBUG, "Forked a child with PID $pid to report judging run for jt$judgeTaskId.");
1659+
return null;
1660+
} else {
1661+
// Child process: reset signal handlers to default.
1662+
pcntl_signal(SIGTERM, SIG_DFL);
1663+
pcntl_signal(SIGINT, SIG_DFL);
1664+
pcntl_signal(SIGHUP, SIG_DFL);
1665+
pcntl_signal(SIGUSR1, SIG_DFL);
1666+
1667+
// The child should use its own curl handle to avoid issues with sharing handles
1668+
// between processes.
1669+
$endpoint = $this->endpoints[$this->endpointID];
1670+
$this->endpoints[$this->endpointID]['ch'] = $this->setup_curl_handle($endpoint['user'], $endpoint['pass']);
1671+
}
1672+
} elseif ($asynchronous) {
1673+
logmsg(LOG_WARNING, "pcntl extension not available, reporting result for jt$judgeTaskId synchronously.");
1674+
}
1675+
1676+
$isChild = isset($pid) && $pid === 0;
1677+
1678+
$success = false;
1679+
for ($i = 0; $i < 5; $i++) {
1680+
if ($i > 0) {
1681+
$sleep_ms = 100 + random_int(200, ($i + 1) * 1000);
1682+
dj_sleep(0.001 * $sleep_ms);
1683+
}
1684+
$response = $this->request(
1685+
sprintf('judgehosts/add-judging-run/%s/%s', $new_judging_run['hostname'],
1686+
urlencode((string)$judgeTaskId)),
1687+
'POST',
1688+
$new_judging_run,
1689+
false
1690+
);
1691+
if ($response !== null) {
1692+
logmsg(LOG_DEBUG, "Adding judging run result for jt$judgeTaskId successful.");
1693+
$success = true;
1694+
break;
1695+
}
1696+
logmsg(LOG_WARNING, "Failed to report jt$judgeTaskId in attempt #" . ($i + 1) . ".");
1697+
}
1698+
1699+
if (!$success) {
1700+
$message = "Final attempt of uploading jt$judgeTaskId was unsuccessful, giving up.";
1701+
if ($isChild) {
1702+
error($message);
1703+
} else {
1704+
warning($message);
1705+
return null;
1706+
}
1707+
}
1708+
1709+
if ($isChild) {
1710+
exit(0);
1711+
}
1712+
1713+
return $response;
1714+
}
1715+
16881716
private function fetchTestcase(string $workdirpath, string $testcase_id, int $judgetaskid, string $testcase_hash): ?array
16891717
{
16901718
// Get both in- and output files, only if we didn't have them already.

0 commit comments

Comments
 (0)