Compare commits

..

5 Commits

Author SHA1 Message Date
364e2dbf68
add gitea refs
Signed-off-by: ahuston-0 <aliceghuston@gmail.com>
Reviewed-on: https://<censored>/ahuston-0/hydra/pulls/1
2025-04-01 14:58:40 -04:00
6d2b98c587
add Gitea pulls docs entry
Signed-off-by: ahuston-0 <aliceghuston@gmail.com>
2025-04-01 14:58:33 -04:00
Faye Chun
ac47bb8607
Merge remote-tracking branch 'origin/master' into add-gitea-pulls 2025-04-01 02:07:47 -04:00
Faye Chun
99e3ad325c
Merge branch 'NixOS:master' into add-gitea-pulls 2025-03-01 22:04:13 -05:00
Faye Chun
2f1fa2b069
Add a plugin to poll Gitea pull requests
Based off the existing GithubPulls.pm and GitlabPulls.pm plugins.

Also adds an integration test for the new 'giteapulls' input type to
the existing 'gitea' test.
2024-12-21 08:02:57 -05:00
46 changed files with 242 additions and 507 deletions

View File

@ -1,10 +1,7 @@
name: "Test" name: "Test"
on: on:
pull_request: pull_request:
merge_group:
push: push:
branches:
- master
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest

20
flake.lock generated
View File

@ -12,16 +12,16 @@
"nixpkgs-regression": [] "nixpkgs-regression": []
}, },
"locked": { "locked": {
"lastModified": 1744030329, "lastModified": 1739899400,
"narHash": "sha256-r+psCOW77vTSTNbxTVrYHeh6OgB0QukbnyUVDwg8s4I=", "narHash": "sha256-q/RgA4bB7zWai4oPySq9mch7qH14IEeom2P64SXdqHs=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nix", "repo": "nix",
"rev": "a4962f73b5fc874d4b16baef47921daf349addfc", "rev": "e310c19a1aeb1ce1ed4d41d5ab2d02db596e0918",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "2.28-maintenance", "ref": "2.26-maintenance",
"repo": "nix", "repo": "nix",
"type": "github" "type": "github"
} }
@ -29,11 +29,11 @@
"nix-eval-jobs": { "nix-eval-jobs": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1744018595, "lastModified": 1743008255,
"narHash": "sha256-v5n6t49X7MOpqS9j0FtI6TWOXvxuZMmGsp2OfUK5QfA=", "narHash": "sha256-Lo4KFBNcY8tmBuCmEr2XV0IUZtxXHmbXPNLkov/QSU0=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nix-eval-jobs", "repo": "nix-eval-jobs",
"rev": "cba718bafe5dc1607c2b6761ecf53c641a6f3b21", "rev": "f7418fc1fa45b96d37baa95ff3c016dd5be3876b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -44,11 +44,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1743987495, "lastModified": 1739461644,
"narHash": "sha256-46T2vMZ4/AfCK0Y2OjlFzJPxmdpP8GtsuEqSSJv3oe4=", "narHash": "sha256-1o1qR0KYozYGRrnqytSpAhVBYLNBHX+Lv6I39zGRzKM=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "db8f4fe18ce772a9c8f3adf321416981c8fe9371", "rev": "97a719c9f0a07923c957cf51b20b329f9fb9d43f",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -4,7 +4,7 @@
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11-small";
inputs.nix = { inputs.nix = {
url = "github:NixOS/nix/2.28-maintenance"; url = "github:NixOS/nix/2.26-maintenance";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
# hide nix dev tooling from our lock file # hide nix dev tooling from our lock file
@ -34,6 +34,7 @@
hydra = final.callPackage ./package.nix { hydra = final.callPackage ./package.nix {
inherit (nixpkgs.lib) fileset; inherit (nixpkgs.lib) fileset;
rawSrc = self; rawSrc = self;
nix-perl-bindings = final.nixComponents.nix-perl-bindings;
}; };
}; };
@ -72,29 +73,21 @@
validate-openapi = hydraJobs.tests.validate-openapi.${system}; validate-openapi = hydraJobs.tests.validate-openapi.${system};
}); });
packages = forEachSystem (system: let packages = forEachSystem (system: {
nixComponents = {
inherit (nix.packages.${system})
nix-util
nix-store
nix-expr
nix-fetchers
nix-flake
nix-main
nix-cmd
nix-cli
nix-perl-bindings
;
};
in {
nix-eval-jobs = nixpkgs.legacyPackages.${system}.callPackage nix-eval-jobs { nix-eval-jobs = nixpkgs.legacyPackages.${system}.callPackage nix-eval-jobs {
inherit nixComponents; nix = nix.packages.${system}.nix;
}; };
hydra = nixpkgs.legacyPackages.${system}.callPackage ./package.nix { hydra = nixpkgs.legacyPackages.${system}.callPackage ./package.nix {
inherit (nixpkgs.lib) fileset; inherit (nixpkgs.lib) fileset;
inherit nixComponents;
inherit (self.packages.${system}) nix-eval-jobs; inherit (self.packages.${system}) nix-eval-jobs;
rawSrc = self; rawSrc = self;
inherit (nix.packages.${system})
nix-util
nix-store
nix-main
nix-cli
;
nix-perl-bindings = nix.hydraJobs.perlBindings.${system};
}; };
default = self.packages.${system}.hydra; default = self.packages.${system}.hydra;
}); });

View File

@ -12,6 +12,20 @@ nix_util_dep = dependency('nix-util', required: true)
nix_store_dep = dependency('nix-store', required: true) nix_store_dep = dependency('nix-store', required: true)
nix_main_dep = dependency('nix-main', required: true) nix_main_dep = dependency('nix-main', required: true)
# Nix need extra flags not provided in its pkg-config files.
nix_dep = declare_dependency(
dependencies: [
nix_util_dep,
nix_store_dep,
nix_main_dep,
],
compile_args: [
'-include', 'nix/config-util.hh',
'-include', 'nix/config-store.hh',
'-include', 'nix/config-main.hh',
],
)
pqxx_dep = dependency('libpqxx', required: true) pqxx_dep = dependency('libpqxx', required: true)
prom_cpp_core_dep = dependency('prometheus-cpp-core', required: true) prom_cpp_core_dep = dependency('prometheus-cpp-core', required: true)

View File

@ -27,7 +27,8 @@ in
{ {
install = forEachSystem (system: install = forEachSystem (system:
(import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).simpleTest { with import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; };
simpleTest {
name = "hydra-install"; name = "hydra-install";
nodes.machine = hydraServer; nodes.machine = hydraServer;
testScript = testScript =
@ -42,7 +43,8 @@ in
}); });
notifications = forEachSystem (system: notifications = forEachSystem (system:
(import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).simpleTest { with import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; };
simpleTest {
name = "hydra-notifications"; name = "hydra-notifications";
nodes.machine = { nodes.machine = {
imports = [ hydraServer ]; imports = [ hydraServer ];
@ -54,7 +56,7 @@ in
''; '';
services.influxdb.enable = true; services.influxdb.enable = true;
}; };
testScript = { nodes, ... }: '' testScript = ''
machine.wait_for_job("hydra-init") machine.wait_for_job("hydra-init")
# Create an admin account and some other state. # Create an admin account and some other state.
@ -85,7 +87,7 @@ in
# Setup the project and jobset # Setup the project and jobset
machine.succeed( machine.succeed(
"su - hydra -c 'perl -I ${nodes.machine.services.hydra-dev.package.perlDeps}/lib/perl5/site_perl ${./t/setup-notifications-jobset.pl}' >&2" "su - hydra -c 'perl -I ${config.services.hydra-dev.package.perlDeps}/lib/perl5/site_perl ${./t/setup-notifications-jobset.pl}' >&2"
) )
# Wait until hydra has build the job and # Wait until hydra has build the job and
@ -99,10 +101,9 @@ in
}); });
gitea = forEachSystem (system: gitea = forEachSystem (system:
let let pkgs = nixpkgs.legacyPackages.${system}; in
pkgs = nixpkgs.legacyPackages.${system}; with import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; };
in makeTest {
(import (nixpkgs + "/nixos/lib/testing-python.nix") { inherit system; }).makeTest {
name = "hydra-gitea"; name = "hydra-gitea";
nodes.machine = { pkgs, ... }: { nodes.machine = { pkgs, ... }: {
imports = [ hydraServer ]; imports = [ hydraServer ];

View File

@ -8,7 +8,11 @@
, perlPackages , perlPackages
, nixComponents , nix-util
, nix-store
, nix-main
, nix-cli
, nix-perl-bindings
, git , git
, makeWrapper , makeWrapper
@ -61,7 +65,7 @@ let
name = "hydra-perl-deps"; name = "hydra-perl-deps";
paths = lib.closePropagation paths = lib.closePropagation
([ ([
nixComponents.nix-perl-bindings nix-perl-bindings
git git
] ++ (with perlPackages; [ ] ++ (with perlPackages; [
AuthenSASL AuthenSASL
@ -89,7 +93,6 @@ let
DateTime DateTime
DBDPg DBDPg
DBDSQLite DBDSQLite
DBIxClassHelpers
DigestSHA1 DigestSHA1
EmailMIME EmailMIME
EmailSender EmailSender
@ -162,7 +165,7 @@ stdenv.mkDerivation (finalAttrs: {
nukeReferences nukeReferences
pkg-config pkg-config
mdbook mdbook
nixComponents.nix-cli nix-cli
perlDeps perlDeps
perl perl
unzip unzip
@ -172,9 +175,9 @@ stdenv.mkDerivation (finalAttrs: {
libpqxx libpqxx
openssl openssl
libxslt libxslt
nixComponents.nix-util nix-util
nixComponents.nix-store nix-store
nixComponents.nix-main nix-main
perlDeps perlDeps
perl perl
boost boost
@ -201,14 +204,14 @@ stdenv.mkDerivation (finalAttrs: {
glibcLocales glibcLocales
libressl.nc libressl.nc
python3 python3
nixComponents.nix-cli nix-cli
]; ];
hydraPath = lib.makeBinPath ( hydraPath = lib.makeBinPath (
[ [
subversion subversion
openssh openssh
nixComponents.nix-cli nix-cli
coreutils coreutils
findutils findutils
pixz pixz
@ -269,7 +272,7 @@ stdenv.mkDerivation (finalAttrs: {
--prefix PATH ':' $out/bin:$hydraPath \ --prefix PATH ':' $out/bin:$hydraPath \
--set HYDRA_RELEASE ${version} \ --set HYDRA_RELEASE ${version} \
--set HYDRA_HOME $out/libexec/hydra \ --set HYDRA_HOME $out/libexec/hydra \
--set NIX_RELEASE ${nixComponents.nix-cli.name or "unknown"} \ --set NIX_RELEASE ${nix-cli.name or "unknown"} \
--set NIX_EVAL_JOBS_RELEASE ${nix-eval-jobs.name or "unknown"} --set NIX_EVAL_JOBS_RELEASE ${nix-eval-jobs.name or "unknown"}
done done
''; '';

View File

@ -1,8 +1,8 @@
#include "db.hh" #include "db.hh"
#include "hydra-config.hh" #include "hydra-config.hh"
#include <nix/util/pool.hh> #include "pool.hh"
#include <nix/main/shared.hh> #include "shared.hh"
#include <nix/util/signals.hh> #include "signals.hh"
#include <algorithm> #include <algorithm>
#include <thread> #include <thread>

View File

@ -2,8 +2,7 @@ hydra_evaluator = executable('hydra-evaluator',
'hydra-evaluator.cc', 'hydra-evaluator.cc',
dependencies: [ dependencies: [
libhydra_dep, libhydra_dep,
nix_util_dep, nix_dep,
nix_main_dep,
pqxx_dep, pqxx_dep,
], ],
install: true, install: true,

View File

@ -5,20 +5,20 @@
#include <sys/stat.h> #include <sys/stat.h>
#include <fcntl.h> #include <fcntl.h>
#include <nix/store/build-result.hh> #include "build-result.hh"
#include <nix/store/path.hh> #include "path.hh"
#include <nix/store/legacy-ssh-store.hh> #include "legacy-ssh-store.hh"
#include <nix/store/serve-protocol.hh> #include "serve-protocol.hh"
#include <nix/store/serve-protocol-impl.hh> #include "serve-protocol-impl.hh"
#include "state.hh" #include "state.hh"
#include <nix/util/current-process.hh> #include "current-process.hh"
#include <nix/util/processes.hh> #include "processes.hh"
#include <nix/util/util.hh> #include "util.hh"
#include <nix/store/serve-protocol.hh> #include "serve-protocol.hh"
#include <nix/store/serve-protocol-impl.hh> #include "serve-protocol-impl.hh"
#include <nix/store/ssh.hh> #include "ssh.hh"
#include <nix/util/finally.hh> #include "finally.hh"
#include <nix/util/url.hh> #include "url.hh"
using namespace nix; using namespace nix;
@ -386,19 +386,8 @@ void RemoteResult::updateWithBuildResult(const nix::BuildResult & buildResult)
} }
/* Utility guard object to auto-release a semaphore on destruction. */
template <typename T>
class SemaphoreReleaser {
public:
SemaphoreReleaser(T* s) : sem(s) {}
~SemaphoreReleaser() { sem->release(); }
private:
T* sem;
};
void State::buildRemote(ref<Store> destStore, void State::buildRemote(ref<Store> destStore,
std::unique_ptr<MachineReservation> reservation,
::Machine::ptr machine, Step::ptr step, ::Machine::ptr machine, Step::ptr step,
const ServeProto::BuildOptions & buildOptions, const ServeProto::BuildOptions & buildOptions,
RemoteResult & result, std::shared_ptr<ActiveStep> activeStep, RemoteResult & result, std::shared_ptr<ActiveStep> activeStep,
@ -538,23 +527,6 @@ void State::buildRemote(ref<Store> destStore,
result.logFile = ""; result.logFile = "";
} }
/* Throttle CPU-bound work. Opportunistically skip updating the current
* step, since this requires a DB roundtrip. */
if (!localWorkThrottler.try_acquire()) {
MaintainCount<counter> mc(nrStepsWaitingForDownloadSlot);
updateStep(ssWaitingForLocalSlot);
localWorkThrottler.acquire();
}
SemaphoreReleaser releaser(&localWorkThrottler);
/* Once we've started copying outputs, release the machine reservation
* so further builds can happen. We do not release the machine earlier
* to avoid situations where the queue runner is bottlenecked on
* copying outputs and we end up building too many things that we
* haven't been able to allow copy slots for. */
reservation.reset();
wakeDispatcher();
StorePathSet outputs; StorePathSet outputs;
for (auto & [_, realisation] : buildResult.builtOutputs) for (auto & [_, realisation] : buildResult.builtOutputs)
outputs.insert(realisation.outPath); outputs.insert(realisation.outPath);

View File

@ -1,7 +1,7 @@
#include "hydra-build-result.hh" #include "hydra-build-result.hh"
#include <nix/store/store-api.hh> #include "store-api.hh"
#include <nix/util/util.hh> #include "util.hh"
#include <nix/util/source-accessor.hh> #include "source-accessor.hh"
#include <regex> #include <regex>

View File

@ -2,8 +2,8 @@
#include "state.hh" #include "state.hh"
#include "hydra-build-result.hh" #include "hydra-build-result.hh"
#include <nix/util/finally.hh> #include "finally.hh"
#include <nix/store/binary-cache-store.hh> #include "binary-cache-store.hh"
using namespace nix; using namespace nix;
@ -16,7 +16,7 @@ void setThreadName(const std::string & name)
} }
void State::builder(std::unique_ptr<MachineReservation> reservation) void State::builder(MachineReservation::ptr reservation)
{ {
setThreadName("bld~" + std::string(reservation->step->drvPath.to_string())); setThreadName("bld~" + std::string(reservation->step->drvPath.to_string()));
@ -35,20 +35,22 @@ void State::builder(std::unique_ptr<MachineReservation> reservation)
activeSteps_.lock()->erase(activeStep); activeSteps_.lock()->erase(activeStep);
}); });
std::string machine = reservation->machine->storeUri.render();
try { try {
auto destStore = getDestStore(); auto destStore = getDestStore();
// Might release the reservation. res = doBuildStep(destStore, reservation, activeStep);
res = doBuildStep(destStore, std::move(reservation), activeStep);
} catch (std::exception & e) { } catch (std::exception & e) {
printMsg(lvlError, "uncaught exception building %s on %s: %s", printMsg(lvlError, "uncaught exception building %s on %s: %s",
localStore->printStorePath(activeStep->step->drvPath), localStore->printStorePath(reservation->step->drvPath),
machine, reservation->machine->storeUri.render(),
e.what()); e.what());
} }
} }
/* Release the machine and wake up the dispatcher. */
assert(reservation.unique());
reservation = 0;
wakeDispatcher();
/* If there was a temporary failure, retry the step after an /* If there was a temporary failure, retry the step after an
exponentially increasing interval. */ exponentially increasing interval. */
Step::ptr step = wstep.lock(); Step::ptr step = wstep.lock();
@ -70,11 +72,11 @@ void State::builder(std::unique_ptr<MachineReservation> reservation)
State::StepResult State::doBuildStep(nix::ref<Store> destStore, State::StepResult State::doBuildStep(nix::ref<Store> destStore,
std::unique_ptr<MachineReservation> reservation, MachineReservation::ptr reservation,
std::shared_ptr<ActiveStep> activeStep) std::shared_ptr<ActiveStep> activeStep)
{ {
auto step(reservation->step); auto & step(reservation->step);
auto machine(reservation->machine); auto & machine(reservation->machine);
{ {
auto step_(step->state.lock()); auto step_(step->state.lock());
@ -209,7 +211,7 @@ State::StepResult State::doBuildStep(nix::ref<Store> destStore,
try { try {
/* FIXME: referring builds may have conflicting timeouts. */ /* FIXME: referring builds may have conflicting timeouts. */
buildRemote(destStore, std::move(reservation), machine, step, buildOptions, result, activeStep, updateStep, narMembers); buildRemote(destStore, machine, step, buildOptions, result, activeStep, updateStep, narMembers);
} catch (Error & e) { } catch (Error & e) {
if (activeStep->state_.lock()->cancelled) { if (activeStep->state_.lock()->cancelled) {
printInfo("marking step %d of build %d as cancelled", stepNr, buildId); printInfo("marking step %d of build %d as cancelled", stepNr, buildId);

View File

@ -40,15 +40,13 @@ void State::dispatcher()
printMsg(lvlDebug, "dispatcher woken up"); printMsg(lvlDebug, "dispatcher woken up");
nrDispatcherWakeups++; nrDispatcherWakeups++;
auto t_before_work = std::chrono::steady_clock::now(); auto now1 = std::chrono::steady_clock::now();
auto sleepUntil = doDispatch(); auto sleepUntil = doDispatch();
auto t_after_work = std::chrono::steady_clock::now(); auto now2 = std::chrono::steady_clock::now();
prom.dispatcher_time_spent_running.Increment( dispatchTimeMs += std::chrono::duration_cast<std::chrono::milliseconds>(now2 - now1).count();
std::chrono::duration_cast<std::chrono::microseconds>(t_after_work - t_before_work).count());
dispatchTimeMs += std::chrono::duration_cast<std::chrono::milliseconds>(t_after_work - t_before_work).count();
/* Sleep until we're woken up (either because a runnable build /* Sleep until we're woken up (either because a runnable build
is added, or because a build finishes). */ is added, or because a build finishes). */
@ -62,10 +60,6 @@ void State::dispatcher()
*dispatcherWakeup_ = false; *dispatcherWakeup_ = false;
} }
auto t_after_sleep = std::chrono::steady_clock::now();
prom.dispatcher_time_spent_waiting.Increment(
std::chrono::duration_cast<std::chrono::microseconds>(t_after_sleep - t_after_work).count());
} catch (std::exception & e) { } catch (std::exception & e) {
printError("dispatcher: %s", e.what()); printError("dispatcher: %s", e.what());
sleep(1); sleep(1);
@ -288,7 +282,7 @@ system_time State::doDispatch()
/* Make a slot reservation and start a thread to /* Make a slot reservation and start a thread to
do the build. */ do the build. */
auto builderThread = std::thread(&State::builder, this, auto builderThread = std::thread(&State::builder, this,
std::make_unique<MachineReservation>(*this, step, mi.machine)); std::make_shared<MachineReservation>(*this, step, mi.machine));
builderThread.detach(); // FIXME? builderThread.detach(); // FIXME?
keepGoing = true; keepGoing = true;

View File

@ -2,9 +2,9 @@
#include <memory> #include <memory>
#include <nix/util/hash.hh> #include "hash.hh"
#include <nix/store/derivations.hh> #include "derivations.hh"
#include <nix/store/store-api.hh> #include "store-api.hh"
#include "nar-extractor.hh" #include "nar-extractor.hh"
struct BuildProduct struct BuildProduct

View File

@ -11,16 +11,16 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <nix/util/signals.hh> #include "signals.hh"
#include "state.hh" #include "state.hh"
#include "hydra-build-result.hh" #include "hydra-build-result.hh"
#include <nix/store/store-api.hh> #include "store-api.hh"
#include <nix/store/remote-store.hh> #include "remote-store.hh"
#include <nix/store/globals.hh> #include "globals.hh"
#include "hydra-config.hh" #include "hydra-config.hh"
#include <nix/store/s3-binary-cache-store.hh> #include "s3-binary-cache-store.hh"
#include <nix/main/shared.hh> #include "shared.hh"
using namespace nix; using namespace nix;
using nlohmann::json; using nlohmann::json;
@ -70,31 +70,10 @@ State::PromMetrics::PromMetrics()
.Register(*registry) .Register(*registry)
.Add({}) .Add({})
) )
, dispatcher_time_spent_running( , queue_max_id(
prometheus::BuildCounter() prometheus::BuildGauge()
.Name("hydraqueuerunner_dispatcher_time_spent_running") .Name("hydraqueuerunner_queue_max_build_id_info")
.Help("Time (in micros) spent running the dispatcher") .Help("Maximum build record ID in the queue")
.Register(*registry)
.Add({})
)
, dispatcher_time_spent_waiting(
prometheus::BuildCounter()
.Name("hydraqueuerunner_dispatcher_time_spent_waiting")
.Help("Time (in micros) spent waiting for the dispatcher to obtain work")
.Register(*registry)
.Add({})
)
, queue_monitor_time_spent_running(
prometheus::BuildCounter()
.Name("hydraqueuerunner_queue_monitor_time_spent_running")
.Help("Time (in micros) spent running the queue monitor")
.Register(*registry)
.Add({})
)
, queue_monitor_time_spent_waiting(
prometheus::BuildCounter()
.Name("hydraqueuerunner_queue_monitor_time_spent_waiting")
.Help("Time (in micros) spent waiting for the queue monitor to obtain work")
.Register(*registry) .Register(*registry)
.Add({}) .Add({})
) )
@ -106,7 +85,6 @@ State::State(std::optional<std::string> metricsAddrOpt)
: config(std::make_unique<HydraConfig>()) : config(std::make_unique<HydraConfig>())
, maxUnsupportedTime(config->getIntOption("max_unsupported_time", 0)) , maxUnsupportedTime(config->getIntOption("max_unsupported_time", 0))
, dbPool(config->getIntOption("max_db_connections", 128)) , dbPool(config->getIntOption("max_db_connections", 128))
, localWorkThrottler(config->getIntOption("max_local_worker_threads", std::min(maxSupportedLocalWorkers, std::max(4u, std::thread::hardware_concurrency()) - 2)))
, maxOutputSize(config->getIntOption("max_output_size", 2ULL << 30)) , maxOutputSize(config->getIntOption("max_output_size", 2ULL << 30))
, maxLogSize(config->getIntOption("max_log_size", 64ULL << 20)) , maxLogSize(config->getIntOption("max_log_size", 64ULL << 20))
, uploadLogsToBinaryCache(config->getBoolOption("upload_logs_to_binary_cache", false)) , uploadLogsToBinaryCache(config->getBoolOption("upload_logs_to_binary_cache", false))
@ -573,7 +551,6 @@ void State::dumpStatus(Connection & conn)
{"nrActiveSteps", activeSteps_.lock()->size()}, {"nrActiveSteps", activeSteps_.lock()->size()},
{"nrStepsBuilding", nrStepsBuilding.load()}, {"nrStepsBuilding", nrStepsBuilding.load()},
{"nrStepsCopyingTo", nrStepsCopyingTo.load()}, {"nrStepsCopyingTo", nrStepsCopyingTo.load()},
{"nrStepsWaitingForDownloadSlot", nrStepsWaitingForDownloadSlot.load()},
{"nrStepsCopyingFrom", nrStepsCopyingFrom.load()}, {"nrStepsCopyingFrom", nrStepsCopyingFrom.load()},
{"nrStepsWaiting", nrStepsWaiting.load()}, {"nrStepsWaiting", nrStepsWaiting.load()},
{"nrUnsupportedSteps", nrUnsupportedSteps.load()}, {"nrUnsupportedSteps", nrUnsupportedSteps.load()},
@ -615,7 +592,6 @@ void State::dumpStatus(Connection & conn)
} }
{ {
auto machines_json = json::object();
auto machines_(machines.lock()); auto machines_(machines.lock());
for (auto & i : *machines_) { for (auto & i : *machines_) {
auto & m(i.second); auto & m(i.second);
@ -642,9 +618,8 @@ void State::dumpStatus(Connection & conn)
machine["avgStepTime"] = (float) s->totalStepTime / s->nrStepsDone; machine["avgStepTime"] = (float) s->totalStepTime / s->nrStepsDone;
machine["avgStepBuildTime"] = (float) s->totalStepBuildTime / s->nrStepsDone; machine["avgStepBuildTime"] = (float) s->totalStepBuildTime / s->nrStepsDone;
} }
machines_json[m->storeUri.render()] = machine; statusJson["machines"][m->storeUri.render()] = machine;
} }
statusJson["machines"] = machines_json;
} }
{ {

View File

@ -13,9 +13,7 @@ hydra_queue_runner = executable('hydra-queue-runner',
srcs, srcs,
dependencies: [ dependencies: [
libhydra_dep, libhydra_dep,
nix_util_dep, nix_dep,
nix_store_dep,
nix_main_dep,
pqxx_dep, pqxx_dep,
prom_cpp_core_dep, prom_cpp_core_dep,
prom_cpp_pull_dep, prom_cpp_pull_dep,

View File

@ -1,6 +1,6 @@
#include "nar-extractor.hh" #include "nar-extractor.hh"
#include <nix/util/archive.hh> #include "archive.hh"
#include <unordered_set> #include <unordered_set>

View File

@ -1,9 +1,9 @@
#pragma once #pragma once
#include <nix/util/source-accessor.hh> #include "source-accessor.hh"
#include <nix/util/types.hh> #include "types.hh"
#include <nix/util/serialise.hh> #include "serialise.hh"
#include <nix/util/hash.hh> #include "hash.hh"
struct NarMemberData struct NarMemberData
{ {

View File

@ -1,8 +1,6 @@
#include "state.hh" #include "state.hh"
#include "hydra-build-result.hh" #include "hydra-build-result.hh"
#include <nix/store/globals.hh> #include "globals.hh"
#include <nix/store/parsed-derivations.hh>
#include <nix/util/thread-pool.hh>
#include <cstring> #include <cstring>
@ -39,21 +37,16 @@ void State::queueMonitorLoop(Connection & conn)
auto destStore = getDestStore(); auto destStore = getDestStore();
unsigned int lastBuildId = 0;
bool quit = false; bool quit = false;
while (!quit) { while (!quit) {
auto t_before_work = std::chrono::steady_clock::now();
localStore->clearPathInfoCache(); localStore->clearPathInfoCache();
bool done = getQueuedBuilds(conn, destStore); bool done = getQueuedBuilds(conn, destStore, lastBuildId);
if (buildOne && buildOneDone) quit = true; if (buildOne && buildOneDone) quit = true;
auto t_after_work = std::chrono::steady_clock::now();
prom.queue_monitor_time_spent_running.Increment(
std::chrono::duration_cast<std::chrono::microseconds>(t_after_work - t_before_work).count());
/* Sleep until we get notification from the database about an /* Sleep until we get notification from the database about an
event. */ event. */
if (done && !quit) { if (done && !quit) {
@ -63,10 +56,12 @@ void State::queueMonitorLoop(Connection & conn)
conn.get_notifs(); conn.get_notifs();
if (auto lowestId = buildsAdded.get()) { if (auto lowestId = buildsAdded.get()) {
lastBuildId = std::min(lastBuildId, static_cast<unsigned>(std::stoul(*lowestId) - 1));
printMsg(lvlTalkative, "got notification: new builds added to the queue"); printMsg(lvlTalkative, "got notification: new builds added to the queue");
} }
if (buildsRestarted.get()) { if (buildsRestarted.get()) {
printMsg(lvlTalkative, "got notification: builds restarted"); printMsg(lvlTalkative, "got notification: builds restarted");
lastBuildId = 0; // check all builds
} }
if (buildsCancelled.get() || buildsDeleted.get() || buildsBumped.get()) { if (buildsCancelled.get() || buildsDeleted.get() || buildsBumped.get()) {
printMsg(lvlTalkative, "got notification: builds cancelled or bumped"); printMsg(lvlTalkative, "got notification: builds cancelled or bumped");
@ -76,10 +71,6 @@ void State::queueMonitorLoop(Connection & conn)
printMsg(lvlTalkative, "got notification: jobset shares changed"); printMsg(lvlTalkative, "got notification: jobset shares changed");
processJobsetSharesChange(conn); processJobsetSharesChange(conn);
} }
auto t_after_sleep = std::chrono::steady_clock::now();
prom.queue_monitor_time_spent_waiting.Increment(
std::chrono::duration_cast<std::chrono::microseconds>(t_after_sleep - t_after_work).count());
} }
exit(0); exit(0);
@ -93,18 +84,20 @@ struct PreviousFailure : public std::exception {
bool State::getQueuedBuilds(Connection & conn, bool State::getQueuedBuilds(Connection & conn,
ref<Store> destStore) ref<Store> destStore, unsigned int & lastBuildId)
{ {
prom.queue_checks_started.Increment(); prom.queue_checks_started.Increment();
printInfo("checking the queue for builds..."); printInfo("checking the queue for builds > %d...", lastBuildId);
/* Grab the queued builds from the database, but don't process /* Grab the queued builds from the database, but don't process
them yet (since we don't want a long-running transaction). */ them yet (since we don't want a long-running transaction). */
std::vector<BuildID> newIDs; std::vector<BuildID> newIDs;
std::unordered_map<BuildID, Build::ptr> newBuildsByID; std::map<BuildID, Build::ptr> newBuildsByID;
std::multimap<StorePath, BuildID> newBuildsByPath; std::multimap<StorePath, BuildID> newBuildsByPath;
unsigned int newLastBuildId = lastBuildId;
{ {
pqxx::work txn(conn); pqxx::work txn(conn);
@ -113,12 +106,17 @@ bool State::getQueuedBuilds(Connection & conn,
"jobsets.name as jobset, job, drvPath, maxsilent, timeout, timestamp, " "jobsets.name as jobset, job, drvPath, maxsilent, timeout, timestamp, "
"globalPriority, priority from Builds " "globalPriority, priority from Builds "
"inner join jobsets on builds.jobset_id = jobsets.id " "inner join jobsets on builds.jobset_id = jobsets.id "
"where finished = 0 order by globalPriority desc, random()"); "where builds.id > $1 and finished = 0 order by globalPriority desc, builds.id",
lastBuildId);
for (auto const & row : res) { for (auto const & row : res) {
auto builds_(builds.lock()); auto builds_(builds.lock());
BuildID id = row["id"].as<BuildID>(); BuildID id = row["id"].as<BuildID>();
if (buildOne && id != buildOne) continue; if (buildOne && id != buildOne) continue;
if (id > newLastBuildId) {
newLastBuildId = id;
prom.queue_max_id.Set(id);
}
if (builds_->count(id)) continue; if (builds_->count(id)) continue;
auto build = std::make_shared<Build>( auto build = std::make_shared<Build>(
@ -320,13 +318,15 @@ bool State::getQueuedBuilds(Connection & conn,
/* Stop after a certain time to allow priority bumps to be /* Stop after a certain time to allow priority bumps to be
processed. */ processed. */
if (std::chrono::system_clock::now() > start + std::chrono::seconds(60)) { if (std::chrono::system_clock::now() > start + std::chrono::seconds(600)) {
prom.queue_checks_early_exits.Increment(); prom.queue_checks_early_exits.Increment();
break; break;
} }
} }
prom.queue_checks_finished.Increment(); prom.queue_checks_finished.Increment();
lastBuildId = newBuildsByID.empty() ? newLastBuildId : newBuildsByID.begin()->first - 1;
return newBuildsByID.empty(); return newBuildsByID.empty();
} }
@ -405,34 +405,6 @@ void State::processQueueChange(Connection & conn)
} }
std::map<DrvOutput, std::optional<StorePath>> State::getMissingRemotePaths(
ref<Store> destStore,
const std::map<DrvOutput, std::optional<StorePath>> & paths)
{
Sync<std::map<DrvOutput, std::optional<StorePath>>> missing_;
ThreadPool tp;
for (auto & [output, maybeOutputPath] : paths) {
if (!maybeOutputPath) {
auto missing(missing_.lock());
missing->insert({output, maybeOutputPath});
} else {
tp.enqueue([&] {
if (!destStore->isValidPath(*maybeOutputPath)) {
auto missing(missing_.lock());
missing->insert({output, maybeOutputPath});
}
});
}
}
tp.process();
auto missing(missing_.lock());
return *missing;
}
Step::ptr State::createStep(ref<Store> destStore, Step::ptr State::createStep(ref<Store> destStore,
Connection & conn, Build::ptr build, const StorePath & drvPath, Connection & conn, Build::ptr build, const StorePath & drvPath,
Build::ptr referringBuild, Step::ptr referringStep, std::set<StorePath> & finishedDrvs, Build::ptr referringBuild, Step::ptr referringStep, std::set<StorePath> & finishedDrvs,
@ -491,17 +463,14 @@ Step::ptr State::createStep(ref<Store> destStore,
it's not runnable yet, and other threads won't make it it's not runnable yet, and other threads won't make it
runnable while step->created == false. */ runnable while step->created == false. */
step->drv = std::make_unique<Derivation>(localStore->readDerivation(drvPath)); step->drv = std::make_unique<Derivation>(localStore->readDerivation(drvPath));
{ step->parsedDrv = std::make_unique<ParsedDerivation>(drvPath, *step->drv);
auto parsedDrv = ParsedDerivation{drvPath, *step->drv};
step->drvOptions = std::make_unique<DerivationOptions>(DerivationOptions::fromParsedDerivation(parsedDrv));
}
step->preferLocalBuild = step->drvOptions->willBuildLocally(*localStore, *step->drv); step->preferLocalBuild = step->parsedDrv->willBuildLocally(*localStore);
step->isDeterministic = getOr(step->drv->env, "isDetermistic", "0") == "1"; step->isDeterministic = getOr(step->drv->env, "isDetermistic", "0") == "1";
step->systemType = step->drv->platform; step->systemType = step->drv->platform;
{ {
StringSet features = step->requiredSystemFeatures = step->drvOptions->getRequiredSystemFeatures(*step->drv); StringSet features = step->requiredSystemFeatures = step->parsedDrv->getRequiredSystemFeatures();
if (step->preferLocalBuild) if (step->preferLocalBuild)
features.insert("local"); features.insert("local");
if (!features.empty()) { if (!features.empty()) {
@ -516,15 +485,16 @@ Step::ptr State::createStep(ref<Store> destStore,
/* Are all outputs valid? */ /* Are all outputs valid? */
auto outputHashes = staticOutputHashes(*localStore, *(step->drv)); auto outputHashes = staticOutputHashes(*localStore, *(step->drv));
std::map<DrvOutput, std::optional<StorePath>> paths; bool valid = true;
std::map<DrvOutput, std::optional<StorePath>> missing;
for (auto & [outputName, maybeOutputPath] : destStore->queryPartialDerivationOutputMap(drvPath, &*localStore)) { for (auto & [outputName, maybeOutputPath] : destStore->queryPartialDerivationOutputMap(drvPath, &*localStore)) {
auto outputHash = outputHashes.at(outputName); auto outputHash = outputHashes.at(outputName);
paths.insert({{outputHash, outputName}, maybeOutputPath}); if (maybeOutputPath && destStore->isValidPath(*maybeOutputPath))
continue;
valid = false;
missing.insert({{outputHash, outputName}, maybeOutputPath});
} }
auto missing = getMissingRemotePaths(destStore, paths);
bool valid = missing.empty();
/* Try to copy the missing paths from the local store or from /* Try to copy the missing paths from the local store or from
substitutes. */ substitutes. */
if (!missing.empty()) { if (!missing.empty()) {

View File

@ -6,8 +6,6 @@
#include <map> #include <map>
#include <memory> #include <memory>
#include <queue> #include <queue>
#include <regex>
#include <semaphore>
#include <prometheus/counter.h> #include <prometheus/counter.h>
#include <prometheus/gauge.h> #include <prometheus/gauge.h>
@ -15,18 +13,17 @@
#include "db.hh" #include "db.hh"
#include <nix/store/derivations.hh> #include "parsed-derivations.hh"
#include <nix/store/derivation-options.hh> #include "pathlocks.hh"
#include <nix/store/pathlocks.hh> #include "pool.hh"
#include <nix/util/pool.hh> #include "build-result.hh"
#include <nix/store/build-result.hh> #include "store-api.hh"
#include <nix/store/store-api.hh> #include "sync.hh"
#include <nix/util/sync.hh>
#include "nar-extractor.hh" #include "nar-extractor.hh"
#include <nix/store/serve-protocol.hh> #include "serve-protocol.hh"
#include <nix/store/serve-protocol-impl.hh> #include "serve-protocol-impl.hh"
#include <nix/store/serve-protocol-connection.hh> #include "serve-protocol-connection.hh"
#include <nix/store/machines.hh> #include "machines.hh"
typedef unsigned int BuildID; typedef unsigned int BuildID;
@ -60,7 +57,6 @@ typedef enum {
ssConnecting = 10, ssConnecting = 10,
ssSendingInputs = 20, ssSendingInputs = 20,
ssBuilding = 30, ssBuilding = 30,
ssWaitingForLocalSlot = 35,
ssReceivingOutputs = 40, ssReceivingOutputs = 40,
ssPostProcessing = 50, ssPostProcessing = 50,
} StepState; } StepState;
@ -171,7 +167,7 @@ struct Step
nix::StorePath drvPath; nix::StorePath drvPath;
std::unique_ptr<nix::Derivation> drv; std::unique_ptr<nix::Derivation> drv;
std::unique_ptr<nix::DerivationOptions> drvOptions; std::unique_ptr<nix::ParsedDerivation> parsedDrv;
std::set<std::string> requiredSystemFeatures; std::set<std::string> requiredSystemFeatures;
bool preferLocalBuild; bool preferLocalBuild;
bool isDeterministic; bool isDeterministic;
@ -356,10 +352,6 @@ private:
typedef std::map<nix::StoreReference::Variant, Machine::ptr> Machines; typedef std::map<nix::StoreReference::Variant, Machine::ptr> Machines;
nix::Sync<Machines> machines; // FIXME: use atomic_shared_ptr nix::Sync<Machines> machines; // FIXME: use atomic_shared_ptr
/* Throttler for CPU-bound local work. */
static constexpr unsigned int maxSupportedLocalWorkers = 1024;
std::counting_semaphore<maxSupportedLocalWorkers> localWorkThrottler;
/* Various stats. */ /* Various stats. */
time_t startedAt; time_t startedAt;
counter nrBuildsRead{0}; counter nrBuildsRead{0};
@ -369,7 +361,6 @@ private:
counter nrStepsDone{0}; counter nrStepsDone{0};
counter nrStepsBuilding{0}; counter nrStepsBuilding{0};
counter nrStepsCopyingTo{0}; counter nrStepsCopyingTo{0};
counter nrStepsWaitingForDownloadSlot{0};
counter nrStepsCopyingFrom{0}; counter nrStepsCopyingFrom{0};
counter nrStepsWaiting{0}; counter nrStepsWaiting{0};
counter nrUnsupportedSteps{0}; counter nrUnsupportedSteps{0};
@ -400,6 +391,7 @@ private:
struct MachineReservation struct MachineReservation
{ {
typedef std::shared_ptr<MachineReservation> ptr;
State & state; State & state;
Step::ptr step; Step::ptr step;
Machine::ptr machine; Machine::ptr machine;
@ -457,12 +449,7 @@ private:
prometheus::Counter& queue_steps_created; prometheus::Counter& queue_steps_created;
prometheus::Counter& queue_checks_early_exits; prometheus::Counter& queue_checks_early_exits;
prometheus::Counter& queue_checks_finished; prometheus::Counter& queue_checks_finished;
prometheus::Gauge& queue_max_id;
prometheus::Counter& dispatcher_time_spent_running;
prometheus::Counter& dispatcher_time_spent_waiting;
prometheus::Counter& queue_monitor_time_spent_running;
prometheus::Counter& queue_monitor_time_spent_waiting;
PromMetrics(); PromMetrics();
}; };
@ -506,7 +493,8 @@ private:
void queueMonitorLoop(Connection & conn); void queueMonitorLoop(Connection & conn);
/* Check the queue for new builds. */ /* Check the queue for new builds. */
bool getQueuedBuilds(Connection & conn, nix::ref<nix::Store> destStore); bool getQueuedBuilds(Connection & conn,
nix::ref<nix::Store> destStore, unsigned int & lastBuildId);
/* Handle cancellation, deletion and priority bumps. */ /* Handle cancellation, deletion and priority bumps. */
void processQueueChange(Connection & conn); void processQueueChange(Connection & conn);
@ -514,12 +502,6 @@ private:
BuildOutput getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore, BuildOutput getBuildOutputCached(Connection & conn, nix::ref<nix::Store> destStore,
const nix::StorePath & drvPath); const nix::StorePath & drvPath);
/* Returns paths missing from the remote store. Paths are processed in
* parallel to work around the possible latency of remote stores. */
std::map<nix::DrvOutput, std::optional<nix::StorePath>> getMissingRemotePaths(
nix::ref<nix::Store> destStore,
const std::map<nix::DrvOutput, std::optional<nix::StorePath>> & paths);
Step::ptr createStep(nix::ref<nix::Store> store, Step::ptr createStep(nix::ref<nix::Store> store,
Connection & conn, Build::ptr build, const nix::StorePath & drvPath, Connection & conn, Build::ptr build, const nix::StorePath & drvPath,
Build::ptr referringBuild, Step::ptr referringStep, std::set<nix::StorePath> & finishedDrvs, Build::ptr referringBuild, Step::ptr referringStep, std::set<nix::StorePath> & finishedDrvs,
@ -549,17 +531,16 @@ private:
void abortUnsupported(); void abortUnsupported();
void builder(std::unique_ptr<MachineReservation> reservation); void builder(MachineReservation::ptr reservation);
/* Perform the given build step. Return true if the step is to be /* Perform the given build step. Return true if the step is to be
retried. */ retried. */
enum StepResult { sDone, sRetry, sMaybeCancelled }; enum StepResult { sDone, sRetry, sMaybeCancelled };
StepResult doBuildStep(nix::ref<nix::Store> destStore, StepResult doBuildStep(nix::ref<nix::Store> destStore,
std::unique_ptr<MachineReservation> reservation, MachineReservation::ptr reservation,
std::shared_ptr<ActiveStep> activeStep); std::shared_ptr<ActiveStep> activeStep);
void buildRemote(nix::ref<nix::Store> destStore, void buildRemote(nix::ref<nix::Store> destStore,
std::unique_ptr<MachineReservation> reservation,
Machine::ptr machine, Step::ptr step, Machine::ptr machine, Step::ptr step,
const nix::ServeProto::BuildOptions & buildOptions, const nix::ServeProto::BuildOptions & buildOptions,
RemoteResult & result, std::shared_ptr<ActiveStep> activeStep, RemoteResult & result, std::shared_ptr<ActiveStep> activeStep,

View File

@ -238,7 +238,7 @@ sub serveFile {
# XSS hole. # XSS hole.
$c->response->header('Content-Security-Policy' => 'sandbox allow-scripts'); $c->response->header('Content-Security-Policy' => 'sandbox allow-scripts');
$c->stash->{'plain'} = { data => readIntoSocket(cmd => ["nix", "--experimental-features", "nix-command", $c->stash->{'plain'} = { data => grab(cmd => ["nix", "--experimental-features", "nix-command",
"store", "cat", "--store", getStoreUri(), "$path"]) }; "store", "cat", "--store", getStoreUri(), "$path"]) };
# Detect MIME type. # Detect MIME type.

View File

@ -364,21 +364,6 @@ sub evals_GET {
); );
} }
sub errors :Chained('jobsetChain') :PathPart('errors') :Args(0) :ActionClass('REST') { }
sub errors_GET {
my ($self, $c) = @_;
$c->stash->{template} = 'eval-error.tt';
my $jobsetName = $c->stash->{params}->{name};
$c->stash->{jobset} = $c->stash->{project}->jobsets->find(
{ name => $jobsetName },
{ '+columns' => { 'errormsg' => 'errormsg' } }
);
$self->status_ok($c, entity => $c->stash->{jobset});
}
# Redirect to the latest finished evaluation of this jobset. # Redirect to the latest finished evaluation of this jobset.
sub latest_eval : Chained('jobsetChain') PathPart('latest-eval') { sub latest_eval : Chained('jobsetChain') PathPart('latest-eval') {

View File

@ -86,17 +86,6 @@ sub view_GET {
); );
} }
sub errors :Chained('evalChain') :PathPart('errors') :Args(0) :ActionClass('REST') { }
sub errors_GET {
my ($self, $c) = @_;
$c->stash->{template} = 'eval-error.tt';
$c->stash->{eval} = $c->model('DB::JobsetEvals')->find($c->stash->{eval}->id, { prefetch => 'evaluationerror' });
$self->status_ok($c, entity => $c->stash->{eval});
}
sub create_jobset : Chained('evalChain') PathPart('create-jobset') Args(0) { sub create_jobset : Chained('evalChain') PathPart('create-jobset') Args(0) {
my ($self, $c) = @_; my ($self, $c) = @_;

View File

@ -162,7 +162,7 @@ sub status_GET {
{ "buildsteps.busy" => { '!=', 0 } }, { "buildsteps.busy" => { '!=', 0 } },
{ order_by => ["globalpriority DESC", "id"], { order_by => ["globalpriority DESC", "id"],
join => "buildsteps", join => "buildsteps",
columns => [@buildListColumns, 'buildsteps.drvpath', 'buildsteps.type'] columns => [@buildListColumns]
})] })]
); );
} }

View File

@ -37,16 +37,7 @@ sub buildDiff {
my $n = 0; my $n = 0;
foreach my $build (@{$builds}) { foreach my $build (@{$builds}) {
my $aborted = $build->finished != 0 && ( my $aborted = $build->finished != 0 && ($build->buildstatus == 3 || $build->buildstatus == 4);
# aborted
$build->buildstatus == 3
# cancelled
|| $build->buildstatus == 4
# timeout
|| $build->buildstatus == 7
# log limit exceeded
|| $build->buildstatus == 10
);
my $d; my $d;
my $found = 0; my $found = 0;
while ($n < scalar(@{$builds2})) { while ($n < scalar(@{$builds2})) {

View File

@ -36,7 +36,6 @@ our @EXPORT = qw(
jobsetOverview jobsetOverview
jobsetOverview_ jobsetOverview_
pathIsInsidePrefix pathIsInsidePrefix
readIntoSocket
readNixFile readNixFile
registerRoot registerRoot
restartBuilds restartBuilds
@ -297,7 +296,8 @@ sub getEvals {
my @evals = $evals_result_set->search( my @evals = $evals_result_set->search(
{ hasnewbuilds => 1 }, { hasnewbuilds => 1 },
{ order_by => "$me.id DESC", rows => $rows, offset => $offset }); { order_by => "$me.id DESC", rows => $rows, offset => $offset
, prefetch => { evaluationerror => [ ] } });
my @res = (); my @res = ();
my $cache = {}; my $cache = {};
@ -417,16 +417,6 @@ sub pathIsInsidePrefix {
return $cur; return $cur;
} }
sub readIntoSocket{
my (%args) = @_;
my $sock;
eval {
open($sock, "-|", @{$args{cmd}}) or die q(failed to open socket from command:\n $x);
};
return $sock;
}

View File

@ -105,6 +105,4 @@ __PACKAGE__->add_column(
"+id" => { retrieve_on_insert => 1 } "+id" => { retrieve_on_insert => 1 }
); );
__PACKAGE__->mk_group_accessors('column' => 'has_error');
1; 1;

View File

@ -386,8 +386,6 @@ __PACKAGE__->add_column(
"+id" => { retrieve_on_insert => 1 } "+id" => { retrieve_on_insert => 1 }
); );
__PACKAGE__->mk_group_accessors('column' => 'has_error');
sub supportsDynamicRunCommand { sub supportsDynamicRunCommand {
my ($self) = @_; my ($self) = @_;

View File

@ -1,30 +0,0 @@
package Hydra::Schema::ResultSet::EvaluationErrors;
use strict;
use utf8;
use warnings;
use parent 'DBIx::Class::ResultSet';
use Storable qw(dclone);
__PACKAGE__->load_components('Helper::ResultSet::RemoveColumns');
# Exclude expensive error message values unless explicitly requested, and
# replace them with a summary field describing their presence/absence.
sub search_rs {
my ( $class, $query, $attrs ) = @_;
if ($attrs) {
$attrs = dclone($attrs);
}
unless (exists $attrs->{'select'} || exists $attrs->{'columns'}) {
$attrs->{'+columns'}->{'has_error'} = "errormsg != ''";
}
unless (exists $attrs->{'+columns'}->{'errormsg'}) {
push @{ $attrs->{'remove_columns'} }, 'errormsg';
}
return $class->next::method($query, $attrs);
}

View File

@ -1,30 +0,0 @@
package Hydra::Schema::ResultSet::Jobsets;
use strict;
use utf8;
use warnings;
use parent 'DBIx::Class::ResultSet';
use Storable qw(dclone);
__PACKAGE__->load_components('Helper::ResultSet::RemoveColumns');
# Exclude expensive error message values unless explicitly requested, and
# replace them with a summary field describing their presence/absence.
sub search_rs {
my ( $class, $query, $attrs ) = @_;
if ($attrs) {
$attrs = dclone($attrs);
}
unless (exists $attrs->{'select'} || exists $attrs->{'columns'}) {
$attrs->{'+columns'}->{'has_error'} = "errormsg != ''";
}
unless (exists $attrs->{'+columns'}->{'errormsg'}) {
push @{ $attrs->{'remove_columns'} }, 'errormsg';
}
return $class->next::method($query, $attrs);
}

View File

@ -2,8 +2,8 @@
#include <pqxx/pqxx> #include <pqxx/pqxx>
#include <nix/util/environment-variables.hh> #include "environment-variables.hh"
#include <nix/util/util.hh> #include "util.hh"
struct Connection : pqxx::connection struct Connection : pqxx::connection

View File

@ -2,8 +2,8 @@
#include <map> #include <map>
#include <nix/util/file-system.hh> #include "file-system.hh"
#include <nix/util/util.hh> #include "util.hh"
struct HydraConfig struct HydraConfig
{ {

View File

@ -61,7 +61,21 @@ END;
<td>[% IF step.busy != 0 || ((step.machine || step.starttime) && (step.status == 0 || step.status == 1 || step.status == 3 || step.status == 4 || step.status == 7)); INCLUDE renderMachineName machine=step.machine; ELSE; "<em>n/a</em>"; END %]</td> <td>[% IF step.busy != 0 || ((step.machine || step.starttime) && (step.status == 0 || step.status == 1 || step.status == 3 || step.status == 4 || step.status == 7)); INCLUDE renderMachineName machine=step.machine; ELSE; "<em>n/a</em>"; END %]</td>
<td class="step-status"> <td class="step-status">
[% IF step.busy != 0 %] [% IF step.busy != 0 %]
[% INCLUDE renderBusyStatus %] [% IF step.busy == 1 %]
<strong>Preparing</strong>
[% ELSIF step.busy == 10 %]
<strong>Connecting</strong>
[% ELSIF step.busy == 20 %]
<strong>Sending inputs</strong>
[% ELSIF step.busy == 30 %]
<strong>Building</strong>
[% ELSIF step.busy == 40 %]
<strong>Receiving outputs</strong>
[% ELSIF step.busy == 50 %]
<strong>Post-processing</strong>
[% ELSE %]
<strong>Unknown state</strong>
[% END %]
[% ELSIF step.status == 0 %] [% ELSIF step.status == 0 %]
[% IF step.isnondeterministic %] [% IF step.isnondeterministic %]
<span class="warn">Succeeded with non-determistic result</span> <span class="warn">Succeeded with non-determistic result</span>

View File

@ -91,17 +91,6 @@ BLOCK renderDuration;
duration % 60 %]s[% duration % 60 %]s[%
END; END;
BLOCK renderDrvInfo;
drvname = step.drvpath
.substr(11) # strip `/nix/store/`
.split('-').slice(1).join("-") # strip hash part
.substr(0, -4); # strip `.drv`
IF drvname != releasename;
IF step.type == 0; action = "Build"; ELSE; action = "Substitution"; END;
IF drvname; %]<em> ([% action %] of [% drvname %])</em>[% END;
END;
END;
BLOCK renderBuildListHeader %] BLOCK renderBuildListHeader %]
<table class="table table-striped table-condensed clickable-rows"> <table class="table table-striped table-condensed clickable-rows">
@ -142,12 +131,7 @@ BLOCK renderBuildListBody;
[% END %] [% END %]
<td><a class="row-link" href="[% link %]">[% build.id %]</a></td> <td><a class="row-link" href="[% link %]">[% build.id %]</a></td>
[% IF !hideJobName %] [% IF !hideJobName %]
<td> <td><a href="[%link%]">[% IF !hideJobsetName %][%build.jobset.get_column("project")%]:[%build.jobset.get_column("name")%]:[% END %][%build.get_column("job")%]</td>
<a href="[%link%]">[% IF !hideJobsetName %][%build.jobset.get_column("project")%]:[%build.jobset.get_column("name")%]:[% END %][%build.get_column("job")%]</a>
[% IF showStepName %]
[% INCLUDE renderDrvInfo step=build.buildsteps releasename=build.nixname %]
[% END %]
</td>
[% END %] [% END %]
<td class="nowrap">[% t = showSchedulingInfo ? build.timestamp : build.stoptime; IF t; INCLUDE renderRelativeDate timestamp=(showSchedulingInfo ? build.timestamp : build.stoptime); ELSE; "-"; END %]</td> <td class="nowrap">[% t = showSchedulingInfo ? build.timestamp : build.stoptime; IF t; INCLUDE renderRelativeDate timestamp=(showSchedulingInfo ? build.timestamp : build.stoptime); ELSE; "-"; END %]</td>
<td>[% !showSchedulingInfo and build.get_column('releasename') ? build.get_column('releasename') : build.nixname %]</td> <td>[% !showSchedulingInfo and build.get_column('releasename') ? build.get_column('releasename') : build.nixname %]</td>
@ -261,27 +245,6 @@ BLOCK renderBuildStatusIcon;
END; END;
BLOCK renderBusyStatus;
IF step.busy == 1 %]
<strong>Preparing</strong>
[% ELSIF step.busy == 10 %]
<strong>Connecting</strong>
[% ELSIF step.busy == 20 %]
<strong>Sending inputs</strong>
[% ELSIF step.busy == 30 %]
<strong>Building</strong>
[% ELSIF step.busy == 35 %]
<strong>Waiting to receive outputs</strong>
[% ELSIF step.busy == 40 %]
<strong>Receiving outputs</strong>
[% ELSIF step.busy == 50 %]
<strong>Post-processing</strong>
[% ELSE %]
<strong>Unknown state</strong>
[% END;
END;
BLOCK renderStatus; BLOCK renderStatus;
IF build.finished; IF build.finished;
buildstatus = build.buildstatus; buildstatus = build.buildstatus;
@ -513,7 +476,7 @@ BLOCK renderEvals %]
ELSE %] ELSE %]
- -
[% END %] [% END %]
[% IF eval.evaluationerror.has_error %] [% IF eval.evaluationerror.errormsg %]
<span class="badge badge-warning">Eval Errors</span> <span class="badge badge-warning">Eval Errors</span>
[% END %] [% END %]
</td> </td>
@ -639,7 +602,7 @@ BLOCK renderJobsetOverview %]
<td>[% HTML.escape(j.description) %]</td> <td>[% HTML.escape(j.description) %]</td>
<td>[% IF j.lastcheckedtime; <td>[% IF j.lastcheckedtime;
INCLUDE renderDateTime timestamp = j.lastcheckedtime; INCLUDE renderDateTime timestamp = j.lastcheckedtime;
IF j.has_error || j.fetcherrormsg; %]&nbsp;<span class = 'badge badge-warning'>Error</span>[% END; IF j.errormsg || j.fetcherrormsg; %]&nbsp;<span class = 'badge badge-warning'>Error</span>[% END;
ELSE; "-"; ELSE; "-";
END %]</td> END %]</td>
[% IF j.get_column('nrtotal') > 0 %] [% IF j.get_column('nrtotal') > 0 %]

View File

@ -1,26 +0,0 @@
[% PROCESS common.tt %]
<!DOCTYPE html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
[% INCLUDE style.tt %]
</head>
<body>
<div class="tab-content tab-pane">
<div id="tabs-errors" class="">
[% IF jobset %]
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=(jobset.errortime || jobset.lastcheckedtime) %].</p>
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(jobset.fetcherrormsg || jobset.errormsg) %]</pre></div></div>
[% ELSIF eval %]
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=(eval.evaluationerror.errortime || eval.timestamp) %].</p>
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(eval.evaluationerror.errormsg) %]</pre></div></div>
[% END %]
</div>
</div>
</body>
</html>

View File

@ -65,7 +65,7 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
[% END %] [% END %]
[% IF aborted.size > 0 %] [% IF aborted.size > 0 %]
<li class="nav-item"><a class="nav-link" href="#tabs-aborted" data-toggle="tab"><span class="text-warning">Aborted / Timed out Jobs ([% aborted.size %])</span></a></li> <li class="nav-item"><a class="nav-link" href="#tabs-aborted" data-toggle="tab"><span class="text-warning">Aborted Jobs ([% aborted.size %])</span></a></li>
[% END %] [% END %]
[% IF nowFail.size > 0 %] [% IF nowFail.size > 0 %]
<li class="nav-item"><a class="nav-link" href="#tabs-now-fail" data-toggle="tab"><span class="text-warning">Newly Failing Jobs ([% nowFail.size %])</span></a></li> <li class="nav-item"><a class="nav-link" href="#tabs-now-fail" data-toggle="tab"><span class="text-warning">Newly Failing Jobs ([% nowFail.size %])</span></a></li>
@ -90,7 +90,7 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
[% END %] [% END %]
<li class="nav-item"><a class="nav-link" href="#tabs-inputs" data-toggle="tab">Inputs</a></li> <li class="nav-item"><a class="nav-link" href="#tabs-inputs" data-toggle="tab">Inputs</a></li>
[% IF eval.evaluationerror.has_error %] [% IF eval.evaluationerror.errormsg %]
<li class="nav-item"><a class="nav-link" href="#tabs-errors" data-toggle="tab"><span class="text-warning">Evaluation Errors</span></a></li> <li class="nav-item"><a class="nav-link" href="#tabs-errors" data-toggle="tab"><span class="text-warning">Evaluation Errors</span></a></li>
[% END %] [% END %]
</ul> </ul>
@ -108,6 +108,13 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
<div class="tab-content"> <div class="tab-content">
[% IF eval.evaluationerror.errormsg %]
<div id="tabs-errors" class="tab-pane">
<p>Errors occurred at [% INCLUDE renderDateTime timestamp=(eval.evaluationerror.errortime || eval.timestamp) %].</p>
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(eval.evaluationerror.errormsg) %]</pre></div></div>
</div>
[% END %]
<div id="tabs-aborted" class="tab-pane"> <div id="tabs-aborted" class="tab-pane">
[% INCLUDE renderSome builds=aborted tabname="#tabs-aborted" %] [% INCLUDE renderSome builds=aborted tabname="#tabs-aborted" %]
</div> </div>
@ -165,9 +172,10 @@ c.uri_for(c.controller('JobsetEval').action_for('view'),
[% END %] [% END %]
</div> </div>
[% IF eval.evaluationerror.has_error %] [% IF eval.evaluationerror.errormsg %]
<div id="tabs-errors" class="tab-pane"> <div id="tabs-errors" class="tab-pane">
<iframe src="[% c.uri_for(c.controller('JobsetEval').action_for('errors'), [eval.id], params) %]" loading="lazy" frameBorder="0" width="100%"></iframe> <p>Errors occurred at [% INCLUDE renderDateTime timestamp=(eval.evaluationerror.errortime || eval.timestamp) %].</p>
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(eval.evaluationerror.errormsg) %]</pre></div></div>
</div> </div>
[% END %] [% END %]
</div> </div>

View File

@ -61,7 +61,7 @@
[% END %] [% END %]
<li class="nav-item"><a class="nav-link active" href="#tabs-evaluations" data-toggle="tab">Evaluations</a></li> <li class="nav-item"><a class="nav-link active" href="#tabs-evaluations" data-toggle="tab">Evaluations</a></li>
[% IF jobset.has_error || jobset.fetcherrormsg %] [% IF jobset.errormsg || jobset.fetcherrormsg %]
<li class="nav-item"><a class="nav-link" href="#tabs-errors" data-toggle="tab"><span class="text-warning">Evaluation Errors</span></a></li> <li class="nav-item"><a class="nav-link" href="#tabs-errors" data-toggle="tab"><span class="text-warning">Evaluation Errors</span></a></li>
[% END %] [% END %]
<li class="nav-item"><a class="nav-link" href="#tabs-jobs" data-toggle="tab">Jobs</a></li> <li class="nav-item"><a class="nav-link" href="#tabs-jobs" data-toggle="tab">Jobs</a></li>
@ -79,7 +79,7 @@
<th>Last checked:</th> <th>Last checked:</th>
<td> <td>
[% IF jobset.lastcheckedtime %] [% IF jobset.lastcheckedtime %]
[% INCLUDE renderDateTime timestamp = jobset.lastcheckedtime %], [% IF jobset.has_error || jobset.fetcherrormsg %]<em class="text-warning">with errors!</em>[% ELSE %]<em>no errors</em>[% END %] [% INCLUDE renderDateTime timestamp = jobset.lastcheckedtime %], [% IF jobset.errormsg || jobset.fetcherrormsg %]<em class="text-warning">with errors!</em>[% ELSE %]<em>no errors</em>[% END %]
[% ELSE %] [% ELSE %]
<em>never</em> <em>never</em>
[% END %] [% END %]
@ -117,9 +117,10 @@
</div> </div>
[% IF jobset.has_error || jobset.fetcherrormsg %] [% IF jobset.errormsg || jobset.fetcherrormsg %]
<div id="tabs-errors" class="tab-pane"> <div id="tabs-errors" class="tab-pane">
<iframe src="[% c.uri_for('/jobset' project.name jobset.name "errors") %]" loading="lazy" frameBorder="0" width="100%"></iframe> <p>Errors occurred at [% INCLUDE renderDateTime timestamp=(jobset.errortime || jobset.lastcheckedtime) %].</p>
<div class="card bg-light"><div class="card-body"><pre>[% HTML.escape(jobset.fetcherrormsg || jobset.errormsg) %]</pre></div></div>
</div> </div>
[% END %] [% END %]

View File

@ -10,7 +10,31 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=Edge" /> <meta http-equiv="X-UA-Compatible" content="IE=Edge" />
[% INCLUDE style.tt %]
<script type="text/javascript" src="[% c.uri_for("/static/js/jquery/jquery-3.4.1.min.js") %]"></script>
<script type="text/javascript" src="[% c.uri_for("/static/js/jquery/jquery-ui-1.10.4.min.js") %]"></script>
<script type="text/javascript" src="[% c.uri_for("/static/js/moment/moment-2.24.0.min.js") %]"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="[% c.uri_for("/static/fontawesome/css/all.css") %]" rel="stylesheet" />
<script type="text/javascript" src="[% c.uri_for("/static/js/popper.min.js") %]"></script>
<script type="text/javascript" src="[% c.uri_for("/static/bootstrap/js/bootstrap.min.js") %]"></script>
<link href="[% c.uri_for("/static/bootstrap/css/bootstrap.min.css") %]" rel="stylesheet" />
<!-- hydra.css may need to be moved to before boostrap to make the @media rule work. -->
<link rel="stylesheet" href="[% c.uri_for("/static/css/hydra.css") %]" type="text/css" />
<link rel="stylesheet" href="[% c.uri_for("/static/css/rotated-th.css") %]" type="text/css" />
<style>
.popover { max-width: 40%; }
</style>
<script type="text/javascript" src="[% c.uri_for("/static/js/bootbox.min.js") %]"></script>
<link rel="stylesheet" href="[% c.uri_for("/static/css/tree.css") %]" type="text/css" />
<script type="text/javascript" src="[% c.uri_for("/static/js/common.js") %]"></script>
[% IF c.config.enable_google_login %] [% IF c.config.enable_google_login %]
<meta name="google-signin-client_id" content="[% c.config.google_client_id %]"> <meta name="google-signin-client_id" content="[% c.config.google_client_id %]">

View File

@ -6,10 +6,10 @@
<thead> <thead>
<tr> <tr>
<th>Job</th> <th>Job</th>
<th>System</th>
<th>Build</th> <th>Build</th>
<th>Step</th> <th>Step</th>
<th>What</th> <th>What</th>
<th>Status</th>
<th>Since</th> <th>Since</th>
</tr> </tr>
</thead> </thead>
@ -40,10 +40,10 @@
[% idle = 0 %] [% idle = 0 %]
<tr> <tr>
<td><tt>[% INCLUDE renderFullJobName project=step.project jobset=step.jobset job=step.job %]</tt></td> <td><tt>[% INCLUDE renderFullJobName project=step.project jobset=step.jobset job=step.job %]</tt></td>
<td><tt>[% step.system %]</tt></td>
<td><a href="[% c.uri_for('/build' step.build) %]">[% step.build %]</a></td> <td><a href="[% c.uri_for('/build' step.build) %]">[% step.build %]</a></td>
<td>[% IF step.busy >= 30 %]<a class="row-link" href="[% c.uri_for('/build' step.build 'nixlog' step.stepnr 'tail') %]">[% step.stepnr %]</a>[% ELSE; step.stepnr; END %]</td> <td>[% IF step.busy >= 30 %]<a class="row-link" href="[% c.uri_for('/build' step.build 'nixlog' step.stepnr 'tail') %]">[% step.stepnr %]</a>[% ELSE; step.stepnr; END %]</td>
<td><tt>[% step.drvpath.match('-(.*)').0 %]</tt></td> <td><tt>[% step.drvpath.match('-(.*)').0 %]</tt></td>
<td>[% INCLUDE renderBusyStatus %]</td>
<td style="width: 10em">[% INCLUDE renderDuration duration = curTime - step.starttime %] </td> <td style="width: 10em">[% INCLUDE renderDuration duration = curTime - step.starttime %] </td>
</tr> </tr>
[% END %] [% END %]

View File

@ -129,12 +129,6 @@ $(document).ready(function() {
el.addClass("is-local"); el.addClass("is-local");
} }
}); });
[...document.getElementsByTagName("iframe")].forEach((element) => {
element.contentWindow.addEventListener("DOMContentLoaded", (_) => {
element.style.height = element.contentWindow.document.body.scrollHeight + 'px';
})
})
}); });
var tabsLoaded = {}; var tabsLoaded = {};

View File

@ -7,7 +7,7 @@
[% ELSE %] [% ELSE %]
[% INCLUDE renderBuildList builds=resource showSchedulingInfo=1 hideResultInfo=1 busy=1 showStepName=1 %] [% INCLUDE renderBuildList builds=resource showSchedulingInfo=1 hideResultInfo=1 busy=1 %]
[% END %] [% END %]

View File

@ -1,24 +0,0 @@
<script type="text/javascript" src="[% c.uri_for("/static/js/jquery/jquery-3.4.1.min.js") %]"></script>
<script type="text/javascript" src="[% c.uri_for("/static/js/jquery/jquery-ui-1.10.4.min.js") %]"></script>
<script type="text/javascript" src="[% c.uri_for("/static/js/moment/moment-2.24.0.min.js") %]"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link href="[% c.uri_for("/static/fontawesome/css/all.css") %]" rel="stylesheet" />
<script type="text/javascript" src="[% c.uri_for("/static/js/popper.min.js") %]"></script>
<script type="text/javascript" src="[% c.uri_for("/static/bootstrap/js/bootstrap.min.js") %]"></script>
<link href="[% c.uri_for("/static/bootstrap/css/bootstrap.min.css") %]" rel="stylesheet" />
<!-- hydra.css may need to be moved to before boostrap to make the @media rule work. -->
<link rel="stylesheet" href="[% c.uri_for("/static/css/hydra.css") %]" type="text/css" />
<link rel="stylesheet" href="[% c.uri_for("/static/css/rotated-th.css") %]" type="text/css" />
<style>
.popover { max-width: 40%; }
</style>
<script type="text/javascript" src="[% c.uri_for("/static/js/bootbox.min.js") %]"></script>
<link rel="stylesheet" href="[% c.uri_for("/static/css/tree.css") %]" type="text/css" />
<script type="text/javascript" src="[% c.uri_for("/static/js/common.js") %]"></script>

View File

@ -32,9 +32,4 @@ subtest "/jobset/PROJECT/JOBSET/evals" => sub {
ok($jobsetevals->is_success, "The page showing the jobset evals returns 200."); ok($jobsetevals->is_success, "The page showing the jobset evals returns 200.");
}; };
subtest "/jobset/PROJECT/JOBSET/errors" => sub {
my $jobsetevals = request(GET '/jobset/' . $project->name . '/' . $jobset->name . '/errors');
ok($jobsetevals->is_success, "The page showing the jobset eval errors returns 200.");
};
done_testing; done_testing;

View File

@ -35,10 +35,6 @@ subtest "Fetching the eval's overview" => sub {
is($fetch->code, 200, "channel page is 200"); is($fetch->code, 200, "channel page is 200");
}; };
subtest "Fetching the eval's overview" => sub {
my $fetch = request(GET '/eval/' . $eval->id, '/errors');
is($fetch->code, 200, "errors page is 200");
};
done_testing; done_testing;

View File

@ -61,8 +61,8 @@ subtest "* selects all except current aggregate" => sub {
$jobset->discard_changes; # refresh from DB $jobset->discard_changes; # refresh from DB
is( is(
$jobset->has_error, $jobset->errormsg,
0, "",
"eval-errors non-empty" "eval-errors non-empty"
); );
}; };
@ -101,7 +101,7 @@ subtest "trivial cycle check" => sub {
ok(utf8::decode($stderr), "Stderr output is UTF8-clean"); ok(utf8::decode($stderr), "Stderr output is UTF8-clean");
$jobset->discard_changes({ '+columns' => {'errormsg' => 'errormsg'} }); # refresh from DB $jobset->discard_changes; # refresh from DB
like( like(
$jobset->errormsg, $jobset->errormsg,
qr/Dependency cycle: indirect_aggregate <-> ok_aggregate/, qr/Dependency cycle: indirect_aggregate <-> ok_aggregate/,
@ -123,7 +123,7 @@ subtest "cycle check with globbing" => sub {
ok(utf8::decode($stderr), "Stderr output is UTF8-clean"); ok(utf8::decode($stderr), "Stderr output is UTF8-clean");
$jobset->discard_changes({ '+columns' => {'errormsg' => 'errormsg'} }); # refresh from DB $jobset->discard_changes; # refresh from DB
like( like(
$jobset->errormsg, $jobset->errormsg,
qr/aggregate job indirect_aggregate failed with the error: Dependency cycle: indirect_aggregate <-> packages.constituentA/, qr/aggregate job indirect_aggregate failed with the error: Dependency cycle: indirect_aggregate <-> packages.constituentA/,

View File

@ -14,7 +14,7 @@ our @EXPORT = qw(
sub evalSucceeds { sub evalSucceeds {
my ($jobset) = @_; my ($jobset) = @_;
my ($res, $stdout, $stderr) = captureStdoutStderr(60, ("hydra-eval-jobset", $jobset->project->name, $jobset->name)); my ($res, $stdout, $stderr) = captureStdoutStderr(60, ("hydra-eval-jobset", $jobset->project->name, $jobset->name));
$jobset->discard_changes({ '+columns' => {'errormsg' => 'errormsg'} }); # refresh from DB $jobset->discard_changes; # refresh from DB
if ($res) { if ($res) {
chomp $stdout; chomp $stderr; chomp $stdout; chomp $stderr;
utf8::decode($stdout) or die "Invalid unicode in stdout."; utf8::decode($stdout) or die "Invalid unicode in stdout.";
@ -29,7 +29,7 @@ sub evalSucceeds {
sub evalFails { sub evalFails {
my ($jobset) = @_; my ($jobset) = @_;
my ($res, $stdout, $stderr) = captureStdoutStderr(60, ("hydra-eval-jobset", $jobset->project->name, $jobset->name)); my ($res, $stdout, $stderr) = captureStdoutStderr(60, ("hydra-eval-jobset", $jobset->project->name, $jobset->name));
$jobset->discard_changes({ '+columns' => {'errormsg' => 'errormsg'} }); # refresh from DB $jobset->discard_changes; # refresh from DB
if (!$res) { if (!$res) {
chomp $stdout; chomp $stderr; chomp $stdout; chomp $stderr;
utf8::decode($stdout) or die "Invalid unicode in stdout."; utf8::decode($stdout) or die "Invalid unicode in stdout.";

View File

@ -13,7 +13,7 @@ my $constituentBuildA = $builds->{"constituentA"};
my $constituentBuildB = $builds->{"constituentB"}; my $constituentBuildB = $builds->{"constituentB"};
my $eval = $constituentBuildA->jobsetevals->first(); my $eval = $constituentBuildA->jobsetevals->first();
is($eval->evaluationerror->has_error, 0); is($eval->evaluationerror->errormsg, "");
subtest "Verifying the direct aggregate" => sub { subtest "Verifying the direct aggregate" => sub {
my $aggBuild = $builds->{"direct_aggregate"}; my $aggBuild = $builds->{"direct_aggregate"};