Files
lix/src/libstore/build/worker.cc
T
eldritch horrors aa33c34c9b libstore: merge ContinueImmediately and StillAlive
nothing needs to signal being still active but not actively pollable,
only that immediate polling for the next goal work phase is in order.

Change-Id: Ia43c1015e94ba4f5f6b9cb92943da608c4a01555
2024-09-29 15:29:56 +00:00

411 lines
12 KiB
C++

#include "charptr-cast.hh"
#include "worker.hh"
#include "finally.hh"
#include "substitution-goal.hh"
#include "drv-output-substitution-goal.hh"
#include "local-derivation-goal.hh"
#include "signals.hh"
#include "hook-instance.hh" // IWYU pragma: keep
namespace nix {
namespace {
struct ErrorHandler : kj::TaskSet::ErrorHandler
{
void taskFailed(kj::Exception && e) override
{
printError("unexpected async failure in Worker: %s", kj::str(e).cStr());
abort();
}
} errorHandler;
}
Worker::Worker(Store & store, Store & evalStore, kj::AsyncIoContext & aio)
: act(*logger, actRealise)
, actDerivations(*logger, actBuilds)
, actSubstitutions(*logger, actCopyPaths)
, store(store)
, evalStore(evalStore)
, aio(aio)
/* Make sure that we are always allowed to run at least one substitution.
This prevents infinite waiting. */
, substitutions(std::max<unsigned>(1, settings.maxSubstitutionJobs))
, localBuilds(settings.maxBuildJobs)
, children(errorHandler)
{
/* Debugging: prevent recursive workers. */
}
Worker::~Worker()
{
/* Explicitly get rid of all strong pointers now. After this all
goals that refer to this worker should be gone. (Otherwise we
are in trouble, since goals may call childTerminated() etc. in
their destructors). */
topGoals.clear();
awake.clear();
children.clear();
assert(expectedSubstitutions == 0);
assert(expectedDownloadSize == 0);
assert(expectedNarSize == 0);
}
template<typename ID, std::derived_from<Goal> G>
std::pair<std::shared_ptr<G>, kj::Promise<void>> Worker::makeGoalCommon(
std::map<ID, CachedGoal<G>> & map,
const ID & key,
InvocableR<std::unique_ptr<G>> auto create,
std::invocable<G &> auto modify
)
{
auto [it, _inserted] = map.try_emplace(key);
auto & goal_weak = it->second;
auto goal = goal_weak.goal.lock();
if (!goal) {
struct Deleter
{
std::map<ID, CachedGoal<G>> * from;
std::map<ID, CachedGoal<G>>::const_iterator iter;
void operator()(G * g)
{
from->erase(iter);
delete g;
}
};
// this dance is necessary to be memory-safe in exceptional cases.
// if *anything* here throws we must still delete the goal object.
goal = [&] {
std::unique_ptr<G, Deleter> tmp(nullptr, Deleter{&map, it});
tmp.reset(create().release());
return tmp;
}();
goal->notify = std::move(goal_weak.fulfiller);
goal_weak.goal = goal;
wakeUp(goal);
} else {
modify(*goal);
}
return {goal, goal_weak.promise->addBranch()};
}
std::pair<std::shared_ptr<DerivationGoal>, kj::Promise<void>> Worker::makeDerivationGoal(
const StorePath & drvPath, const OutputsSpec & wantedOutputs, BuildMode buildMode
)
{
return makeGoalCommon(
derivationGoals,
drvPath,
[&]() -> std::unique_ptr<DerivationGoal> {
return !dynamic_cast<LocalStore *>(&store)
? std::make_unique<DerivationGoal>(
drvPath, wantedOutputs, *this, running, buildMode
)
: LocalDerivationGoal::makeLocalDerivationGoal(
drvPath, wantedOutputs, *this, running, buildMode
);
},
[&](DerivationGoal & g) { g.addWantedOutputs(wantedOutputs); }
);
}
std::pair<std::shared_ptr<DerivationGoal>, kj::Promise<void>> Worker::makeBasicDerivationGoal(
const StorePath & drvPath,
const BasicDerivation & drv,
const OutputsSpec & wantedOutputs,
BuildMode buildMode
)
{
return makeGoalCommon(
derivationGoals,
drvPath,
[&]() -> std::unique_ptr<DerivationGoal> {
return !dynamic_cast<LocalStore *>(&store)
? std::make_unique<DerivationGoal>(
drvPath, drv, wantedOutputs, *this, running, buildMode
)
: LocalDerivationGoal::makeLocalDerivationGoal(
drvPath, drv, wantedOutputs, *this, running, buildMode
);
},
[&](DerivationGoal & g) { g.addWantedOutputs(wantedOutputs); }
);
}
std::pair<std::shared_ptr<PathSubstitutionGoal>, kj::Promise<void>>
Worker::makePathSubstitutionGoal(
const StorePath & path, RepairFlag repair, std::optional<ContentAddress> ca
)
{
return makeGoalCommon(
substitutionGoals,
path,
[&] { return std::make_unique<PathSubstitutionGoal>(path, *this, running, repair, ca); },
[&](auto &) {}
);
}
std::pair<std::shared_ptr<DrvOutputSubstitutionGoal>, kj::Promise<void>>
Worker::makeDrvOutputSubstitutionGoal(
const DrvOutput & id, RepairFlag repair, std::optional<ContentAddress> ca
)
{
return makeGoalCommon(
drvOutputSubstitutionGoals,
id,
[&] { return std::make_unique<DrvOutputSubstitutionGoal>(id, *this, running, repair, ca); },
[&](auto &) {}
);
}
std::pair<GoalPtr, kj::Promise<void>> Worker::makeGoal(const DerivedPath & req, BuildMode buildMode)
{
return std::visit(overloaded {
[&](const DerivedPath::Built & bfd) -> std::pair<GoalPtr, kj::Promise<void>> {
if (auto bop = std::get_if<DerivedPath::Opaque>(&*bfd.drvPath))
return makeDerivationGoal(bop->path, bfd.outputs, buildMode);
else
throw UnimplementedError("Building dynamic derivations in one shot is not yet implemented.");
},
[&](const DerivedPath::Opaque & bo) -> std::pair<GoalPtr, kj::Promise<void>> {
return makePathSubstitutionGoal(bo.path, buildMode == bmRepair ? Repair : NoRepair);
},
}, req.raw());
}
void Worker::goalFinished(GoalPtr goal, Goal::Finished & f)
{
goal->trace("done");
assert(!goal->exitCode.has_value());
goal->exitCode = f.exitCode;
goal->ex = f.ex;
permanentFailure |= f.permanentFailure;
timedOut |= f.timedOut;
hashMismatch |= f.hashMismatch;
checkMismatch |= f.checkMismatch;
goal->notify->fulfill();
goal->cleanup();
if (topGoals.find(goal) != topGoals.end()) {
topGoals.erase(goal);
/* If a top-level goal failed, then kill all other goals
(unless keepGoing was set). */
if (goal->exitCode == Goal::ecFailed && !settings.keepGoing)
topGoals.clear();
}
}
void Worker::handleWorkResult(GoalPtr goal, Goal::WorkResult how)
{
std::visit(
overloaded{
[&](Goal::StillAlive) { wakeUp(goal); },
[&](Goal::Finished & f) { goalFinished(goal, f); },
},
how
);
updateStatistics();
}
void Worker::wakeUp(GoalPtr goal)
{
goal->trace("woken up");
awake.insert(goal);
}
void Worker::childStarted(GoalPtr goal, kj::Promise<Result<Goal::WorkResult>> promise)
{
children.add(promise
.then([this, goal](auto result) {
if (result.has_value()) {
handleWorkResult(goal, std::move(result.assume_value()));
} else {
childException = result.assume_error();
}
})
.attach(Finally{[this, goal] {
childTerminated(goal);
}}));
}
void Worker::childTerminated(GoalPtr goal)
{
if (childFinished) {
childFinished->fulfill();
}
}
void Worker::updateStatistics()
{
// only update progress info while running. this notably excludes updating
// progress info while destroying, which causes the progress bar to assert
if (running && statisticsOutdated) {
actDerivations.progress(
doneBuilds, expectedBuilds + doneBuilds, runningBuilds, failedBuilds
);
actSubstitutions.progress(
doneSubstitutions,
expectedSubstitutions + doneSubstitutions,
runningSubstitutions,
failedSubstitutions
);
act.setExpected(actFileTransfer, expectedDownloadSize + doneDownloadSize);
act.setExpected(actCopyPath, expectedNarSize + doneNarSize);
statisticsOutdated = false;
}
}
std::vector<GoalPtr> Worker::run(std::function<Targets (GoalFactory &)> req)
{
auto _topGoals = req(goalFactory());
assert(!running);
running = true;
Finally const _stop([&] { running = false; });
updateStatistics();
topGoals.clear();
for (auto & [goal, _promise] : _topGoals) {
topGoals.insert(goal);
}
debug("entered goal loop");
while (1) {
checkInterrupt();
// TODO GC interface?
if (auto localStore = dynamic_cast<LocalStore *>(&store))
localStore->autoGC(false);
/* Call every wake goal (in the ordering established by
CompareGoalPtrs). */
while (!awake.empty() && !topGoals.empty()) {
Goals awake2 = std::move(awake);
for (auto & goal : awake2) {
checkInterrupt();
auto result = goal->work();
if (result.poll(aio.waitScope)) {
handleWorkResult(goal, result.wait(aio.waitScope).value());
} else {
childStarted(goal, std::move(result));
}
if (topGoals.empty()) break; // stuff may have been cancelled
}
}
if (topGoals.empty()) break;
/* Wait for input. */
if (!children.isEmpty())
waitForInput();
else {
assert(!awake.empty());
}
if (childException) {
std::rethrow_exception(childException);
}
}
/* If --keep-going is not set, it's possible that the main goal
exited while some of its subgoals were still active. But if
--keep-going *is* set, then they must all be finished now. */
assert(!settings.keepGoing || awake.empty());
assert(!settings.keepGoing || children.isEmpty());
std::vector<GoalPtr> results;
for (auto & [i, _p] : _topGoals) {
results.push_back(i);
}
return results;
}
void Worker::waitForInput()
{
printMsg(lvlVomit, "waiting for children");
auto waitFor = [&]{
auto pair = kj::newPromiseAndFulfiller<void>();
this->childFinished = kj::mv(pair.fulfiller);
return kj::mv(pair.promise);
}();
if (settings.minFree.get() != 0) {
// Periodicallty wake up to see if we need to run the garbage collector.
waitFor = waitFor.exclusiveJoin(aio.provider->getTimer().afterDelay(10 * kj::SECONDS));
}
waitFor.wait(aio.waitScope);
}
unsigned int Worker::failingExitStatus()
{
// See API docs in header for explanation
unsigned int mask = 0;
bool buildFailure = permanentFailure || timedOut || hashMismatch;
if (buildFailure)
mask |= 0x04; // 100
if (timedOut)
mask |= 0x01; // 101
if (hashMismatch)
mask |= 0x02; // 102
if (checkMismatch) {
mask |= 0x08; // 104
}
if (mask)
mask |= 0x60;
return mask ? mask : 1;
}
bool Worker::pathContentsGood(const StorePath & path)
{
auto i = pathContentsGoodCache.find(path);
if (i != pathContentsGoodCache.end()) return i->second;
printInfo("checking path '%s'...", store.printStorePath(path));
auto info = store.queryPathInfo(path);
bool res;
if (!pathExists(store.printStorePath(path)))
res = false;
else {
HashResult current = hashPath(info->narHash.type, store.printStorePath(path));
Hash nullHash(HashType::SHA256);
res = info->narHash == nullHash || info->narHash == current.first;
}
pathContentsGoodCache.insert_or_assign(path, res);
if (!res)
printError("path '%s' is corrupted or missing!", store.printStorePath(path));
return res;
}
void Worker::markContentsGood(const StorePath & path)
{
pathContentsGoodCache.insert_or_assign(path, true);
}
}