Skip to content

Commit 4d5e8a1

Browse files
author
epriestley
committed
Split Harbormaster workers apart so build steps can run in parallel
Summary: Ref T1049. Currently, the Harbormaster worker looks like this: foreach (step) { run_step(step); } This means steps can't ever be run in parallel. Instead, split it into two workers. The "Build" worker starts things off, and basically does: update_build(); (We could theoretically do this in the original process because it should never take very long, but since there's a lock and it's a little bit complex it seemed cleaner to separate it.) The "Target" worker runs an individual target (like a command, or an HTTP request, or whatever), then updates the build: run_one_step(step); update_build(); The new `update_build()` mechanism in `HarbormasterBuildEngine` does this, roughly: figure_out_overall_status_of_all_steps(); if (build is done) { done(); } if (build is fail) { fail(); } foreach (step that is ready to run) { queue_target_worker_for_step(step); } So, overall: - The part of the code that updates Builds is completely separated from the part of the code that updates Targets. - Targets can run in parallel. Test Plan: - Ran a bunch of builds via `bin/harbormaster build`. - Ran a bunch of builds via web UI. Reviewers: btrahan Reviewed By: btrahan CC: aran Maniphest Tasks: T1049 Differential Revision: https://secure.phabricator.com/D7890
1 parent 6abe65b commit 4d5e8a1

File tree

5 files changed

+306
-71
lines changed

5 files changed

+306
-71
lines changed

src/__phutil_library_map__.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -707,6 +707,7 @@
707707
'HarbormasterBuildArtifact' => 'applications/harbormaster/storage/build/HarbormasterBuildArtifact.php',
708708
'HarbormasterBuildArtifactQuery' => 'applications/harbormaster/query/HarbormasterBuildArtifactQuery.php',
709709
'HarbormasterBuildCancelController' => 'applications/harbormaster/controller/HarbormasterBuildCancelController.php',
710+
'HarbormasterBuildEngine' => 'applications/harbormaster/engine/HarbormasterBuildEngine.php',
710711
'HarbormasterBuildItem' => 'applications/harbormaster/storage/build/HarbormasterBuildItem.php',
711712
'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php',
712713
'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php',
@@ -757,7 +758,9 @@
757758
'HarbormasterStepAddController' => 'applications/harbormaster/controller/HarbormasterStepAddController.php',
758759
'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php',
759760
'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php',
761+
'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php',
760762
'HarbormasterUIEventListener' => 'applications/harbormaster/event/HarbormasterUIEventListener.php',
763+
'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php',
761764
'HeraldAction' => 'applications/herald/storage/HeraldAction.php',
762765
'HeraldAdapter' => 'applications/herald/adapter/HeraldAdapter.php',
763766
'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php',
@@ -3159,6 +3162,7 @@
31593162
),
31603163
'HarbormasterBuildArtifactQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
31613164
'HarbormasterBuildCancelController' => 'HarbormasterController',
3165+
'HarbormasterBuildEngine' => 'Phobject',
31623166
'HarbormasterBuildItem' => 'HarbormasterDAO',
31633167
'HarbormasterBuildItemQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
31643168
'HarbormasterBuildLog' =>
@@ -3193,7 +3197,7 @@
31933197
),
31943198
'HarbormasterBuildTargetQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
31953199
'HarbormasterBuildViewController' => 'HarbormasterController',
3196-
'HarbormasterBuildWorker' => 'PhabricatorWorker',
3200+
'HarbormasterBuildWorker' => 'HarbormasterWorker',
31973201
'HarbormasterBuildable' =>
31983202
array(
31993203
0 => 'HarbormasterDAO',
@@ -3238,7 +3242,9 @@
32383242
'HarbormasterStepAddController' => 'HarbormasterController',
32393243
'HarbormasterStepDeleteController' => 'HarbormasterController',
32403244
'HarbormasterStepEditController' => 'HarbormasterController',
3245+
'HarbormasterTargetWorker' => 'HarbormasterWorker',
32413246
'HarbormasterUIEventListener' => 'PhabricatorEventListener',
3247+
'HarbormasterWorker' => 'PhabricatorWorker',
32423248
'HeraldAction' => 'HeraldDAO',
32433249
'HeraldApplyTranscript' => 'HeraldDAO',
32443250
'HeraldCapabilityManageGlobalRules' => 'PhabricatorPolicyCapability',
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
<?php
2+
3+
/**
4+
* Moves a build forward by queuing build tasks, canceling or restarting the
5+
* build, or failing it in response to task failures.
6+
*/
7+
final class HarbormasterBuildEngine extends Phobject {
8+
9+
private $build;
10+
private $viewer;
11+
private $newBuildTargets = array();
12+
13+
public function queueNewBuildTarget(HarbormasterBuildTarget $target) {
14+
$this->newBuildTargets[] = $target;
15+
return $this;
16+
}
17+
18+
public function getNewBuildTargets() {
19+
return $this->newBuildTargets;
20+
}
21+
22+
public function setViewer(PhabricatorUser $viewer) {
23+
$this->viewer = $viewer;
24+
return $this;
25+
}
26+
27+
public function getViewer() {
28+
return $this->viewer;
29+
}
30+
31+
public function setBuild(HarbormasterBuild $build) {
32+
$this->build = $build;
33+
return $this;
34+
}
35+
36+
public function getBuild() {
37+
return $this->build;
38+
}
39+
40+
public function continueBuild() {
41+
$build = $this->getBuild();
42+
43+
$lock_key = 'harbormaster.build:'.$build->getID();
44+
$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);
45+
46+
$build->reload();
47+
48+
try {
49+
$this->updateBuild($build);
50+
} catch (Exception $ex) {
51+
// If any exception is raised, the build is marked as a failure and the
52+
// exception is re-thrown (this ensures we don't leave builds in an
53+
// inconsistent state).
54+
$build->setBuildStatus(HarbormasterBuild::STATUS_ERROR);
55+
$build->save();
56+
57+
$lock->unlock();
58+
throw $ex;
59+
}
60+
61+
$lock->unlock();
62+
63+
// NOTE: We queue new targets after releasing the lock so that in-process
64+
// execution via `bin/harbormaster` does not reenter the locked region.
65+
foreach ($this->getNewBuildTargets() as $target) {
66+
$task = PhabricatorWorker::scheduleTask(
67+
'HarbormasterTargetWorker',
68+
array(
69+
'targetID' => $target->getID(),
70+
));
71+
}
72+
}
73+
74+
private function updateBuild(HarbormasterBuild $build) {
75+
// TODO: Handle cancellation and restarts.
76+
77+
if ($build->getBuildStatus() == HarbormasterBuild::STATUS_PENDING) {
78+
$this->destroyBuildTargets($build);
79+
$build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING);
80+
$build->save();
81+
}
82+
83+
if ($build->getBuildStatus() == HarbormasterBuild::STATUS_BUILDING) {
84+
return $this->updateBuildSteps($build);
85+
}
86+
}
87+
88+
private function destroyBuildTargets(HarbormasterBuild $build) {
89+
$targets = id(new HarbormasterBuildTargetQuery())
90+
->setViewer($this->getViewer())
91+
->withBuildPHIDs(array($build->getPHID()))
92+
->execute();
93+
foreach ($targets as $target) {
94+
$target->delete();
95+
}
96+
}
97+
98+
private function updateBuildSteps(HarbormasterBuild $build) {
99+
$targets = id(new HarbormasterBuildTargetQuery())
100+
->setViewer($this->getViewer())
101+
->withBuildPHIDs(array($build->getPHID()))
102+
->execute();
103+
$targets = mgroup($targets, 'getBuildStepPHID');
104+
105+
$steps = id(new HarbormasterBuildStepQuery())
106+
->setViewer($this->getViewer())
107+
->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID()))
108+
->execute();
109+
110+
// Identify steps which are complete.
111+
112+
$complete = array();
113+
$failed = array();
114+
$waiting = array();
115+
foreach ($steps as $step) {
116+
$step_targets = idx($targets, $step->getPHID(), array());
117+
118+
if ($step_targets) {
119+
$is_complete = true;
120+
foreach ($step_targets as $target) {
121+
// TODO: Move this to a top-level "status" field on BuildTarget.
122+
if (!$target->getDetail('__done__')) {
123+
$is_complete = false;
124+
break;
125+
}
126+
}
127+
128+
$is_failed = false;
129+
foreach ($step_targets as $target) {
130+
// TODO: Move this to a top-level "status" field on BuildTarget.
131+
if ($target->getDetail('__failed__')) {
132+
$is_failed = true;
133+
break;
134+
}
135+
}
136+
137+
$is_waiting = false;
138+
} else {
139+
$is_complete = false;
140+
$is_failed = false;
141+
$is_waiting = true;
142+
}
143+
144+
if ($is_complete) {
145+
$complete[$step->getPHID()] = true;
146+
}
147+
148+
if ($is_failed) {
149+
$failed[$step->getPHID()] = true;
150+
}
151+
152+
if ($is_waiting) {
153+
$waiting[$step->getPHID()] = true;
154+
}
155+
}
156+
157+
// If every step is complete, we're done with this build. Mark it passed
158+
// and bail.
159+
if (count($complete) == count($steps)) {
160+
$build->setBuildStatus(HarbormasterBuild::STATUS_PASSED);
161+
$build->save();
162+
return;
163+
}
164+
165+
// If any step failed, fail the whole build, then bail.
166+
if (count($failed)) {
167+
$build->setBuildStatus(HarbormasterBuild::STATUS_FAILED);
168+
$build->save();
169+
return;
170+
}
171+
172+
// Identify all the steps which are ready to run (because all their
173+
// depdendencies are complete).
174+
175+
$previous_step = null;
176+
$runnable = array();
177+
foreach ($steps as $step) {
178+
// TODO: For now, we're hard coding sequential dependencies into build
179+
// steps. In the future, we can be smart about this instead.
180+
181+
if ($previous_step) {
182+
$dependencies = array($previous_step);
183+
} else {
184+
$dependencies = array();
185+
}
186+
187+
if (isset($waiting[$step->getPHID()])) {
188+
$can_run = true;
189+
foreach ($dependencies as $dependency) {
190+
if (empty($complete[$dependency->getPHID()])) {
191+
$can_run = false;
192+
break;
193+
}
194+
}
195+
196+
if ($can_run) {
197+
$runnable[] = $step;
198+
}
199+
}
200+
201+
$previous_step = $step;
202+
}
203+
204+
if (!$runnable) {
205+
// TODO: This means the build is deadlocked, probably? It should not
206+
// normally be possible, but we should communicate it more clearly.
207+
$build->setBuildStatus(HarbormasterBuild::STATUS_FAILED);
208+
$build->save();
209+
return;
210+
}
211+
212+
foreach ($runnable as $runnable_step) {
213+
$target = HarbormasterBuildTarget::initializeNewBuildTarget(
214+
$build,
215+
$step,
216+
$build->retrieveVariablesFromBuild());
217+
$target->save();
218+
219+
$this->queueNewBuildTarget($target);
220+
}
221+
}
222+
223+
}
Lines changed: 8 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,28 @@
11
<?php
22

33
/**
4-
* Run builds
4+
* Start a build.
55
*/
6-
final class HarbormasterBuildWorker extends PhabricatorWorker {
7-
8-
public function getRequiredLeaseTime() {
9-
return 60 * 60 * 24;
10-
}
6+
final class HarbormasterBuildWorker extends HarbormasterWorker {
117

128
public function doWork() {
139
$data = $this->getTaskData();
1410
$id = idx($data, 'buildID');
11+
$viewer = $this->getViewer();
1512

16-
// Get a reference to the build.
1713
$build = id(new HarbormasterBuildQuery())
18-
->setViewer(PhabricatorUser::getOmnipotentUser())
19-
->withBuildStatuses(array(HarbormasterBuild::STATUS_PENDING))
14+
->setViewer($viewer)
2015
->withIDs(array($id))
2116
->executeOne();
2217
if (!$build) {
2318
throw new PhabricatorWorkerPermanentFailureException(
2419
pht('Invalid build ID "%s".', $id));
2520
}
2621

27-
// It's possible for the user to request cancellation before
28-
// a worker picks up a build. We check to see if the build
29-
// is already cancelled, and return if it is.
30-
if ($build->checkForCancellation()) {
31-
return;
32-
}
33-
34-
try {
35-
$build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING);
36-
$build->save();
37-
38-
$buildable = $build->getBuildable();
39-
$plan = $build->getBuildPlan();
40-
41-
$steps = $plan->loadOrderedBuildSteps();
42-
43-
// Perform the build.
44-
foreach ($steps as $step) {
45-
46-
// Create the target at this step.
47-
// TODO: Support variable artifacts.
48-
$target = HarbormasterBuildTarget::initializeNewBuildTarget(
49-
$build,
50-
$step,
51-
$build->retrieveVariablesFromBuild());
52-
$target->save();
53-
54-
$implementation = $target->getImplementation();
55-
if (!$implementation->validateSettings()) {
56-
$build->setBuildStatus(HarbormasterBuild::STATUS_ERROR);
57-
break;
58-
}
59-
$implementation->execute($build, $target);
60-
if ($build->getBuildStatus() !== HarbormasterBuild::STATUS_BUILDING) {
61-
break;
62-
}
63-
if ($build->checkForCancellation()) {
64-
break;
65-
}
66-
}
67-
68-
// Check to see if the user requested cancellation. If they did and
69-
// we get to here, they might have either cancelled too late, or the
70-
// step isn't cancellation aware. In either case we ignore the result
71-
// and move to a cancelled state.
72-
$build->checkForCancellation();
73-
74-
// If we get to here, then the build has finished. Set it to passed
75-
// if no build step explicitly set the status.
76-
if ($build->getBuildStatus() === HarbormasterBuild::STATUS_BUILDING) {
77-
$build->setBuildStatus(HarbormasterBuild::STATUS_PASSED);
78-
}
79-
$build->save();
80-
} catch (Exception $e) {
81-
// If any exception is raised, the build is marked as a failure and
82-
// the exception is re-thrown (this ensures we don't leave builds
83-
// in an inconsistent state).
84-
$build->setBuildStatus(HarbormasterBuild::STATUS_ERROR);
85-
$build->save();
86-
throw $e;
87-
}
22+
id(new HarbormasterBuildEngine())
23+
->setViewer($viewer)
24+
->setBuild($build)
25+
->continueBuild();
8826
}
8927

9028
}

0 commit comments

Comments
 (0)