improve infinite recursion errors

This commit is contained in:
Jörg Thalheim 2023-12-16 10:10:49 +01:00
parent 83df9d4e24
commit 4d97e5a386
3 changed files with 86 additions and 19 deletions

View file

@ -104,7 +104,7 @@ struct State {
std::exception_ptr exc; std::exception_ptr exc;
}; };
void handleBrokenWorkerPipe(Proc &proc) { void handleBrokenWorkerPipe(Proc &proc, std::string_view msg) {
// we already took the process status from Proc, no // we already took the process status from Proc, no
// need to wait for it again to avoid error messages // need to wait for it again to avoid error messages
pid_t pid = proc.pid.release(); pid_t pid = proc.pid.release();
@ -113,27 +113,49 @@ void handleBrokenWorkerPipe(Proc &proc) {
int rc = waitpid(pid, &status, WNOHANG); int rc = waitpid(pid, &status, WNOHANG);
if (rc == 0) { if (rc == 0) {
kill(pid, SIGKILL); kill(pid, SIGKILL);
throw Error("BUG: worker pipe closed but worker still running?"); throw Error(
"BUG: while %s, worker pipe got closed but evaluation worker still running?",
msg);
} else if (rc == -1) { } else if (rc == -1) {
kill(pid, SIGKILL); kill(pid, SIGKILL);
throw Error("BUG: waitpid waiting for worker failed: %s", throw Error("BUG: while %s, waitpid for evaluation worker failed: %s",
strerror(errno)); msg, strerror(errno));
} else { } else {
if (WIFEXITED(status)) { if (WIFEXITED(status)) {
throw Error("evaluation worker exited with %d", if (WEXITSTATUS(status) == 1) {
WEXITSTATUS(status)); throw Error(
"while %s, evaluation worker exited with exit code 1, "
"(possibly an infinite recursion)",
msg);
}
throw Error("while %s, evaluation worker exited with %d",
msg, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) { } else if (WIFSIGNALED(status)) {
if (WTERMSIG(status) == SIGKILL) { if (WTERMSIG(status) == SIGKILL) {
throw Error("evaluation worker killed by SIGKILL, maybe " throw Error(
"memory limit reached?"); "while %s, evaluation worker got killed by SIGKILL, maybe "
"memory limit reached?",
msg);
} }
throw Error("evaluation worker killed by signal %d (%s)", throw Error(
WTERMSIG(status), strsignal(WTERMSIG(status))); "while %s, evaluation worker got killed by signal %d (%s)",
msg, WTERMSIG(status), strsignal(WTERMSIG(status)));
} // else ignore WIFSTOPPED and WIFCONTINUED } // else ignore WIFSTOPPED and WIFCONTINUED
} }
} }
} }
std::string joinAttrPath(json &attrPath) {
std::string joined;
for (auto& element : attrPath) {
if (!joined.empty()) {
joined += '.';
}
joined += element.get<std::string>();
}
return joined;
}
void collector(Sync<State> &state_, std::condition_variable &wakeup) { void collector(Sync<State> &state_, std::condition_variable &wakeup) {
try { try {
std::optional<std::unique_ptr<Proc>> proc_; std::optional<std::unique_ptr<Proc>> proc_;
@ -151,7 +173,7 @@ void collector(Sync<State> &state_, std::condition_variable &wakeup) {
/* Check whether the existing worker process is still there. */ /* Check whether the existing worker process is still there. */
auto s = fromReader->readLine(); auto s = fromReader->readLine();
if (s.empty()) { if (s.empty()) {
handleBrokenWorkerPipe(*proc.get()); handleBrokenWorkerPipe(*proc.get(), "checking worker process");
} else if (s == "restart") { } else if (s == "restart") {
proc_ = std::nullopt; proc_ = std::nullopt;
fromReader_ = std::nullopt; fromReader_ = std::nullopt;
@ -176,7 +198,7 @@ void collector(Sync<State> &state_, std::condition_variable &wakeup) {
if ((state->todo.empty() && state->active.empty()) || if ((state->todo.empty() && state->active.empty()) ||
state->exc) { state->exc) {
if (tryWriteLine(proc->to.get(), "exit") < 0) { if (tryWriteLine(proc->to.get(), "exit") < 0) {
handleBrokenWorkerPipe(*proc.get()); handleBrokenWorkerPipe(*proc.get(), "sending exit");
} }
return; return;
} }
@ -191,13 +213,15 @@ void collector(Sync<State> &state_, std::condition_variable &wakeup) {
/* Tell the worker to evaluate it. */ /* Tell the worker to evaluate it. */
if (tryWriteLine(proc->to.get(), "do " + attrPath.dump()) < 0) { if (tryWriteLine(proc->to.get(), "do " + attrPath.dump()) < 0) {
handleBrokenWorkerPipe(*proc.get()); auto msg = "sending attrPath '" + joinAttrPath(attrPath) + "'";
handleBrokenWorkerPipe(*proc.get(), msg);
} }
/* Wait for the response. */ /* Wait for the response. */
auto respString = fromReader->readLine(); auto respString = fromReader->readLine();
if (respString.empty()) { if (respString.empty()) {
handleBrokenWorkerPipe(*proc.get()); auto msg = "reading result for attrPath '" + joinAttrPath(attrPath) + "'";
handleBrokenWorkerPipe(*proc.get(), msg);
} }
json response; json response;
try { try {

View file

@ -8,6 +8,22 @@
{ {
hydraJobs = import ./ci.nix { inherit pkgs; }; hydraJobs = import ./ci.nix { inherit pkgs; };
legacyPackages.x86_64-linux.brokenPackage = throw "this is an evaluation error"; legacyPackages.x86_64-linux = {
brokenPkgs = {
brokenPackage = throw "this is an evaluation error";
};
infiniteRecursionPkgs = {
packageWithInfiniteRecursion =
let
recursion = [ recursion ];
in
derivation {
inherit (pkgs) system;
name = "drvB";
recursiveAttr = recursion;
builder = ":";
};
};
};
}; };
} }

View file

@ -1,10 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import subprocess
import json import json
from tempfile import TemporaryDirectory import subprocess
from pathlib import Path from pathlib import Path
from typing import List, Dict, Any from tempfile import TemporaryDirectory
from typing import Any, Dict, List
TEST_ROOT = Path(__file__).parent.resolve() TEST_ROOT = Path(__file__).parent.resolve()
PROJECT_ROOT = TEST_ROOT.parent PROJECT_ROOT = TEST_ROOT.parent
@ -77,8 +77,10 @@ def test_eval_error() -> None:
"--gc-roots-dir", "--gc-roots-dir",
tempdir, tempdir,
"--meta", "--meta",
"--workers",
"1",
"--flake", "--flake",
".#legacyPackages.x86_64-linux", ".#legacyPackages.x86_64-linux.brokenPkgs",
] ]
res = subprocess.run( res = subprocess.run(
cmd, cmd,
@ -86,6 +88,31 @@ def test_eval_error() -> None:
text=True, text=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
) )
print(res.stdout)
attrs = json.loads(res.stdout) attrs = json.loads(res.stdout)
assert attrs["attr"] == "brokenPackage" assert attrs["attr"] == "brokenPackage"
assert "this is an evaluation error" in attrs["error"] assert "this is an evaluation error" in attrs["error"]
def test_recursion_error() -> None:
with TemporaryDirectory() as tempdir:
cmd = [
str(BIN),
"--gc-roots-dir",
tempdir,
"--meta",
"--workers",
"1",
"--flake",
".#legacyPackages.x86_64-linux.infiniteRecursionPkgs",
]
res = subprocess.run(
cmd,
cwd=TEST_ROOT.joinpath("assets"),
text=True,
stderr=subprocess.PIPE,
)
assert res.returncode == 1
print(res.stderr)
assert 'packageWithInfiniteRecursion' in res.stderr
assert "possible infinite recursion" in res.stderr