When a patch or executable is submitted for testing it is stored in the staging directory for two reasons, first because the corresponding Job does not exist yet, and second because the process may not have write permissions to the Job directory. So far transfering the file to the Job directory was the responsibility of the Step that needs it. However this breaks down when more than one Step needs that file (e.g. a Windows executable build Step and a Wine testing one). There is also a (very unlikely) race condition between the scheduler and the Job creation: the Scheduler could run the first saved Task, mark it and the Job as completed before the Job's other Steps and Tasks get saved to the database. Those would then never get run since the Jobs has completed. So this patch makes staging the responsibility of the Job and introduces a staging Status for it. It also introduces a new Status which indicates the Job is still being set up to avoid race conditions. Note that this patch preserves the Step-based staging mechanism for backward compatibility during the transition.
Signed-off-by: Francois Gouget fgouget@codeweavers.com ---
Note: * This requires updating the database schema and restarting the TestBot Engine and web server. * The following patches switch each Job creator to the new scheme.
testbot/bin/CheckForWinetestUpdate.pl | 2 ++ testbot/ddl/update36.sql | 4 +++ testbot/ddl/winetestbot.sql | 2 +- testbot/lib/WineTestBot/Engine/Scheduler.pm | 30 ++++++++++++++++++++- testbot/lib/WineTestBot/Jobs.pm | 20 +++++++++++--- testbot/lib/WineTestBot/Patches.pm | 1 + testbot/web/Submit.pl | 1 + 7 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 testbot/ddl/update36.sql
diff --git a/testbot/bin/CheckForWinetestUpdate.pl b/testbot/bin/CheckForWinetestUpdate.pl index f41148f12..17dab8236 100755 --- a/testbot/bin/CheckForWinetestUpdate.pl +++ b/testbot/bin/CheckForWinetestUpdate.pl @@ -200,6 +200,7 @@ sub AddJob($$$) # First create a new job my $Jobs = CreateJobs(); my $NewJob = $Jobs->Add(); + $NewJob->Status("queued"); $NewJob->User(GetBatchUser()); $NewJob->Priority($BaseJob && $Bits == 32 ? 8 : 9); $NewJob->Remarks($Remarks); @@ -243,6 +244,7 @@ sub AddReconfigJob() # First create a new job my $Jobs = CreateJobs(); my $NewJob = $Jobs->Add(); + $NewJob->Status("queued"); $NewJob->User(GetBatchUser()); $NewJob->Priority(3); $NewJob->Remarks($Remarks); diff --git a/testbot/ddl/update36.sql b/testbot/ddl/update36.sql new file mode 100644 index 000000000..2f4bf7fa2 --- /dev/null +++ b/testbot/ddl/update36.sql @@ -0,0 +1,4 @@ +USE winetestbot; + +ALTER TABLE Jobs + MODIFY Status ENUM('new', 'staging', 'queued', 'running', 'completed', 'badpatch', 'badbuild', 'boterror', 'canceled') NOT NULL; diff --git a/testbot/ddl/winetestbot.sql b/testbot/ddl/winetestbot.sql index 9a28385a9..1e7b1edd1 100644 --- a/testbot/ddl/winetestbot.sql +++ b/testbot/ddl/winetestbot.sql @@ -113,7 +113,7 @@ CREATE TABLE Jobs BranchName VARCHAR(20) NOT NULL, UserName VARCHAR(40) NOT NULL, Priority INT(1) NOT NULL, - Status ENUM('queued', 'running', 'completed', 'badpatch', 'badbuild', 'boterror', 'canceled') NOT NULL, + Status ENUM('new', 'staging', 'queued', 'running', 'completed', 'badpatch', 'badbuild', 'boterror', 'canceled') NOT NULL, Remarks VARCHAR(128) NULL, Submitted DATETIME NOT NULL, Ended DATETIME NULL, diff --git a/testbot/lib/WineTestBot/Engine/Scheduler.pm b/testbot/lib/WineTestBot/Engine/Scheduler.pm index 10c0b70ed..85bc0f397 100644 --- a/testbot/lib/WineTestBot/Engine/Scheduler.pm +++ b/testbot/lib/WineTestBot/Engine/Scheduler.pm @@ -30,6 +30,8 @@ WineTestBot::Engine::Scheduler - Schedules the TestBot tasks use Exporter 'import'; our @EXPORT = qw(ScheduleJobs CheckJobs);
+use File::Copy; + use WineTestBot::Config; use WineTestBot::Engine::Events; use WineTestBot::Jobs; @@ -524,11 +526,37 @@ sub _ScheduleTasks($) # Process the jobs in decreasing priority order my $JobRank; my $Jobs = CreateJobs($Sched->{VMs}); - $Jobs->AddFilter("Status", ["queued", "running"]); + $Jobs->AddFilter("Status", ["staging", "queued", "running"]); foreach my $Job (sort CompareJobPriority @{$Jobs->GetItems()}) { $JobRank++;
+ if ($Job->Status eq "staging") + { + # Move the file(s) from the staging directory to the job directory + my %Staged; + my $JobDir = $Job->CreateDir(); + foreach my $Step (@{$Job->Steps->Clone()->GetItems()}) + { + # Ignore steps that need a file provided by the previous step + next if ($Step->PreviousNo or !defined $Step->FileName); + # Skip the step if its file has already been staged + next if ($Staged{$Step->FileName}); + + my $StagingFile = "job". $Job->Id ."_". $Step->FileName; + if (move("$DataDir/staging/$StagingFile", "$JobDir/". $Step->FileName)) + { + $Staged{$Step->FileName} = 1; + } + else + { + LogMsg "Could not move the '$StagingFile' staging file: $!"; + } + } + $Job->Status("queued"); + $Job->Save(); + } + # The per-step lists of VMs that should be getting ready to run # before we prepare the next step my %StepVMs = ("" => []); # no dependency for the first step diff --git a/testbot/lib/WineTestBot/Jobs.pm b/testbot/lib/WineTestBot/Jobs.pm index 13a389586..3fd78aaa2 100644 --- a/testbot/lib/WineTestBot/Jobs.pm +++ b/testbot/lib/WineTestBot/Jobs.pm @@ -1,6 +1,6 @@ # -*- Mode: Perl; perl-indent-level: 2; indent-tabs-mode: nil -*- # Copyright 2009 Ge van Geldorp -# Copyright 2012-2017 Francois Gouget +# Copyright 2012-2018 Francois Gouget # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -52,7 +52,19 @@ A Job's lifecycle is as follows: =over
=item * -A Job is created with Status set to queued which means it is ready to run. +A Job is created with Status set to new. During this time the process setting +up the Job can add new Steps and Tasks to it. It can also put the required +input files into place in the staging directory with a name of the form +'job<JobID>_<FileName>' where <JobId> is the Id of the new Job, and FileName is +the name of a file a Step depends on. + +=item * +Once the Job set up is complete its Status is set to staging. This indicates +that the TestBot Engine can retrieve the files it depends on from the staging +directory and move them to the Job's directory. + +=item * +Then the Status is set to queued which indicates the Job is ready to run.
=item * As soon as one of the Step starts running, the Job's Status field is set to @@ -93,7 +105,7 @@ sub InitializeNew($$) my ($self, $Collection) = @_;
$self->Branch(CreateBranches()->GetDefaultBranch()); - $self->Status("queued"); + $self->Status("new"); $self->Submitted(time());
$self->SUPER::InitializeNew($Collection); @@ -446,7 +458,7 @@ my @PropertyDescriptors = ( CreateItemrefPropertyDescriptor("Branch", "Branch", !1, 1, &CreateBranches, ["BranchName"]), CreateItemrefPropertyDescriptor("User", "Author", !1, 1, &CreateUsers, ["UserName"]), CreateBasicPropertyDescriptor("Priority", "Priority", !1, 1, "N", 1), - CreateEnumPropertyDescriptor("Status", "Status", !1, 1, ['queued', 'running', 'completed', 'badpatch', 'badbuild', 'boterror', 'canceled']), + CreateEnumPropertyDescriptor("Status", "Status", !1, 1, ['new', 'staging', 'queued', 'running', 'completed', 'badpatch', 'badbuild', 'boterror', 'canceled']), CreateBasicPropertyDescriptor("Remarks", "Remarks", !1, !1, "A", 128), CreateBasicPropertyDescriptor("Submitted", "Submitted", !1, !1, "DT", 19), CreateBasicPropertyDescriptor("Ended", "Ended", !1, !1, "DT", 19), diff --git a/testbot/lib/WineTestBot/Patches.pm b/testbot/lib/WineTestBot/Patches.pm index d5be1b355..4f428133f 100644 --- a/testbot/lib/WineTestBot/Patches.pm +++ b/testbot/lib/WineTestBot/Patches.pm @@ -166,6 +166,7 @@ sub Submit($$$)
# Create a new job for this patch my $NewJob = $Jobs->Add(); + $NewJob->Status("queued"); $NewJob->User($User); $NewJob->Priority(6); my $PropertyDescriptor = $Jobs->GetPropertyDescriptorByName("Remarks"); diff --git a/testbot/web/Submit.pl b/testbot/web/Submit.pl index ae69e7ce5..8e22621fd 100644 --- a/testbot/web/Submit.pl +++ b/testbot/web/Submit.pl @@ -774,6 +774,7 @@ sub OnSubmit($) # First create a new job my $Jobs = CreateJobs(); my $NewJob = $Jobs->Add(); + $NewJob->Status("queued"); $NewJob->User($self->GetCurrentSession()->User); $NewJob->Priority(5); if ($self->GetParam("Remarks"))
The new approach ensures there is no race condition with the TestBot Engine between saving the job and saving the steps.
Signed-off-by: Francois Gouget fgouget@codeweavers.com --- testbot/bin/CheckForWinetestUpdate.pl | 46 +++++++++++++++++++-------- 1 file changed, 32 insertions(+), 14 deletions(-)
diff --git a/testbot/bin/CheckForWinetestUpdate.pl b/testbot/bin/CheckForWinetestUpdate.pl index 17dab8236..137db572c 100755 --- a/testbot/bin/CheckForWinetestUpdate.pl +++ b/testbot/bin/CheckForWinetestUpdate.pl @@ -192,15 +192,9 @@ sub AddJob($$$) return 1; }
- # Create a hard link in staging so it can then be moved into the job - # directory. This is ok because the latest file is never overwritten. - my $StagingFileName = CreateNewLink("$DataDir/latest/$LatestBaseName", - "$DataDir/staging", "_$LatestBaseName"); - # First create a new job my $Jobs = CreateJobs(); my $NewJob = $Jobs->Add(); - $NewJob->Status("queued"); $NewJob->User(GetBatchUser()); $NewJob->Priority($BaseJob && $Bits == 32 ? 8 : 9); $NewJob->Remarks($Remarks); @@ -210,9 +204,9 @@ sub AddJob($$$) my $NewStep = $Steps->Add(); my $BitsSuffix = ($Bits == 64 ? "64" : ""); $NewStep->Type("suite"); - $NewStep->FileName(basename($StagingFileName)); + $NewStep->FileName($LatestBaseName); $NewStep->FileType($Bits == 64 ? "exe64" : "exe32"); - $NewStep->InStaging(1); + $NewStep->InStaging(!1);
# Add a task for each VM my $Tasks = $NewStep->Tasks; @@ -224,12 +218,28 @@ sub AddJob($$$) $Task->Timeout($SuiteTimeout); }
- # Now save the whole thing + # Save it all my ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); if (defined $ErrMessage) { - Error "Failed to save job: $ErrMessage\n"; - unlink($StagingFileName); + LogMsg "Failed to save the $LatestBaseName job: $ErrMessage\n"; + return 0; + } + + # Stage the test file so it can be picked up by the job + if (!link("$DataDir/latest/$LatestBaseName", + "$DataDir/staging/job". $NewJob->Id ."_$LatestBaseName")) + { + Error "Failed to stage $LatestBaseName: $!\n"; + return 0; + } + + # Switch Status to staging to indicate we are done setting up the job + $NewJob->Status("staging"); + ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); + if (defined $ErrMessage) + { + Error "Failed to save the $LatestBaseName job (staging): $ErrMessage\n"; return 0; }
@@ -244,7 +254,6 @@ sub AddReconfigJob() # First create a new job my $Jobs = CreateJobs(); my $NewJob = $Jobs->Add(); - $NewJob->Status("queued"); $NewJob->User(GetBatchUser()); $NewJob->Priority(3); $NewJob->Remarks($Remarks); @@ -266,11 +275,20 @@ sub AddReconfigJob() $Task->VM($BuildVM); $Task->Timeout($ReconfigTimeout);
- # Now save the whole thing + # Save it all my ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); if (defined $ErrMessage) { - Error "Failed to save reconfig job: $ErrMessage\n"; + Error "Failed to save the Reconfig job: $ErrMessage\n"; + return 0; + } + + # Switch Status to staging to indicate we are done setting up the job + $NewJob->Status("staging"); + ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); + if (defined $ErrMessage) + { + Error "Failed to save the Reconfig job (staging): $ErrMessage\n"; return 0; } }
Signed-off-by: Francois Gouget fgouget@codeweavers.com --- testbot/web/Submit.pl | 63 ++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 34 deletions(-)
diff --git a/testbot/web/Submit.pl b/testbot/web/Submit.pl index 8e22621fd..d5265a3b3 100644 --- a/testbot/web/Submit.pl +++ b/testbot/web/Submit.pl @@ -753,28 +753,11 @@ sub OnSubmit($) my $BaseName = $self->ValidateAndGetFileName("FileName"); return !1 if (!$BaseName);
- # Store the file in the staging directory until the relevant Job and Step - # IDs are known and it can be moved to the jobs directory tree. But rename - # it so it does not get overwritten if the user submits another one before - # the Engine gets around to doing so. - my $StagingFileName = CreateNewFile("$DataDir/staging", "_$BaseName"); - - my $TmpStagingFullPath = $self->GetTmpStagingFullPath($BaseName); - if ($StagingFileName and !rename($TmpStagingFullPath, $StagingFileName)) - { - $self->{ErrMessage} = "Could not rename '$TmpStagingFullPath' to '$StagingFileName': $!\n"; - unlink($StagingFileName); - $StagingFileName = undef; - } - # If needed fall back to the existing staging file and hope for the best. - $StagingFileName = basename($StagingFileName || $TmpStagingFullPath); - # See also Patches::Submit() in lib/WineTestBot/Patches.pm
# First create a new job my $Jobs = CreateJobs(); my $NewJob = $Jobs->Add(); - $NewJob->Status("queued"); $NewJob->User($self->GetCurrentSession()->User); $NewJob->Priority(5); if ($self->GetParam("Remarks")) @@ -792,15 +775,15 @@ sub OnSubmit($) } my $Steps = $NewJob->Steps;
+ my $BuildStep; my $FileType = $self->GetParam("FileType"); - my $BuildStepNo; if ($FileType eq "patchdlls" || $FileType eq "patchprograms") { # This is a patch so add a build step... - my $BuildStep = $Steps->Add(); - $BuildStep->FileName($StagingFileName); + $BuildStep = $Steps->Add(); + $BuildStep->FileName($BaseName); $BuildStep->FileType($FileType); - $BuildStep->InStaging(1); + $BuildStep->InStaging(!1); $BuildStep->Type("build"); $BuildStep->DebugLevel(0);
@@ -813,14 +796,13 @@ sub OnSubmit($) $Task->VM($BuildVM); $Task->Timeout($BuildTimeout);
- # Save this step (&job+task) so the others can reference it + # Save the build step so the others can reference it my ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); if (defined($ErrMessage)) { $self->{ErrMessage} = $ErrMessage; return !1; } - $BuildStepNo = 1; }
# Add steps and tasks for the 32 and 64-bit tests @@ -844,22 +826,18 @@ sub OnSubmit($) { # First create the test step my $TestStep = $Steps->Add(); - $TestStep->PreviousNo($BuildStepNo); if ($FileType eq "patchdlls" || $FileType eq "patchprograms") { - my $FileName=$self->GetParam("TestExecutable"); - if ($Bits eq "64") - { - $FileName =~ s/_test.exe$/_test64.exe/; - } - $TestStep->FileName($FileName); - $TestStep->InStaging(!1); + $TestStep->PreviousNo($BuildStep->No); + my $TestExe = basename($self->GetParam("TestExecutable")); + $TestExe =~ s/_test.exe$/_test64.exe/ if ($Bits eq "64"); + $TestStep->FileName($TestExe); } else { - $TestStep->FileName($StagingFileName); - $TestStep->InStaging(1); + $TestStep->FileName($BaseName); } + $TestStep->InStaging(!1); $TestStep->FileType("exe$Bits"); $TestStep->Type("single"); $TestStep->DebugLevel($self->GetParam("DebugLevel")); @@ -875,7 +853,7 @@ sub OnSubmit($) } }
- # Now save the whole thing (or whatever's left to save) + # Now save it all (or whatever's left to save) my ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); if (defined($ErrMessage)) { @@ -883,6 +861,23 @@ sub OnSubmit($) return !1; }
+ # Stage the test patch/executable so the job can pick it up + my $TmpStagingFullPath = $self->GetTmpStagingFullPath($BaseName); + if (!rename($TmpStagingFullPath, "$DataDir/staging/job". $NewJob->Id ."_$BaseName")) + { + $self->{ErrMessage} = "Could not stage '$BaseName': $!\n"; + return !1; + } + + # Switch Status to staging to indicate we are done setting up the job + $NewJob->Status("staging"); + ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); + if (defined($ErrMessage)) + { + $self->{ErrMessage} = $ErrMessage; + return !1; + } + # Notify engine my $ErrMessage = RescheduleJobs(); if (defined $ErrMessage)
Signed-off-by: Francois Gouget fgouget@codeweavers.com --- testbot/lib/WineTestBot/Patches.pm | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-)
diff --git a/testbot/lib/WineTestBot/Patches.pm b/testbot/lib/WineTestBot/Patches.pm index 4f428133f..e349dfe88 100644 --- a/testbot/lib/WineTestBot/Patches.pm +++ b/testbot/lib/WineTestBot/Patches.pm @@ -166,7 +166,6 @@ sub Submit($$$)
# Create a new job for this patch my $NewJob = $Jobs->Add(); - $NewJob->Status("queued"); $NewJob->User($User); $NewJob->Priority(6); my $PropertyDescriptor = $Jobs->GetPropertyDescriptorByName("Remarks"); @@ -181,11 +180,9 @@ sub Submit($$$) # Add build step to the job my $Steps = $NewJob->Steps; my $NewStep = $Steps->Add(); - # Create a link to the patch file in the staging dir - my $StagingFileName = CreateNewLink($PatchFileName, "$DataDir/staging", "_patch.diff"); - $NewStep->FileName(basename($StagingFileName)); + $NewStep->FileName("patch.diff"); $NewStep->FileType($TestInfo->{Type}); - $NewStep->InStaging(1); + $NewStep->InStaging(!1); $NewStep->Type("build"); $NewStep->DebugLevel(0);
@@ -198,7 +195,7 @@ sub Submit($$$) $Task->VM($BuildVM); $Task->Timeout($BuildTimeout);
- # Save this step (&job+task) so the others can reference it + # Save the build step so other steps can reference it my ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); if (defined($ErrMessage)) { @@ -206,6 +203,13 @@ sub Submit($$$) return $ErrMessage; }
+ # Stage the patch so it can be picked up by the job + if (!link($PatchFileName, "$DataDir/staging/job". $NewJob->Id ."_patch.diff")) + { + $self->Disposition("Failed to prepare patch file"); + return $!; + } + foreach my $Unit (sort keys %{$TestInfo->{Units}}) { # Add 32 and 64-bit tasks @@ -217,7 +221,7 @@ sub Submit($$$) if (@{$VMs->GetKeys()}) { # Create the corresponding Step - $NewStep = $Steps->Add(); + my $NewStep = $Steps->Add(); $NewStep->PreviousNo(1); my $FileName = $TestInfo->{ExeBase}; $FileName .= "64" if ($Bits eq "64"); @@ -240,13 +244,23 @@ sub Submit($$$) } }
+ # Save it all ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); - if (defined($ErrMessage)) + if (defined $ErrMessage) { $self->Disposition("Failed to submit job"); return $ErrMessage; }
+ # Switch Status to staging to indicate we are done setting up the job + $NewJob->Status("staging"); + ($ErrKey, $ErrProperty, $ErrMessage) = $Jobs->Save(); + if (defined $ErrMessage) + { + $self->Disposition("Failed to submit job (staging)"); + return $ErrMessage; + } + if ($First) { $First = !1;