4 * Moves a build forward by queuing build tasks, canceling or restarting the
5 * build, or failing it in response to task failures.
7 final class HarbormasterBuildEngine
extends Phobject
{
11 private $newBuildTargets = array();
12 private $artifactReleaseQueue = array();
13 private $forceBuildableUpdate;
15 public function setForceBuildableUpdate($force_buildable_update) {
16 $this->forceBuildableUpdate
= $force_buildable_update;
20 public function shouldForceBuildableUpdate() {
21 return $this->forceBuildableUpdate
;
24 public function queueNewBuildTarget(HarbormasterBuildTarget
$target) {
25 $this->newBuildTargets
[] = $target;
29 public function getNewBuildTargets() {
30 return $this->newBuildTargets
;
33 public function setViewer(PhabricatorUser
$viewer) {
34 $this->viewer
= $viewer;
38 public function getViewer() {
42 public function setBuild(HarbormasterBuild
$build) {
43 $this->build
= $build;
47 public function getBuild() {
51 public function continueBuild() {
52 $viewer = $this->getViewer();
53 $build = $this->getBuild();
55 $lock_key = 'harbormaster.build:'.$build->getID();
56 $lock = PhabricatorGlobalLock
::newLock($lock_key)->lock(15);
59 $old_status = $build->getBuildStatus();
62 $this->updateBuild($build);
63 } catch (Exception
$ex) {
64 // If any exception is raised, the build is marked as a failure and the
65 // exception is re-thrown (this ensures we don't leave builds in an
66 // inconsistent state).
67 $build->setBuildStatus(HarbormasterBuildStatus
::STATUS_ERROR
);
72 $build->releaseAllArtifacts($viewer);
79 // NOTE: We queue new targets after releasing the lock so that in-process
80 // execution via `bin/harbormaster` does not reenter the locked region.
81 foreach ($this->getNewBuildTargets() as $target) {
82 $task = PhabricatorWorker
::scheduleTask(
83 'HarbormasterTargetWorker',
85 'targetID' => $target->getID(),
88 'objectPHID' => $target->getPHID(),
92 // If the build changed status, we might need to update the overall status
94 $new_status = $build->getBuildStatus();
95 if ($new_status != $old_status ||
$this->shouldForceBuildableUpdate()) {
96 $this->updateBuildable($build->getBuildable());
99 $this->releaseQueuedArtifacts();
101 // If we are no longer building for any reason, release all artifacts.
102 if (!$build->isBuilding()) {
103 $build->releaseAllArtifacts($viewer);
107 private function updateBuild(HarbormasterBuild
$build) {
108 $viewer = $this->getViewer();
110 $content_source = PhabricatorContentSource
::newForSource(
111 PhabricatorDaemonContentSource
::SOURCECONST
);
113 $acting_phid = $viewer->getPHID();
115 $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
118 $editor = $build->getApplicationTransactionEditor()
120 ->setActingAsPHID($acting_phid)
121 ->setContentSource($content_source)
122 ->setContinueOnNoEffect(true)
123 ->setContinueOnMissingFields(true);
127 $messages = $build->getUnprocessedMessagesForApply();
128 foreach ($messages as $message) {
129 $message_type = $message->getType();
132 HarbormasterBuildMessageTransaction
::getTransactionTypeForMessageType(
135 if (!$message_xaction) {
139 $xactions[] = $build->getApplicationTransactionTemplate()
140 ->setAuthorPHID($message->getAuthorPHID())
141 ->setTransactionType($message_xaction)
142 ->setNewValue($message_type);
146 if ($build->isPending()) {
147 // TODO: This should be a transaction.
149 $build->restartBuild($viewer);
150 $build->setBuildStatus(HarbormasterBuildStatus
::STATUS_BUILDING
);
156 $editor->applyTransactions($build, $xactions);
157 $build->markUnprocessedMessagesAsProcessed();
160 if ($build->getBuildStatus() == HarbormasterBuildStatus
::STATUS_BUILDING
) {
161 $this->updateBuildSteps($build);
165 private function updateBuildSteps(HarbormasterBuild
$build) {
166 $all_targets = id(new HarbormasterBuildTargetQuery())
167 ->setViewer($this->getViewer())
168 ->withBuildPHIDs(array($build->getPHID()))
169 ->withBuildGenerations(array($build->getBuildGeneration()))
172 $this->updateWaitingTargets($all_targets);
174 $targets = mgroup($all_targets, 'getBuildStepPHID');
176 $steps = id(new HarbormasterBuildStepQuery())
177 ->setViewer($this->getViewer())
178 ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID()))
180 $steps = mpull($steps, null, 'getPHID');
182 // Identify steps which are in various states.
189 foreach ($steps as $step) {
190 $step_targets = idx($targets, $step->getPHID(), array());
195 $is_underway = false;
196 foreach ($step_targets as $target) {
197 if ($target->isUnderway()) {
204 foreach ($step_targets as $target) {
205 if ($target->isWaiting()) {
212 foreach ($step_targets as $target) {
213 if (!$target->isComplete()) {
214 $is_complete = false;
220 foreach ($step_targets as $target) {
221 if ($target->isFailed()) {
228 $is_underway = false;
230 $is_complete = false;
235 $queued[$step->getPHID()] = true;
239 $underway[$step->getPHID()] = true;
243 $waiting[$step->getPHID()] = true;
247 $complete[$step->getPHID()] = true;
251 $failed[$step->getPHID()] = true;
255 // If any step failed, fail the whole build, then bail.
256 if (count($failed)) {
257 $build->setBuildStatus(HarbormasterBuildStatus
::STATUS_FAILED
);
262 // If every step is complete, we're done with this build. Mark it passed
264 if (count($complete) == count($steps)) {
265 $build->setBuildStatus(HarbormasterBuildStatus
::STATUS_PASSED
);
270 // Release any artifacts which are not inputs to any remaining build
271 // step. We're done with these, so something else is free to use them.
272 $ongoing_phids = array_keys($queued +
$waiting +
$underway);
273 $ongoing_steps = array_select_keys($steps, $ongoing_phids);
274 $this->releaseUnusedArtifacts($all_targets, $ongoing_steps);
276 // Identify all the steps which are ready to run (because all their
277 // dependencies are complete).
280 foreach ($steps as $step) {
281 $dependencies = $step->getStepImplementation()->getDependencies($step);
283 if (isset($queued[$step->getPHID()])) {
285 foreach ($dependencies as $dependency) {
286 if (empty($complete[$dependency])) {
298 if (!$runnable && !$waiting && !$underway) {
299 // This means the build is deadlocked, and the user has configured
300 // circular dependencies.
301 $build->setBuildStatus(HarbormasterBuildStatus
::STATUS_DEADLOCKED
);
306 foreach ($runnable as $runnable_step) {
307 $target = HarbormasterBuildTarget
::initializeNewBuildTarget(
310 $build->retrieveVariablesFromBuild());
313 $this->queueNewBuildTarget($target);
319 * Release any artifacts which aren't used by any running or waiting steps.
321 * This releases artifacts as soon as they're no longer used. This can be
322 * particularly relevant when a build uses multiple hosts since it returns
323 * hosts to the pool more quickly.
325 * @param list<HarbormasterBuildTarget> Targets in the build.
326 * @param list<HarbormasterBuildStep> List of running and waiting steps.
329 private function releaseUnusedArtifacts(array $targets, array $steps) {
330 assert_instances_of($targets, 'HarbormasterBuildTarget');
331 assert_instances_of($steps, 'HarbormasterBuildStep');
333 if (!$targets ||
!$steps) {
337 $target_phids = mpull($targets, 'getPHID');
339 $artifacts = id(new HarbormasterBuildArtifactQuery())
340 ->setViewer($this->getViewer())
341 ->withBuildTargetPHIDs($target_phids)
342 ->withIsReleased(false)
348 // Collect all the artifacts that remaining build steps accept as inputs.
349 $must_keep = array();
350 foreach ($steps as $step) {
351 $inputs = $step->getStepImplementation()->getArtifactInputs();
352 foreach ($inputs as $input) {
353 $artifact_key = $input['key'];
354 $must_keep[$artifact_key] = true;
358 // Queue unreleased artifacts which no remaining step uses for immediate
360 foreach ($artifacts as $artifact) {
361 $key = $artifact->getArtifactKey();
362 if (isset($must_keep[$key])) {
366 $this->artifactReleaseQueue
[] = $artifact;
372 * Process messages which were sent to these targets, kicking applicable
373 * targets out of "Waiting" and into either "Passed" or "Failed".
375 * @param list<HarbormasterBuildTarget> List of targets to process.
378 private function updateWaitingTargets(array $targets) {
379 assert_instances_of($targets, 'HarbormasterBuildTarget');
381 // We only care about messages for targets which are actually in a waiting
383 $waiting_targets = array();
384 foreach ($targets as $target) {
385 if ($target->isWaiting()) {
386 $waiting_targets[$target->getPHID()] = $target;
390 if (!$waiting_targets) {
394 $messages = id(new HarbormasterBuildMessageQuery())
395 ->setViewer($this->getViewer())
396 ->withReceiverPHIDs(array_keys($waiting_targets))
397 ->withConsumed(false)
400 foreach ($messages as $message) {
401 $target = $waiting_targets[$message->getReceiverPHID()];
403 switch ($message->getType()) {
404 case HarbormasterMessageType
::MESSAGE_PASS
:
405 $new_status = HarbormasterBuildTarget
::STATUS_PASSED
;
407 case HarbormasterMessageType
::MESSAGE_FAIL
:
408 $new_status = HarbormasterBuildTarget
::STATUS_FAILED
;
410 case HarbormasterMessageType
::MESSAGE_WORK
:
416 if ($new_status !== null) {
417 $message->setIsConsumed(true);
420 $target->setTargetStatus($new_status);
422 if ($target->isComplete()) {
423 $target->setDateCompleted(PhabricatorTime
::getNow());
433 * Update the overall status of the buildable this build is attached to.
435 * After a build changes state (for example, passes or fails) it may affect
436 * the overall state of the associated buildable. Compute the new aggregate
437 * state and save it on the buildable.
439 * @param HarbormasterBuild The buildable to update.
442 public function updateBuildable(HarbormasterBuildable
$buildable) {
443 $viewer = $this->getViewer();
445 $lock_key = 'harbormaster.buildable:'.$buildable->getID();
446 $lock = PhabricatorGlobalLock
::newLock($lock_key)->lock(15);
448 $buildable = id(new HarbormasterBuildableQuery())
450 ->withIDs(array($buildable->getID()))
454 $messages = id(new HarbormasterBuildMessageQuery())
456 ->withReceiverPHIDs(array($buildable->getPHID()))
457 ->withConsumed(false)
460 $done_preparing = false;
461 $update_container = false;
462 foreach ($messages as $message) {
463 switch ($message->getType()) {
464 case HarbormasterMessageType
::BUILDABLE_BUILD
:
465 $done_preparing = true;
467 case HarbormasterMessageType
::BUILDABLE_CONTAINER
:
468 $update_container = true;
475 ->setIsConsumed(true)
479 // If we received a "build" command, all builds are scheduled and we can
480 // move out of "preparing" into "building".
481 if ($done_preparing) {
482 if ($buildable->isPreparing()) {
484 ->setBuildableStatus(HarbormasterBuildableStatus
::STATUS_BUILDING
)
489 // If we've been informed that the container for the buildable has
490 // changed, update it.
491 if ($update_container) {
492 $object = id(new PhabricatorObjectQuery())
494 ->withPHIDs(array($buildable->getBuildablePHID()))
498 ->setContainerPHID($object->getHarbormasterContainerPHID())
503 $old = clone $buildable;
505 // Don't update the buildable status if we're still preparing builds: more
506 // builds may still be scheduled shortly, so even if every build we know
507 // about so far has passed, that doesn't mean the buildable has actually
508 // passed everything it needs to.
510 if (!$buildable->isPreparing()) {
511 $behavior_key = HarbormasterBuildPlanBehavior
::BEHAVIOR_BUILDABLE
;
512 $behavior = HarbormasterBuildPlanBehavior
::getBehavior($behavior_key);
514 $key_never = HarbormasterBuildPlanBehavior
::BUILDABLE_NEVER
;
515 $key_building = HarbormasterBuildPlanBehavior
::BUILDABLE_IF_BUILDING
;
519 foreach ($buildable->getBuilds() as $build) {
520 $plan = $build->getBuildPlan();
521 $option = $behavior->getPlanOption($plan);
522 $option_key = $option->getKey();
524 $is_never = ($option_key === $key_never);
525 $is_building = ($option_key === $key_building);
527 // If this build "Never" affects the buildable, ignore it.
532 // If this build affects the buildable "If Building", but is already
533 // complete, ignore it.
534 if ($is_building && $build->isComplete()) {
538 if (!$build->isPassed()) {
542 if ($build->isComplete() && !$build->isPassed()) {
548 $new_status = HarbormasterBuildableStatus
::STATUS_FAILED
;
549 } else if ($all_pass) {
550 $new_status = HarbormasterBuildableStatus
::STATUS_PASSED
;
552 $new_status = HarbormasterBuildableStatus
::STATUS_BUILDING
;
555 $did_update = ($old->getBuildableStatus() !== $new_status);
557 $buildable->setBuildableStatus($new_status);
564 // Don't publish anything if we're still preparing builds.
565 if ($buildable->isPreparing()) {
569 $this->publishBuildable($old, $buildable);
572 public function publishBuildable(
573 HarbormasterBuildable
$old,
574 HarbormasterBuildable
$new) {
576 $viewer = $this->getViewer();
578 // Publish the buildable. We publish buildables even if they haven't
579 // changed status in Harbormaster because applications may care about
580 // different things than Harbormaster does. For example, Differential
581 // does not care about local lint and unit tests when deciding whether
582 // a revision should move out of draft or not.
584 // NOTE: We're publishing both automatic and manual buildables. Buildable
585 // objects should generally ignore manual buildables, but it's up to them
588 $object = id(new PhabricatorObjectQuery())
590 ->withPHIDs(array($new->getBuildablePHID()))
596 $engine = HarbormasterBuildableEngine
::newForObject($object, $viewer);
598 $daemon_source = PhabricatorContentSource
::newForSource(
599 PhabricatorDaemonContentSource
::SOURCECONST
);
601 $harbormaster_phid = id(new PhabricatorHarbormasterApplication())
605 ->setActingAsPHID($harbormaster_phid)
606 ->setContentSource($daemon_source)
607 ->publishBuildable($old, $new);
610 private function releaseQueuedArtifacts() {
611 foreach ($this->artifactReleaseQueue
as $key => $artifact) {
612 $artifact->releaseArtifact();
613 unset($this->artifactReleaseQueue
[$key]);