Path: blob/master/src/applications/harbormaster/engine/HarbormasterBuildEngine.php
12256 views
<?php12/**3* Moves a build forward by queuing build tasks, canceling or restarting the4* build, or failing it in response to task failures.5*/6final class HarbormasterBuildEngine extends Phobject {78private $build;9private $viewer;10private $newBuildTargets = array();11private $artifactReleaseQueue = array();12private $forceBuildableUpdate;1314public function setForceBuildableUpdate($force_buildable_update) {15$this->forceBuildableUpdate = $force_buildable_update;16return $this;17}1819public function shouldForceBuildableUpdate() {20return $this->forceBuildableUpdate;21}2223public function queueNewBuildTarget(HarbormasterBuildTarget $target) {24$this->newBuildTargets[] = $target;25return $this;26}2728public function getNewBuildTargets() {29return $this->newBuildTargets;30}3132public function setViewer(PhabricatorUser $viewer) {33$this->viewer = $viewer;34return $this;35}3637public function getViewer() {38return $this->viewer;39}4041public function setBuild(HarbormasterBuild $build) {42$this->build = $build;43return $this;44}4546public function getBuild() {47return $this->build;48}4950public function continueBuild() {51$viewer = $this->getViewer();52$build = $this->getBuild();5354$lock_key = 'harbormaster.build:'.$build->getID();55$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);5657$build->reload();58$old_status = $build->getBuildStatus();5960try {61$this->updateBuild($build);62} catch (Exception $ex) {63// If any exception is raised, the build is marked as a failure and the64// exception is re-thrown (this ensures we don't leave builds in an65// inconsistent state).66$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ERROR);67$build->save();6869$lock->unlock();7071$build->releaseAllArtifacts($viewer);7273throw $ex;74}7576$lock->unlock();7778// NOTE: We queue new targets after releasing the lock so that in-process79// execution via `bin/harbormaster` does not reenter the locked region.80foreach ($this->getNewBuildTargets() as $target) {81$task = PhabricatorWorker::scheduleTask(82'HarbormasterTargetWorker',83array(84'targetID' => $target->getID(),85),86array(87'objectPHID' => $target->getPHID(),88));89}9091// If the build changed status, we might need to update the overall status92// on the buildable.93$new_status = $build->getBuildStatus();94if ($new_status != $old_status || $this->shouldForceBuildableUpdate()) {95$this->updateBuildable($build->getBuildable());96}9798$this->releaseQueuedArtifacts();99100// If we are no longer building for any reason, release all artifacts.101if (!$build->isBuilding()) {102$build->releaseAllArtifacts($viewer);103}104}105106private function updateBuild(HarbormasterBuild $build) {107$viewer = $this->getViewer();108109$content_source = PhabricatorContentSource::newForSource(110PhabricatorDaemonContentSource::SOURCECONST);111112$acting_phid = $viewer->getPHID();113if (!$acting_phid) {114$acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();115}116117$editor = $build->getApplicationTransactionEditor()118->setActor($viewer)119->setActingAsPHID($acting_phid)120->setContentSource($content_source)121->setContinueOnNoEffect(true)122->setContinueOnMissingFields(true);123124$xactions = array();125126$messages = $build->getUnprocessedMessagesForApply();127foreach ($messages as $message) {128$message_type = $message->getType();129130$message_xaction =131HarbormasterBuildMessageTransaction::getTransactionTypeForMessageType(132$message_type);133134if (!$message_xaction) {135continue;136}137138$xactions[] = $build->getApplicationTransactionTemplate()139->setAuthorPHID($message->getAuthorPHID())140->setTransactionType($message_xaction)141->setNewValue($message_type);142}143144if (!$xactions) {145if ($build->isPending()) {146// TODO: This should be a transaction.147148$build->restartBuild($viewer);149$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);150$build->save();151}152}153154if ($xactions) {155$editor->applyTransactions($build, $xactions);156$build->markUnprocessedMessagesAsProcessed();157}158159if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) {160$this->updateBuildSteps($build);161}162}163164private function updateBuildSteps(HarbormasterBuild $build) {165$all_targets = id(new HarbormasterBuildTargetQuery())166->setViewer($this->getViewer())167->withBuildPHIDs(array($build->getPHID()))168->withBuildGenerations(array($build->getBuildGeneration()))169->execute();170171$this->updateWaitingTargets($all_targets);172173$targets = mgroup($all_targets, 'getBuildStepPHID');174175$steps = id(new HarbormasterBuildStepQuery())176->setViewer($this->getViewer())177->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID()))178->execute();179$steps = mpull($steps, null, 'getPHID');180181// Identify steps which are in various states.182183$queued = array();184$underway = array();185$waiting = array();186$complete = array();187$failed = array();188foreach ($steps as $step) {189$step_targets = idx($targets, $step->getPHID(), array());190191if ($step_targets) {192$is_queued = false;193194$is_underway = false;195foreach ($step_targets as $target) {196if ($target->isUnderway()) {197$is_underway = true;198break;199}200}201202$is_waiting = false;203foreach ($step_targets as $target) {204if ($target->isWaiting()) {205$is_waiting = true;206break;207}208}209210$is_complete = true;211foreach ($step_targets as $target) {212if (!$target->isComplete()) {213$is_complete = false;214break;215}216}217218$is_failed = false;219foreach ($step_targets as $target) {220if ($target->isFailed()) {221$is_failed = true;222break;223}224}225} else {226$is_queued = true;227$is_underway = false;228$is_waiting = false;229$is_complete = false;230$is_failed = false;231}232233if ($is_queued) {234$queued[$step->getPHID()] = true;235}236237if ($is_underway) {238$underway[$step->getPHID()] = true;239}240241if ($is_waiting) {242$waiting[$step->getPHID()] = true;243}244245if ($is_complete) {246$complete[$step->getPHID()] = true;247}248249if ($is_failed) {250$failed[$step->getPHID()] = true;251}252}253254// If any step failed, fail the whole build, then bail.255if (count($failed)) {256$build->setBuildStatus(HarbormasterBuildStatus::STATUS_FAILED);257$build->save();258return;259}260261// If every step is complete, we're done with this build. Mark it passed262// and bail.263if (count($complete) == count($steps)) {264$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PASSED);265$build->save();266return;267}268269// Release any artifacts which are not inputs to any remaining build270// step. We're done with these, so something else is free to use them.271$ongoing_phids = array_keys($queued + $waiting + $underway);272$ongoing_steps = array_select_keys($steps, $ongoing_phids);273$this->releaseUnusedArtifacts($all_targets, $ongoing_steps);274275// Identify all the steps which are ready to run (because all their276// dependencies are complete).277278$runnable = array();279foreach ($steps as $step) {280$dependencies = $step->getStepImplementation()->getDependencies($step);281282if (isset($queued[$step->getPHID()])) {283$can_run = true;284foreach ($dependencies as $dependency) {285if (empty($complete[$dependency])) {286$can_run = false;287break;288}289}290291if ($can_run) {292$runnable[] = $step;293}294}295}296297if (!$runnable && !$waiting && !$underway) {298// This means the build is deadlocked, and the user has configured299// circular dependencies.300$build->setBuildStatus(HarbormasterBuildStatus::STATUS_DEADLOCKED);301$build->save();302return;303}304305foreach ($runnable as $runnable_step) {306$target = HarbormasterBuildTarget::initializeNewBuildTarget(307$build,308$runnable_step,309$build->retrieveVariablesFromBuild());310$target->save();311312$this->queueNewBuildTarget($target);313}314}315316317/**318* Release any artifacts which aren't used by any running or waiting steps.319*320* This releases artifacts as soon as they're no longer used. This can be321* particularly relevant when a build uses multiple hosts since it returns322* hosts to the pool more quickly.323*324* @param list<HarbormasterBuildTarget> Targets in the build.325* @param list<HarbormasterBuildStep> List of running and waiting steps.326* @return void327*/328private function releaseUnusedArtifacts(array $targets, array $steps) {329assert_instances_of($targets, 'HarbormasterBuildTarget');330assert_instances_of($steps, 'HarbormasterBuildStep');331332if (!$targets || !$steps) {333return;334}335336$target_phids = mpull($targets, 'getPHID');337338$artifacts = id(new HarbormasterBuildArtifactQuery())339->setViewer($this->getViewer())340->withBuildTargetPHIDs($target_phids)341->withIsReleased(false)342->execute();343if (!$artifacts) {344return;345}346347// Collect all the artifacts that remaining build steps accept as inputs.348$must_keep = array();349foreach ($steps as $step) {350$inputs = $step->getStepImplementation()->getArtifactInputs();351foreach ($inputs as $input) {352$artifact_key = $input['key'];353$must_keep[$artifact_key] = true;354}355}356357// Queue unreleased artifacts which no remaining step uses for immediate358// release.359foreach ($artifacts as $artifact) {360$key = $artifact->getArtifactKey();361if (isset($must_keep[$key])) {362continue;363}364365$this->artifactReleaseQueue[] = $artifact;366}367}368369370/**371* Process messages which were sent to these targets, kicking applicable372* targets out of "Waiting" and into either "Passed" or "Failed".373*374* @param list<HarbormasterBuildTarget> List of targets to process.375* @return void376*/377private function updateWaitingTargets(array $targets) {378assert_instances_of($targets, 'HarbormasterBuildTarget');379380// We only care about messages for targets which are actually in a waiting381// state.382$waiting_targets = array();383foreach ($targets as $target) {384if ($target->isWaiting()) {385$waiting_targets[$target->getPHID()] = $target;386}387}388389if (!$waiting_targets) {390return;391}392393$messages = id(new HarbormasterBuildMessageQuery())394->setViewer($this->getViewer())395->withReceiverPHIDs(array_keys($waiting_targets))396->withConsumed(false)397->execute();398399foreach ($messages as $message) {400$target = $waiting_targets[$message->getReceiverPHID()];401402switch ($message->getType()) {403case HarbormasterMessageType::MESSAGE_PASS:404$new_status = HarbormasterBuildTarget::STATUS_PASSED;405break;406case HarbormasterMessageType::MESSAGE_FAIL:407$new_status = HarbormasterBuildTarget::STATUS_FAILED;408break;409case HarbormasterMessageType::MESSAGE_WORK:410default:411$new_status = null;412break;413}414415if ($new_status !== null) {416$message->setIsConsumed(true);417$message->save();418419$target->setTargetStatus($new_status);420421if ($target->isComplete()) {422$target->setDateCompleted(PhabricatorTime::getNow());423}424425$target->save();426}427}428}429430431/**432* Update the overall status of the buildable this build is attached to.433*434* After a build changes state (for example, passes or fails) it may affect435* the overall state of the associated buildable. Compute the new aggregate436* state and save it on the buildable.437*438* @param HarbormasterBuild The buildable to update.439* @return void440*/441public function updateBuildable(HarbormasterBuildable $buildable) {442$viewer = $this->getViewer();443444$lock_key = 'harbormaster.buildable:'.$buildable->getID();445$lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);446447$buildable = id(new HarbormasterBuildableQuery())448->setViewer($viewer)449->withIDs(array($buildable->getID()))450->needBuilds(true)451->executeOne();452453$messages = id(new HarbormasterBuildMessageQuery())454->setViewer($viewer)455->withReceiverPHIDs(array($buildable->getPHID()))456->withConsumed(false)457->execute();458459$done_preparing = false;460$update_container = false;461foreach ($messages as $message) {462switch ($message->getType()) {463case HarbormasterMessageType::BUILDABLE_BUILD:464$done_preparing = true;465break;466case HarbormasterMessageType::BUILDABLE_CONTAINER:467$update_container = true;468break;469default:470break;471}472473$message474->setIsConsumed(true)475->save();476}477478// If we received a "build" command, all builds are scheduled and we can479// move out of "preparing" into "building".480if ($done_preparing) {481if ($buildable->isPreparing()) {482$buildable483->setBuildableStatus(HarbormasterBuildableStatus::STATUS_BUILDING)484->save();485}486}487488// If we've been informed that the container for the buildable has489// changed, update it.490if ($update_container) {491$object = id(new PhabricatorObjectQuery())492->setViewer($viewer)493->withPHIDs(array($buildable->getBuildablePHID()))494->executeOne();495if ($object) {496$buildable497->setContainerPHID($object->getHarbormasterContainerPHID())498->save();499}500}501502$old = clone $buildable;503504// Don't update the buildable status if we're still preparing builds: more505// builds may still be scheduled shortly, so even if every build we know506// about so far has passed, that doesn't mean the buildable has actually507// passed everything it needs to.508509if (!$buildable->isPreparing()) {510$behavior_key = HarbormasterBuildPlanBehavior::BEHAVIOR_BUILDABLE;511$behavior = HarbormasterBuildPlanBehavior::getBehavior($behavior_key);512513$key_never = HarbormasterBuildPlanBehavior::BUILDABLE_NEVER;514$key_building = HarbormasterBuildPlanBehavior::BUILDABLE_IF_BUILDING;515516$all_pass = true;517$any_fail = false;518foreach ($buildable->getBuilds() as $build) {519$plan = $build->getBuildPlan();520$option = $behavior->getPlanOption($plan);521$option_key = $option->getKey();522523$is_never = ($option_key === $key_never);524$is_building = ($option_key === $key_building);525526// If this build "Never" affects the buildable, ignore it.527if ($is_never) {528continue;529}530531// If this build affects the buildable "If Building", but is already532// complete, ignore it.533if ($is_building && $build->isComplete()) {534continue;535}536537if (!$build->isPassed()) {538$all_pass = false;539}540541if ($build->isComplete() && !$build->isPassed()) {542$any_fail = true;543}544}545546if ($any_fail) {547$new_status = HarbormasterBuildableStatus::STATUS_FAILED;548} else if ($all_pass) {549$new_status = HarbormasterBuildableStatus::STATUS_PASSED;550} else {551$new_status = HarbormasterBuildableStatus::STATUS_BUILDING;552}553554$did_update = ($old->getBuildableStatus() !== $new_status);555if ($did_update) {556$buildable->setBuildableStatus($new_status);557$buildable->save();558}559}560561$lock->unlock();562563// Don't publish anything if we're still preparing builds.564if ($buildable->isPreparing()) {565return;566}567568$this->publishBuildable($old, $buildable);569}570571public function publishBuildable(572HarbormasterBuildable $old,573HarbormasterBuildable $new) {574575$viewer = $this->getViewer();576577// Publish the buildable. We publish buildables even if they haven't578// changed status in Harbormaster because applications may care about579// different things than Harbormaster does. For example, Differential580// does not care about local lint and unit tests when deciding whether581// a revision should move out of draft or not.582583// NOTE: We're publishing both automatic and manual buildables. Buildable584// objects should generally ignore manual buildables, but it's up to them585// to decide.586587$object = id(new PhabricatorObjectQuery())588->setViewer($viewer)589->withPHIDs(array($new->getBuildablePHID()))590->executeOne();591if (!$object) {592return;593}594595$engine = HarbormasterBuildableEngine::newForObject($object, $viewer);596597$daemon_source = PhabricatorContentSource::newForSource(598PhabricatorDaemonContentSource::SOURCECONST);599600$harbormaster_phid = id(new PhabricatorHarbormasterApplication())601->getPHID();602603$engine604->setActingAsPHID($harbormaster_phid)605->setContentSource($daemon_source)606->publishBuildable($old, $new);607}608609private function releaseQueuedArtifacts() {610foreach ($this->artifactReleaseQueue as $key => $artifact) {611$artifact->releaseArtifact();612unset($this->artifactReleaseQueue[$key]);613}614}615616}617618619