forceBuildableUpdate = $force_buildable_update; return $this; } public function shouldForceBuildableUpdate() { return $this->forceBuildableUpdate; } public function queueNewBuildTarget(HarbormasterBuildTarget $target) { $this->newBuildTargets[] = $target; return $this; } public function getNewBuildTargets() { return $this->newBuildTargets; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } public function getViewer() { return $this->viewer; } public function setBuild(HarbormasterBuild $build) { $this->build = $build; return $this; } public function getBuild() { return $this->build; } public function continueBuild() { $build = $this->getBuild(); $lock_key = 'harbormaster.build:'.$build->getID(); $lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15); $build->reload(); $old_status = $build->getBuildStatus(); try { $this->updateBuild($build); } catch (Exception $ex) { // If any exception is raised, the build is marked as a failure and the // exception is re-thrown (this ensures we don't leave builds in an // inconsistent state). $build->setBuildStatus(HarbormasterBuild::STATUS_ERROR); $build->save(); $lock->unlock(); throw $ex; } $lock->unlock(); // NOTE: We queue new targets after releasing the lock so that in-process // execution via `bin/harbormaster` does not reenter the locked region. foreach ($this->getNewBuildTargets() as $target) { $task = PhabricatorWorker::scheduleTask( 'HarbormasterTargetWorker', array( 'targetID' => $target->getID(), )); } // If the build changed status, we might need to update the overall status // on the buildable. $new_status = $build->getBuildStatus(); if ($new_status != $old_status || $this->shouldForceBuildableUpdate()) { $this->updateBuildable($build->getBuildable()); } } private function updateBuild(HarbormasterBuild $build) { if (($build->getBuildStatus() == HarbormasterBuild::STATUS_PENDING) || ($build->isRestarting())) { $this->destroyBuildTargets($build); $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); $build->save(); } if ($build->isResuming()) { $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING); $build->save(); } if ($build->isStopping() && !$build->isComplete()) { $build->setBuildStatus(HarbormasterBuild::STATUS_STOPPED); $build->save(); } $build->deleteUnprocessedCommands(); if ($build->getBuildStatus() == HarbormasterBuild::STATUS_BUILDING) { $this->updateBuildSteps($build); } } private function destroyBuildTargets(HarbormasterBuild $build) { $targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($this->getViewer()) ->withBuildPHIDs(array($build->getPHID())) ->execute(); if (!$targets) { return; } $target_phids = mpull($targets, 'getPHID'); $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer($this->getViewer()) ->withBuildTargetPHIDs($target_phids) ->execute(); foreach ($artifacts as $artifact) { $artifact->delete(); } foreach ($targets as $target) { $target->delete(); } } private function updateBuildSteps(HarbormasterBuild $build) { $targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($this->getViewer()) ->withBuildPHIDs(array($build->getPHID())) ->execute(); $this->updateWaitingTargets($targets); $targets = mgroup($targets, 'getBuildStepPHID'); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($this->getViewer()) ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID())) ->execute(); // Identify steps which are in various states. $queued = array(); $underway = array(); $waiting = array(); $complete = array(); $failed = array(); foreach ($steps as $step) { $step_targets = idx($targets, $step->getPHID(), array()); if ($step_targets) { $is_queued = false; $is_underway = false; foreach ($step_targets as $target) { if ($target->isUnderway()) { $is_underway = true; break; } } $is_waiting = false; foreach ($step_targets as $target) { if ($target->isWaiting()) { $is_waiting = true; break; } } $is_complete = true; foreach ($step_targets as $target) { if (!$target->isComplete()) { $is_complete = false; break; } } $is_failed = false; foreach ($step_targets as $target) { if ($target->isFailed()) { $is_failed = true; break; } } } else { $is_queued = true; $is_underway = false; $is_waiting = false; $is_complete = false; $is_failed = false; } if ($is_queued) { $queued[$step->getPHID()] = true; } if ($is_underway) { $underway[$step->getPHID()] = true; } if ($is_waiting) { $waiting[$step->getPHID()] = true; } if ($is_complete) { $complete[$step->getPHID()] = true; } if ($is_failed) { $failed[$step->getPHID()] = true; } } // If any step failed, fail the whole build, then bail. if (count($failed)) { $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); $build->save(); return; } // If every step is complete, we're done with this build. Mark it passed // and bail. if (count($complete) == count($steps)) { $build->setBuildStatus(HarbormasterBuild::STATUS_PASSED); $build->save(); return; } // Identify all the steps which are ready to run (because all their // depdendencies are complete). $previous_step = null; $runnable = array(); foreach ($steps as $step) { // TODO: For now, we're hard coding sequential dependencies into build // steps. In the future, we can be smart about this instead. if ($previous_step) { $dependencies = array($previous_step); } else { $dependencies = array(); } if (isset($queued[$step->getPHID()])) { $can_run = true; foreach ($dependencies as $dependency) { if (empty($complete[$dependency->getPHID()])) { $can_run = false; break; } } if ($can_run) { $runnable[] = $step; } } $previous_step = $step; } if (!$runnable && !$waiting && !$underway) { // TODO: This means the build is deadlocked, probably? It should not // normally be possible yet, but we should communicate it more clearly. $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); $build->save(); return; } foreach ($runnable as $runnable_step) { $target = HarbormasterBuildTarget::initializeNewBuildTarget( $build, $runnable_step, $build->retrieveVariablesFromBuild()); $target->save(); $this->queueNewBuildTarget($target); } } /** * Process messages which were sent to these targets, kicking applicable * targets out of "Waiting" and into either "Passed" or "Failed". * * @param list List of targets to process. * @return void */ private function updateWaitingTargets(array $targets) { assert_instances_of($targets, 'HarbormasterBuildTarget'); // We only care about messages for targets which are actually in a waiting // state. $waiting_targets = array(); foreach ($targets as $target) { if ($target->isWaiting()) { $waiting_targets[$target->getPHID()] = $target; } } if (!$waiting_targets) { return; } $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($this->getViewer()) ->withBuildTargetPHIDs(array_keys($waiting_targets)) ->withConsumed(false) ->execute(); foreach ($messages as $message) { $target = $waiting_targets[$message->getBuildTargetPHID()]; $new_status = null; switch ($message->getType()) { case 'pass': $new_status = HarbormasterBuildTarget::STATUS_PASSED; break; case 'fail': $new_status = HarbormasterBuildTarget::STATUS_FAILED; break; } if ($new_status !== null) { $message->setIsConsumed(true); $message->save(); $target->setTargetStatus($new_status); $target->save(); } } } /** * Update the overall status of the buildable this build is attached to. * * After a build changes state (for example, passes or fails) it may affect * the overall state of the associated buildable. Compute the new aggregate * state and save it on the buildable. * * @param HarbormasterBuild The buildable to update. * @return void */ private function updateBuildable(HarbormasterBuildable $buildable) { $viewer = $this->getViewer(); $lock_key = 'harbormaster.buildable:'.$buildable->getID(); $lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15); $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withIDs(array($buildable->getID())) ->needBuilds(true) ->executeOne(); $all_pass = true; $any_fail = false; foreach ($buildable->getBuilds() as $build) { if ($build->getBuildStatus() != HarbormasterBuild::STATUS_PASSED) { $all_pass = false; } if ($build->getBuildStatus() == HarbormasterBuild::STATUS_FAILED || $build->getBuildStatus() == HarbormasterBuild::STATUS_ERROR) { $any_fail = true; } } if ($any_fail) { $new_status = HarbormasterBuildable::STATUS_FAILED; } else if ($all_pass) { $new_status = HarbormasterBuildable::STATUS_PASSED; } else { $new_status = HarbormasterBuildable::STATUS_BUILDING; } $old_status = $buildable->getBuildableStatus(); $did_update = ($old_status != $new_status); if ($did_update) { $buildable->setBuildableStatus($new_status); $buildable->save(); } $lock->unlock(); // If we changed the buildable status, try to post a transaction to the // object about it. We can safely do this outside of the locked region. // NOTE: We only post transactions for automatic buildables, not for // manual ones: manual builds are test builds, whoever is doing tests // can look at the results themselves, and other users generally don't // care about the outcome. $should_publish = $did_update && $new_status != HarbormasterBuildable::STATUS_BUILDING && !$buildable->getIsManualBuildable(); if ($should_publish) { $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($buildable->getBuildablePHID())) ->executeOne(); if ($object instanceof PhabricatorApplicationTransactionInterface) { $template = $object->getApplicationTransactionTemplate(); if ($template) { $template ->setTransactionType(PhabricatorTransactions::TYPE_BUILDABLE) ->setMetadataValue( 'harbormaster:buildablePHID', $buildable->getPHID()) ->setOldValue($old_status) ->setNewValue($new_status); $harbormaster_phid = id(new PhabricatorApplicationHarbormaster()) ->getPHID(); $daemon_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_DAEMON, array()); $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) ->setActingAsPHID($harbormaster_phid) ->setContentSource($daemon_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $editor->applyTransactions( $object->getApplicationTransactionObject(), array($template)); } } } } }