diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema.pm b/src/HydraFrontend/lib/HydraFrontend/Schema.pm index 41bd4553..e9b028ed 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema.pm @@ -8,8 +8,8 @@ use base 'DBIx::Class::Schema'; __PACKAGE__->load_classes; -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:xP97YDrN7Bm2B/BlbQJ7fQ +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:dBO/r6lVlITiJ/HlltKcpQ # You can replace this text with custom content, and it will be preserved on regeneration diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Buildlogs.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Buildlogs.pm index 5c9991a0..2b6843fd 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Buildlogs.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Buildlogs.pm @@ -21,8 +21,8 @@ __PACKAGE__->set_primary_key("build", "logphase"); __PACKAGE__->belongs_to("build", "HydraFrontend::Schema::Builds", { id => "build" }); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:pt0CJFX1pP9Z2TjqrTjTkw +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:eMNna7u2l0ec+OYuvtGRpg # You can replace this text with custom content, and it will be preserved on regeneration diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Buildproducts.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Buildproducts.pm index 9aa4387e..68797703 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Buildproducts.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Buildproducts.pm @@ -21,8 +21,8 @@ __PACKAGE__->set_primary_key("build", "path"); __PACKAGE__->belongs_to("build", "HydraFrontend::Schema::Builds", { id => "build" }); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:3NKUaF4u4H6ZmIRCeva8yA +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:LaXQ4zxxvzdKFBRVcjMdMQ # You can replace this text with custom content, and it will be preserved on regeneration diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Builds.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Builds.pm index 3f2152f7..c92253ae 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Builds.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Builds.pm @@ -65,8 +65,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:8s5Z03ugocOVb021EwGVag +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:C1XPkCXQImyXduKER0Dllg __PACKAGE__->has_many(dependents => 'HydraFrontend::Schema::Inputs', 'dependency'); diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Inputs.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Inputs.pm index c184ac4c..0781f01b 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Inputs.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Inputs.pm @@ -35,8 +35,8 @@ __PACKAGE__->set_primary_key("id"); __PACKAGE__->belongs_to("build", "HydraFrontend::Schema::Builds", { id => "build" }); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:AzV6B/6CCrroPlO32n2p3A +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:A3Is4VTFkTl2DzrYjzdrZA __PACKAGE__->belongs_to("dependency", "HydraFrontend::Schema::Builds", { id => "dependency" }); diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Jobs.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Jobs.pm index 1dff2914..86250094 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Jobs.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Jobs.pm @@ -46,9 +46,13 @@ __PACKAGE__->belongs_to( ); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:GubRofAmJ/sbJbjyV3aKSQ +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZF8UB1MtbPuOk7wTSFJR5Q +__PACKAGE__->has_many( + "inputs", + "HydraFrontend::Schema::Inputs", + { "foreign.job" => "self.id" }, +); -# You can replace this text with custom content, and it will be preserved on regeneration 1; diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsetinputalts.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsetinputalts.pm index 5c2ebb9a..033c2c50 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsetinputalts.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsetinputalts.pm @@ -33,8 +33,8 @@ __PACKAGE__->belongs_to( ); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ZjjWLbAWExxOqsDz41A3KA +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ibTncC1AslPWt1eiTtwplA # You can replace this text with custom content, and it will be preserved on regeneration diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsetinputs.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsetinputs.pm index 1a371df0..f17cf73d 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsetinputs.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsetinputs.pm @@ -43,8 +43,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:6hzbFjPWQ872UxFhhpxjFg +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:D1UzSZwPtwDmOI7q6g8uKQ # You can replace this text with custom content, and it will be preserved on regeneration diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsets.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsets.pm index 236e06b3..91af1263 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsets.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Jobsets.pm @@ -56,8 +56,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:oRV4yw0DWG5PI0agcM7QHA +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:6Pyrgervmq03S5Nx8QfA1Q # You can replace this text with custom content, and it will be preserved on regeneration diff --git a/src/HydraFrontend/lib/HydraFrontend/Schema/Projects.pm b/src/HydraFrontend/lib/HydraFrontend/Schema/Projects.pm index b0b7069d..1c1104c1 100644 --- a/src/HydraFrontend/lib/HydraFrontend/Schema/Projects.pm +++ b/src/HydraFrontend/lib/HydraFrontend/Schema/Projects.pm @@ -29,8 +29,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 10:30:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:9SeEXSEOH1ocrdkoa7fx5Q +# Created by DBIx::Class::Schema::Loader v0.04005 @ 2008-11-10 14:25:07 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:p8LbF31qRl/JfMK5wfkeCg # You can replace this text with custom content, and it will be preserved on regeneration diff --git a/src/HydraFrontend/root/hydra.css b/src/HydraFrontend/root/hydra.css index cfd9b3da..bf29f776 100644 --- a/src/HydraFrontend/root/hydra.css +++ b/src/HydraFrontend/root/hydra.css @@ -157,6 +157,11 @@ ul.productList { padding-left: 1em; } +tr.runningJob { + background-color: #ff3030; + color: white; +} + /* Sortable tables */ diff --git a/src/HydraFrontend/root/index.tt b/src/HydraFrontend/root/index.tt index d10ffe10..338e4ad7 100644 --- a/src/HydraFrontend/root/index.tt +++ b/src/HydraFrontend/root/index.tt @@ -9,7 +9,7 @@ [% FOREACH job IN jobs -%] - + [% job.priority %] [% job.project.name %] [% job.jobset.name %] diff --git a/src/HydraFrontend/root/project.tt b/src/HydraFrontend/root/project.tt new file mode 100644 index 00000000..2aed2db0 --- /dev/null +++ b/src/HydraFrontend/root/project.tt @@ -0,0 +1,54 @@ +[% WRAPPER layout.tt title="Hydra Overview" %] + +

Project [% project.name %]

+ + +

Definition

+ +[% FOREACH jobset IN project.jobsets -%] + +

Jobset [% jobset.name %]

+ +

+ Description: [% jobset.description %] +
+ Nix expression: [% jobset.nixexprpath %] in input [% jobset.nixexprinput %] +

+ + + + + + + [% FOREACH input IN jobset.jobsetinputs -%] + + + + + + [% END %] + +
Input nameTypeValues
[% input.name %][% input.type %] + [% FOREACH alt IN input.jobsetinputalts -%] + [% IF input.type == "string" %] + "[% alt.value %]" + [% ELSE %] + [% alt.uri %] + [% END %] + [% END %] +
+ +[% END -%] + + +

Jobs

+ + + + + +[% END %] diff --git a/src/build.pl b/src/build.pl new file mode 100644 index 00000000..cee0ef45 --- /dev/null +++ b/src/build.pl @@ -0,0 +1,166 @@ +#! @perl@ -w + +use strict; +use File::Basename; +use HydraFrontend::Schema; + + +my $db = HydraFrontend::Schema->connect("dbi:SQLite:dbname=hydra.sqlite", "", "", {}); + + +sub isValidPath { + my $path = shift; + return system("nix-store --check-validity $path 2> /dev/null") == 0; +} + + +sub buildJob { + my ($job) = @_; + + my $drvPath = $job->drvpath; + my $outPath = $job->outpath; + + my $isCachedBuild = 1; + my $outputCreated = 1; # i.e., the Nix build succeeded (but it could be a positive failure) + my $startTime = 0; + my $stopTime = 0; + + if (!isValidPath($outPath)) { + $isCachedBuild = 0; + + $startTime = time(); + + print " BUILDING\n"; + + my $res = system("nix-store --realise $drvPath"); + + $stopTime = time(); + + $outputCreated = $res == 0; + } + + my $buildStatus; + + if ($outputCreated) { + # "Positive" failures, e.g. the builder returned exit code 0 + # but flagged some error condition. + $buildStatus = -e "$outPath/nix-support/failed" ? 2 : 0; + } else { + $buildStatus = 1; # = Nix failure + } + + $db->txn_do(sub { + my $build = $db->resultset('Builds')->create( + { timestamp => time() + , project => $job->project->name + , jobset => $job->jobset->name + , attrname => $job->attrname + , description => $job->description + , drvpath => $drvPath + , outpath => $outPath + , iscachedbuild => $isCachedBuild + , buildstatus => $buildStatus + , starttime => $startTime + , stoptime => $stopTime + , system => $job->system + }); + print " build ID = ", $build->id, "\n"; + + foreach my $input ($job->inputs) { + $input->job(undef); + $input->build($build->id); + $input->update; + } + + my $logPath = "/nix/var/log/nix/drvs/" . basename $drvPath; + if (-e $logPath) { + print " LOG $logPath\n"; + $db->resultset('Buildlogs')->create( + { build => $build->id + , logphase => "full" + , path => $logPath + , type => "raw" + }); + } + + if ($outputCreated) { + + if (-e "$outPath/log") { + foreach my $logPath (glob "$outPath/log/*") { + print " LOG $logPath\n"; + $db->resultset('Buildlogs')->create( + { build => $build->id + , logphase => basename($logPath) + , path => $logPath + , type => "raw" + }); + } + } + + if (-e "$outPath/nix-support/hydra-build-products") { + open LIST, "$outPath/nix-support/hydra-build-products" or die; + while () { + /^(\w+)\s+([\w-]+)\s+(\S+)$/ or die; + my $type = $1; + my $subtype = $2; + my $path = $3; + die unless -e $path; + $db->resultset('Buildproducts')->create( + { build => $build->id + , type => $type + , subtype => $subtype + , path => $path + }); + } + close LIST; + } else { + $db->resultset('Buildproducts')->create( + { build => $build->id + , type => "nix-build" + , subtype => "" + , path => $outPath + }); + } + } + + $job->delete; + }); +} + + +my $jobId = $ARGV[0] or die; +print "building job $jobId\n"; + +# Lock the job. If necessary, steal the lock from the parent process +# (runner.pl). This is so that if the runner dies, the children +# (i.e. the job builders) can continue to run and won't have the lock +# taken away. +my $job; +$db->txn_do(sub { + ($job) = $db->resultset('Jobs')->search({ id => $jobId }); + die "job $jobId doesn't exist" unless defined $job; + if ($job->busy != 0 && $job->locker != getppid) { + die "job $jobId is already being built"; + } + $job->busy(1); + $job->locker($$); + $job->update; +}); + +die unless $job; + +# Build the job. If it throws an error, unlock the job so that it can +# be retried. +eval { + print "BUILD\n"; + buildJob $job; + print "DONE\n"; +}; +if ($@) { + warn $@; + $db->txn_do(sub { + $job->busy(0); + $job->locker($$); + $job->update; + }); +} diff --git a/src/hydra.sql b/src/hydra.sql index 5cbce7cf..ab9e9bb5 100644 --- a/src/hydra.sql +++ b/src/hydra.sql @@ -129,10 +129,10 @@ create table jobs ( id integer primary key autoincrement not null, timestamp integer not null, -- time this build was added to the db (in Unix time) - priority integer not null, + priority integer not null default 0, - busy integer not null, -- true means someone is building this job now - locker text not null, -- !!! hostname/pid of the process building this job? + busy integer not null default 0, -- true means someone is building this job now + locker text not null default '', -- !!! hostname/pid of the process building this job? -- Info about the inputs. project text not null, -- !!! foreign key diff --git a/src/runner.pl b/src/runner.pl new file mode 100644 index 00000000..9fecd534 --- /dev/null +++ b/src/runner.pl @@ -0,0 +1,65 @@ +#! @perl@ -w + +use strict; +use HydraFrontend::Schema; + + +my $db = HydraFrontend::Schema->connect("dbi:SQLite:dbname=hydra.sqlite", "", "", {}); + + +# Unlock jobs whose building process has died. +$db->txn_do(sub { + my @jobs = $db->resultset('Jobs')->search({ busy => 1 }); + foreach my $job (@jobs) { + my $pid = $job->locker; + if (kill(0, $pid) != 1) { # see if we can signal the process + print "job ", $job->id, " pid $pid died, unlocking\n"; + $job->busy(0); + $job->locker(""); + $job->update; + } + } +}); + + +while (1) { + + print "looking for runnable jobs...\n"; + + my $job; + + $db->txn_do(sub { + + my @jobs = $db->resultset('Jobs')->search({ busy => 0 }, {order_by => ["priority", "timestamp"]}); + + print "# of available jobs: ", scalar(@jobs), "\n"; + + if (scalar @jobs > 0) { + $job = $jobs[0]; + $job->busy(1); + $job->locker($$); + $job->update; + } + + }); + + # Start the job. We need to do this outside the transaction in + # case it aborts or something. + if (defined $job) { + print "starting job ", $job->id, "\n"; + eval { + system("perl -I HydraFrontend/lib -w ./build.pl " . $job->id); + }; + if ($@) { + warn $@; + $db->txn_do(sub { + $job->busy(0); + $job->locker($$); + $job->update; + }); + } + } + + print "sleeping...\n"; + sleep(10); +} diff --git a/src/scheduler.pl b/src/scheduler.pl index 991652e1..a3563aad 100644 --- a/src/scheduler.pl +++ b/src/scheduler.pl @@ -2,7 +2,6 @@ use strict; use XML::Simple; -use File::Basename; use HydraFrontend::Schema; @@ -15,131 +14,6 @@ sub isValidPath { } -sub buildJob { - my ($project, $jobset, $jobName, $description, $drvPath, $outPath, $usedInputs, $system) = @_; - - if (scalar($db->resultset('Builds')->search({project => $project->name, jobset => $jobset->name, attrname => $jobName, outPath => $outPath})) > 0) { - print " already done\n"; - return; - } - - my $isCachedBuild = 1; - my $outputCreated = 1; # i.e., the Nix build succeeded (but it could be a positive failure) - my $startTime = 0; - my $stopTime = 0; - - if (!isValidPath($outPath)) { - $isCachedBuild = 0; - - $startTime = time(); - - print " BUILDING\n"; - - my $res = system("nix-store --realise $drvPath"); - - $stopTime = time(); - - $outputCreated = $res == 0; - } - - my $buildStatus; - - if ($outputCreated) { - # "Positive" failures, e.g. the builder returned exit code 0 - # but flagged some error condition. - $buildStatus = -e "$outPath/nix-support/failed" ? 2 : 0; - } else { - $buildStatus = 1; # = Nix failure - } - - $db->txn_do(sub { - my $build = $db->resultset('Builds')->create( - { timestamp => time() - , project => $project->name - , jobset => $jobset->name - , attrname => $jobName - , description => $description - , drvpath => $drvPath - , outpath => $outPath - , iscachedbuild => $isCachedBuild - , buildstatus => $buildStatus - , starttime => $startTime - , stoptime => $stopTime - , system => $system - }); - print " build ID = ", $build->id, "\n"; - - foreach my $inputName (keys %{$usedInputs}) { - my $input = $usedInputs->{$inputName}; - $db->resultset('Inputs')->create( - { build => $build->id - , name => $inputName - , type => $input->{type} - , uri => $input->{uri} - #, revision => $input->{orig}->revision - #, tag => $input->{orig}->tag - , value => $input->{value} - , dependency => $input->{id} - , path => ($input->{storePath} or "") # !!! temporary hack - }); - } - - my $logPath = "/nix/var/log/nix/drvs/" . basename $drvPath; - if (-e $logPath) { - print " LOG $logPath\n"; - $db->resultset('Buildlogs')->create( - { build => $build->id - , logphase => "full" - , path => $logPath - , type => "raw" - }); - } - - if ($outputCreated) { - - if (-e "$outPath/log") { - foreach my $logPath (glob "$outPath/log/*") { - print " LOG $logPath\n"; - $db->resultset('Buildlogs')->create( - { build => $build->id - , logphase => basename($logPath) - , path => $logPath - , type => "raw" - }); - } - } - - if (-e "$outPath/nix-support/hydra-build-products") { - open LIST, "$outPath/nix-support/hydra-build-products" or die; - while () { - /^(\w+)\s+([\w-]+)\s+(\S+)$/ or die; - my $type = $1; - my $subtype = $2; - my $path = $3; - die unless -e $path; - $db->resultset('Buildproducts')->create( - { build => $build->id - , type => $type - , subtype => $subtype - , path => $path - }); - } - close LIST; - } else { - $db->resultset('Buildproducts')->create( - { build => $build->id - , type => "nix-build" - , subtype => "" - , path => $outPath - }); - } - } - - }); - -} - - sub fetchInput { my ($input, $alt, $inputInfo) = @_; my $type = $input->type; @@ -186,7 +60,53 @@ sub checkJob { die unless $job->{drvPath} eq $drvPath; my $outPath = $job->{outPath}; - buildJob($project, $jobset, $jobName, $description, $drvPath, $outPath, $inputInfo, $job->{system}); + $db->txn_do(sub { + if (scalar($db->resultset('Builds')->search( + { project => $project->name, jobset => $jobset->name + , attrname => $jobName, outPath => $outPath })) > 0) + { + print " already done\n"; + return; + } + + if (scalar($db->resultset('Jobs')->search( + { project => $project->name, jobset => $jobset->name + , attrname => $jobName, outPath => $outPath })) > 0) + { + print " already queued\n"; + return; + } + + print " adding to queue\n"; + my $job = $db->resultset('Jobs')->create( + { timestamp => time() + , priority => 0 + , busy => 0 + , locker => "" + , project => $project->name + , jobset => $jobset->name + , attrname => $jobName + , description => $description + , drvpath => $drvPath + , outpath => $outPath + , system => $job->{system} + }); + + foreach my $inputName (keys %{$inputInfo}) { + my $input = $inputInfo->{$inputName}; + $db->resultset('Inputs')->create( + { job => $job->id + , name => $inputName + , type => $input->{type} + , uri => $input->{uri} + #, revision => $input->{orig}->revision + #, tag => $input->{orig}->tag + , value => $input->{value} + , dependency => $input->{id} + , path => ($input->{storePath} or "") # !!! temporary hack + }); + } + }); };