diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php index 7fdc68d6dd..16a4aee5c7 100644 --- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php +++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php @@ -1,302 +1,289 @@ key = $key; $this->properties = $properties; } public static function newBuildStatusObject($status) { $spec = self::getBuildStatusSpec($status); return new self($status, $spec); } private function getProperty($key) { if (!array_key_exists($key, $this->properties)) { throw new Exception( pht( 'Attempting to access unknown build status property ("%s").', $key)); } return $this->properties[$key]; } public function isBuilding() { return $this->getProperty('isBuilding'); } public function isPaused() { return ($this->key === self::STATUS_PAUSED); } public function isComplete() { return $this->getProperty('isComplete'); } - public function isPending() { - return $this->getProperty('isPending'); - } - public function isPassed() { return ($this->key === self::STATUS_PASSED); } public function isFailed() { return ($this->key === self::STATUS_FAILED); } public function isAborting() { return ($this->key === self::PENDING_ABORTING); } public function isRestarting() { return ($this->key === self::PENDING_RESTARTING); } public function isResuming() { return ($this->key === self::PENDING_RESUMING); } public function isPausing() { return ($this->key === self::PENDING_PAUSING); } + public function isPending() { + return ($this->key === self::STATUS_PENDING); + } + public function getIconIcon() { return $this->getProperty('icon'); } public function getIconColor() { return $this->getProperty('color'); } public function getName() { return $this->getProperty('name'); } /** * Get a human readable name for a build status constant. * * @param const Build status constant. * @return string Human-readable name. */ public static function getBuildStatusName($status) { $spec = self::getBuildStatusSpec($status); return $spec['name']; } public static function getBuildStatusMap() { $specs = self::getBuildStatusSpecMap(); return ipull($specs, 'name'); } public static function getBuildStatusIcon($status) { $spec = self::getBuildStatusSpec($status); return $spec['icon']; } public static function getBuildStatusColor($status) { $spec = self::getBuildStatusSpec($status); return $spec['color']; } public static function getBuildStatusANSIColor($status) { $spec = self::getBuildStatusSpec($status); return $spec['color.ansi']; } public static function getWaitingStatusConstants() { return array( self::STATUS_INACTIVE, self::STATUS_PENDING, ); } public static function getActiveStatusConstants() { return array( self::STATUS_BUILDING, self::STATUS_PAUSED, ); } public static function getIncompleteStatusConstants() { $map = self::getBuildStatusSpecMap(); $constants = array(); foreach ($map as $constant => $spec) { if (!$spec['isComplete']) { $constants[] = $constant; } } return $constants; } public static function getCompletedStatusConstants() { return array( self::STATUS_PASSED, self::STATUS_FAILED, self::STATUS_ABORTED, self::STATUS_ERROR, self::STATUS_DEADLOCKED, ); } private static function getBuildStatusSpecMap() { return array( self::STATUS_INACTIVE => array( 'name' => pht('Inactive'), 'icon' => 'fa-circle-o', 'color' => 'dark', 'color.ansi' => 'yellow', 'isBuilding' => false, 'isComplete' => false, - 'isPending' => false, ), self::STATUS_PENDING => array( 'name' => pht('Pending'), 'icon' => 'fa-circle-o', 'color' => 'blue', 'color.ansi' => 'yellow', 'isBuilding' => true, 'isComplete' => false, - 'isPending' => false, ), self::STATUS_BUILDING => array( 'name' => pht('Building'), 'icon' => 'fa-chevron-circle-right', 'color' => 'blue', 'color.ansi' => 'yellow', 'isBuilding' => true, 'isComplete' => false, - 'isPending' => false, ), self::STATUS_PASSED => array( 'name' => pht('Passed'), 'icon' => 'fa-check-circle', 'color' => 'green', 'color.ansi' => 'green', 'isBuilding' => false, 'isComplete' => true, - 'isPending' => false, ), self::STATUS_FAILED => array( 'name' => pht('Failed'), 'icon' => 'fa-times-circle', 'color' => 'red', 'color.ansi' => 'red', 'isBuilding' => false, 'isComplete' => true, - 'isPending' => false, ), self::STATUS_ABORTED => array( 'name' => pht('Aborted'), 'icon' => 'fa-minus-circle', 'color' => 'red', 'color.ansi' => 'red', 'isBuilding' => false, 'isComplete' => true, - 'isPending' => false, ), self::STATUS_ERROR => array( 'name' => pht('Unexpected Error'), 'icon' => 'fa-minus-circle', 'color' => 'red', 'color.ansi' => 'red', 'isBuilding' => false, 'isComplete' => true, - 'isPending' => false, ), self::STATUS_PAUSED => array( 'name' => pht('Paused'), 'icon' => 'fa-minus-circle', 'color' => 'dark', 'color.ansi' => 'yellow', 'isBuilding' => false, 'isComplete' => false, - 'isPending' => false, ), self::STATUS_DEADLOCKED => array( 'name' => pht('Deadlocked'), 'icon' => 'fa-exclamation-circle', 'color' => 'red', 'color.ansi' => 'red', 'isBuilding' => false, 'isComplete' => true, - 'isPending' => false, ), self::PENDING_PAUSING => array( 'name' => pht('Pausing'), 'icon' => 'fa-exclamation-triangle', 'color' => 'red', 'color.ansi' => 'red', 'isBuilding' => false, 'isComplete' => false, - 'isPending' => true, ), self::PENDING_RESUMING => array( 'name' => pht('Resuming'), 'icon' => 'fa-exclamation-triangle', 'color' => 'red', 'color.ansi' => 'red', 'isBuilding' => false, 'isComplete' => false, - 'isPending' => true, ), self::PENDING_RESTARTING => array( 'name' => pht('Restarting'), 'icon' => 'fa-exclamation-triangle', 'color' => 'red', 'color.ansi' => 'red', 'isBuilding' => false, 'isComplete' => false, - 'isPending' => true, ), self::PENDING_ABORTING => array( 'name' => pht('Aborting'), 'icon' => 'fa-exclamation-triangle', 'color' => 'red', 'color.ansi' => 'red', 'isBuilding' => false, 'isComplete' => false, - 'isPending' => true, ), ); } private static function getBuildStatusSpec($status) { $map = self::getBuildStatusSpecMap(); if (isset($map[$status])) { return $map[$status]; } return array( 'name' => pht('Unknown ("%s")', $status), 'icon' => 'fa-question-circle', 'color' => 'bluegrey', 'color.ansi' => 'magenta', 'isBuilding' => false, 'isComplete' => false, ); } } diff --git a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php index d4c2a44957..b3bda8ffd2 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php @@ -1,125 +1,109 @@ getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: case HarbormasterBuildTransaction::TYPE_COMMAND: return null; } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: return true; case HarbormasterBuildTransaction::TYPE_COMMAND: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: return; case HarbormasterBuildTransaction::TYPE_COMMAND: return $this->executeBuildCommand($object, $xaction); } return parent::applyCustomInternalTransaction($object, $xaction); } private function executeBuildCommand( HarbormasterBuild $build, HarbormasterBuildTransaction $xaction) { - $command = $xaction->getNewValue(); + $actor = $this->getActor(); + $message_type = $xaction->getNewValue(); - switch ($command) { - case HarbormasterBuildCommand::COMMAND_RESTART: - $issuable = $build->canRestartBuild(); + // TODO: Restore logic that tests if the command can issue without causing + // anything to lapse into an invalid state. This should not be the same + // as the logic which powers the web UI: for example, if an "abort" is + // queued we want to disable "Abort" in the web UI, but should obviously + // process it here. + + switch ($message_type) { + case HarbormasterBuildCommand::COMMAND_ABORT: + // TODO: This should move to external effects, perhaps. + $build->releaseAllArtifacts($actor); + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED); break; - case HarbormasterBuildCommand::COMMAND_PAUSE: - $issuable = $build->canPauseBuild(); + case HarbormasterBuildCommand::COMMAND_RESTART: + $build->restartBuild($actor); + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); break; case HarbormasterBuildCommand::COMMAND_RESUME: - $issuable = $build->canResumeBuild(); + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); break; - case HarbormasterBuildCommand::COMMAND_ABORT: - $issuable = $build->canAbortBuild(); + case HarbormasterBuildCommand::COMMAND_PAUSE: + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED); break; - default: - throw new Exception(pht('Unknown command %s', $command)); - } - - if (!$issuable) { - return; } - - $actor = $this->getActor(); - if (!$build->canIssueCommand($actor, $command)) { - return; - } - - HarbormasterBuildMessage::initializeNewMessage($actor) - ->setAuthorPHID($xaction->getAuthorPHID()) - ->setReceiverPHID($build->getPHID()) - ->setType($command) - ->save(); - - PhabricatorWorker::scheduleTask( - 'HarbormasterBuildWorker', - array( - 'buildID' => $build->getID(), - ), - array( - 'objectPHID' => $build->getPHID(), - )); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: case HarbormasterBuildTransaction::TYPE_COMMAND: return; } return parent::applyCustomExternalTransaction($object, $xaction); } } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php index 60d3040e33..29933a39eb 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -1,629 +1,609 @@ 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() { + $viewer = $this->getViewer(); $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(HarbormasterBuildStatus::STATUS_ERROR); $build->save(); $lock->unlock(); - $this->releaseAllArtifacts($build); + $build->releaseAllArtifacts($viewer); 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(), ), array( 'objectPHID' => $target->getPHID(), )); } // 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()); } $this->releaseQueuedArtifacts(); // If we are no longer building for any reason, release all artifacts. if (!$build->isBuilding()) { - $this->releaseAllArtifacts($build); + $build->releaseAllArtifacts($viewer); } } private function updateBuild(HarbormasterBuild $build) { - if ($build->isAborting()) { - $this->releaseAllArtifacts($build); - $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED); - $build->save(); - } + $viewer = $this->getViewer(); - if (($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_PENDING) || - ($build->isRestarting())) { - $this->restartBuild($build); - $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); - $build->save(); - } + $content_source = PhabricatorContentSource::newForSource( + PhabricatorDaemonContentSource::SOURCECONST); - if ($build->isResuming()) { - $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); - $build->save(); + $acting_phid = $viewer->getPHID(); + if (!$acting_phid) { + $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID(); } - if ($build->isPausing() && !$build->isComplete()) { - $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED); - $build->save(); - } + $editor = $build->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($acting_phid) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); - $build->markUnprocessedMessagesAsProcessed(); + $xactions = array(); - if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) { - $this->updateBuildSteps($build); - } - } + $messages = $build->getUnprocessedMessagesForApply(); + foreach ($messages as $message) { + $message_type = $message->getType(); - private function restartBuild(HarbormasterBuild $build) { + $xactions[] = $build->getApplicationTransactionTemplate() + ->setAuthorPHID($message->getAuthorPHID()) + ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) + ->setNewValue($message_type); + } - // We're restarting the build, so release all previous artifacts. - $this->releaseAllArtifacts($build); + if (!$xactions) { + if ($build->isPending()) { + // TODO: This should be a transaction. - // Increment the build generation counter on the build. - $build->setBuildGeneration($build->getBuildGeneration() + 1); + $build->restartBuild($viewer); + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); + $build->save(); + } + } - // Currently running targets should periodically check their build - // generation (which won't have changed) against the build's generation. - // If it is different, they will automatically stop what they're doing - // and abort. + if ($xactions) { + $editor->applyTransactions($build, $xactions); + $build->markUnprocessedMessagesAsProcessed(); + } - // Previously we used to delete targets, logs and artifacts here. Instead, - // leave them around so users can view previous generations of this build. + if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) { + $this->updateBuildSteps($build); + } } private function updateBuildSteps(HarbormasterBuild $build) { $all_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($this->getViewer()) ->withBuildPHIDs(array($build->getPHID())) ->withBuildGenerations(array($build->getBuildGeneration())) ->execute(); $this->updateWaitingTargets($all_targets); $targets = mgroup($all_targets, 'getBuildStepPHID'); $steps = id(new HarbormasterBuildStepQuery()) ->setViewer($this->getViewer()) ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID())) ->execute(); $steps = mpull($steps, null, 'getPHID'); // 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(HarbormasterBuildStatus::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(HarbormasterBuildStatus::STATUS_PASSED); $build->save(); return; } // Release any artifacts which are not inputs to any remaining build // step. We're done with these, so something else is free to use them. $ongoing_phids = array_keys($queued + $waiting + $underway); $ongoing_steps = array_select_keys($steps, $ongoing_phids); $this->releaseUnusedArtifacts($all_targets, $ongoing_steps); // Identify all the steps which are ready to run (because all their // dependencies are complete). $runnable = array(); foreach ($steps as $step) { $dependencies = $step->getStepImplementation()->getDependencies($step); if (isset($queued[$step->getPHID()])) { $can_run = true; foreach ($dependencies as $dependency) { if (empty($complete[$dependency])) { $can_run = false; break; } } if ($can_run) { $runnable[] = $step; } } } if (!$runnable && !$waiting && !$underway) { // This means the build is deadlocked, and the user has configured // circular dependencies. $build->setBuildStatus(HarbormasterBuildStatus::STATUS_DEADLOCKED); $build->save(); return; } foreach ($runnable as $runnable_step) { $target = HarbormasterBuildTarget::initializeNewBuildTarget( $build, $runnable_step, $build->retrieveVariablesFromBuild()); $target->save(); $this->queueNewBuildTarget($target); } } /** * Release any artifacts which aren't used by any running or waiting steps. * * This releases artifacts as soon as they're no longer used. This can be * particularly relevant when a build uses multiple hosts since it returns * hosts to the pool more quickly. * * @param list Targets in the build. * @param list List of running and waiting steps. * @return void */ private function releaseUnusedArtifacts(array $targets, array $steps) { assert_instances_of($targets, 'HarbormasterBuildTarget'); assert_instances_of($steps, 'HarbormasterBuildStep'); if (!$targets || !$steps) { return; } $target_phids = mpull($targets, 'getPHID'); $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer($this->getViewer()) ->withBuildTargetPHIDs($target_phids) ->withIsReleased(false) ->execute(); if (!$artifacts) { return; } // Collect all the artifacts that remaining build steps accept as inputs. $must_keep = array(); foreach ($steps as $step) { $inputs = $step->getStepImplementation()->getArtifactInputs(); foreach ($inputs as $input) { $artifact_key = $input['key']; $must_keep[$artifact_key] = true; } } // Queue unreleased artifacts which no remaining step uses for immediate // release. foreach ($artifacts as $artifact) { $key = $artifact->getArtifactKey(); if (isset($must_keep[$key])) { continue; } $this->artifactReleaseQueue[] = $artifact; } } /** * 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()) ->withReceiverPHIDs(array_keys($waiting_targets)) ->withConsumed(false) ->execute(); foreach ($messages as $message) { $target = $waiting_targets[$message->getReceiverPHID()]; switch ($message->getType()) { case HarbormasterMessageType::MESSAGE_PASS: $new_status = HarbormasterBuildTarget::STATUS_PASSED; break; case HarbormasterMessageType::MESSAGE_FAIL: $new_status = HarbormasterBuildTarget::STATUS_FAILED; break; case HarbormasterMessageType::MESSAGE_WORK: default: $new_status = null; break; } if ($new_status !== null) { $message->setIsConsumed(true); $message->save(); $target->setTargetStatus($new_status); if ($target->isComplete()) { $target->setDateCompleted(PhabricatorTime::getNow()); } $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 */ public 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(); $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withReceiverPHIDs(array($buildable->getPHID())) ->withConsumed(false) ->execute(); $done_preparing = false; $update_container = false; foreach ($messages as $message) { switch ($message->getType()) { case HarbormasterMessageType::BUILDABLE_BUILD: $done_preparing = true; break; case HarbormasterMessageType::BUILDABLE_CONTAINER: $update_container = true; break; default: break; } $message ->setIsConsumed(true) ->save(); } // If we received a "build" command, all builds are scheduled and we can // move out of "preparing" into "building". if ($done_preparing) { if ($buildable->isPreparing()) { $buildable ->setBuildableStatus(HarbormasterBuildableStatus::STATUS_BUILDING) ->save(); } } // If we've been informed that the container for the buildable has // changed, update it. if ($update_container) { $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($buildable->getBuildablePHID())) ->executeOne(); if ($object) { $buildable ->setContainerPHID($object->getHarbormasterContainerPHID()) ->save(); } } $old = clone $buildable; // Don't update the buildable status if we're still preparing builds: more // builds may still be scheduled shortly, so even if every build we know // about so far has passed, that doesn't mean the buildable has actually // passed everything it needs to. if (!$buildable->isPreparing()) { $behavior_key = HarbormasterBuildPlanBehavior::BEHAVIOR_BUILDABLE; $behavior = HarbormasterBuildPlanBehavior::getBehavior($behavior_key); $key_never = HarbormasterBuildPlanBehavior::BUILDABLE_NEVER; $key_building = HarbormasterBuildPlanBehavior::BUILDABLE_IF_BUILDING; $all_pass = true; $any_fail = false; foreach ($buildable->getBuilds() as $build) { $plan = $build->getBuildPlan(); $option = $behavior->getPlanOption($plan); $option_key = $option->getKey(); $is_never = ($option_key === $key_never); $is_building = ($option_key === $key_building); // If this build "Never" affects the buildable, ignore it. if ($is_never) { continue; } // If this build affects the buildable "If Building", but is already // complete, ignore it. if ($is_building && $build->isComplete()) { continue; } if (!$build->isPassed()) { $all_pass = false; } if ($build->isComplete() && !$build->isPassed()) { $any_fail = true; } } if ($any_fail) { $new_status = HarbormasterBuildableStatus::STATUS_FAILED; } else if ($all_pass) { $new_status = HarbormasterBuildableStatus::STATUS_PASSED; } else { $new_status = HarbormasterBuildableStatus::STATUS_BUILDING; } $did_update = ($old->getBuildableStatus() !== $new_status); if ($did_update) { $buildable->setBuildableStatus($new_status); $buildable->save(); } } $lock->unlock(); // Don't publish anything if we're still preparing builds. if ($buildable->isPreparing()) { return; } $this->publishBuildable($old, $buildable); } public function publishBuildable( HarbormasterBuildable $old, HarbormasterBuildable $new) { $viewer = $this->getViewer(); // Publish the buildable. We publish buildables even if they haven't // changed status in Harbormaster because applications may care about // different things than Harbormaster does. For example, Differential // does not care about local lint and unit tests when deciding whether // a revision should move out of draft or not. // NOTE: We're publishing both automatic and manual buildables. Buildable // objects should generally ignore manual buildables, but it's up to them // to decide. $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($new->getBuildablePHID())) ->executeOne(); if (!$object) { return; } $engine = HarbormasterBuildableEngine::newForObject($object, $viewer); $daemon_source = PhabricatorContentSource::newForSource( PhabricatorDaemonContentSource::SOURCECONST); $harbormaster_phid = id(new PhabricatorHarbormasterApplication()) ->getPHID(); $engine ->setActingAsPHID($harbormaster_phid) ->setContentSource($daemon_source) ->publishBuildable($old, $new); } - private function releaseAllArtifacts(HarbormasterBuild $build) { - $targets = id(new HarbormasterBuildTargetQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withBuildPHIDs(array($build->getPHID())) - ->withBuildGenerations(array($build->getBuildGeneration())) - ->execute(); - - if (count($targets) === 0) { - return; - } - - $target_phids = mpull($targets, 'getPHID'); - - $artifacts = id(new HarbormasterBuildArtifactQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withBuildTargetPHIDs($target_phids) - ->withIsReleased(false) - ->execute(); - foreach ($artifacts as $artifact) { - $artifact->releaseArtifact(); - } - } - private function releaseQueuedArtifacts() { foreach ($this->artifactReleaseQueue as $key => $artifact) { $artifact->releaseArtifact(); unset($this->artifactReleaseQueue[$key]); } } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index a96ab13c9f..5b059bbfcf 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,624 +1,675 @@ setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE) ->setBuildGeneration(0); } public function delete() { $this->openTransaction(); $this->deleteUnprocessedMessages(); $result = parent::delete(); $this->saveTransaction(); return $result; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'buildParameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'buildStatus' => 'text32', 'buildGeneration' => 'uint32', 'planAutoKey' => 'text32?', 'initiatorPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_plan' => array( 'columns' => array('buildPlanPHID'), ), 'key_status' => array( 'columns' => array('buildStatus'), ), 'key_planautokey' => array( 'columns' => array('buildablePHID', 'planAutoKey'), 'unique' => true, ), 'key_initiator' => array( 'columns' => array('initiatorPHID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildPHIDType::TYPECONST); } public function attachBuildable(HarbormasterBuildable $buildable) { $this->buildable = $buildable; return $this; } public function getBuildable() { return $this->assertAttached($this->buildable); } public function getName() { if ($this->getBuildPlan()) { return $this->getBuildPlan()->getName(); } return pht('Build'); } public function attachBuildPlan( HarbormasterBuildPlan $build_plan = null) { $this->buildPlan = $build_plan; return $this; } public function getBuildPlan() { return $this->assertAttached($this->buildPlan); } public function getBuildTargets() { return $this->assertAttached($this->buildTargets); } public function attachBuildTargets(array $targets) { $this->buildTargets = $targets; return $this; } public function isBuilding() { return $this->getBuildStatusObject()->isBuilding(); } public function isAutobuild() { return ($this->getPlanAutoKey() !== null); } public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, 'repository.phid' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, 'build.id' => null, 'initiator.phid' => null, 'buildable.phid' => null, 'buildable.object.phid' => null, 'buildable.container.phid' => null, 'build.phid' => null, ); foreach ($this->getBuildParameters() as $key => $value) { $results['build/'.$key] = $value; } $buildable = $this->getBuildable(); $object = $buildable->getBuildableObject(); $object_variables = $object->getBuildVariables(); $results = $object_variables + $results; $results['step.timestamp'] = time(); $results['build.id'] = $this->getID(); $results['initiator.phid'] = $this->getInitiatorPHID(); $results['buildable.phid'] = $buildable->getPHID(); $results['buildable.object.phid'] = $object->getPHID(); $results['buildable.container.phid'] = $buildable->getContainerPHID(); $results['build.phid'] = $this->getPHID(); return $results; } public static function getAvailableBuildVariables() { $objects = id(new PhutilClassMapQuery()) ->setAncestorClass('HarbormasterBuildableInterface') ->execute(); $variables = array(); $variables[] = array( 'step.timestamp' => pht('The current UNIX timestamp.'), 'build.id' => pht('The ID of the current build.'), 'target.phid' => pht('The PHID of the current build target.'), 'initiator.phid' => pht( 'The PHID of the user or Object that initiated the build, '. 'if applicable.'), 'buildable.phid' => pht( 'The object PHID of the Harbormaster Buildable being built.'), 'buildable.object.phid' => pht( 'The object PHID of the object (usually a diff or commit) '. 'being built.'), 'buildable.container.phid' => pht( 'The object PHID of the container (usually a revision or repository) '. 'for the object being built.'), 'build.phid' => pht( 'The object PHID of the Harbormaster Build being built.'), ); foreach ($objects as $object) { $variables[] = $object->getAvailableBuildVariables(); } $variables = array_mergev($variables); return $variables; } public function isComplete() { return $this->getBuildStatusObject()->isComplete(); } public function isPaused() { return $this->getBuildStatusObject()->isPaused(); } public function isPassed() { return $this->getBuildStatusObject()->isPassed(); } public function isFailed() { return $this->getBuildStatusObject()->isFailed(); } + public function isPending() { + return $this->getBuildstatusObject()->isPending(); + } + public function getURI() { $id = $this->getID(); return "/harbormaster/build/{$id}/"; } public function getBuildPendingStatusObject() { + list($pending_status) = $this->getUnprocessedMessageState(); + + if ($pending_status !== null) { + return HarbormasterBuildStatus::newBuildStatusObject($pending_status); + } + + return $this->getBuildStatusObject(); + } + protected function getBuildStatusObject() { + $status_key = $this->getBuildStatus(); + return HarbormasterBuildStatus::newBuildStatusObject($status_key); + } + + public function getObjectName() { + return pht('Build %d', $this->getID()); + } + + +/* -( Build Messages )----------------------------------------------------- */ + + + private function getUnprocessedMessages() { + return $this->assertAttached($this->unprocessedMessages); + } + + public function getUnprocessedMessagesForApply() { + $unprocessed_state = $this->getUnprocessedMessageState(); + list($pending_status, $apply_messages) = $unprocessed_state; + + return $apply_messages; + } + + private function getUnprocessedMessageState() { // NOTE: If a build has multiple unprocessed messages, we'll ignore // messages that are obsoleted by a later or stronger message. // // For example, if a build has both "pause" and "abort" messages in queue, // we just ignore the "pause" message and perform an "abort", since pausing // first wouldn't affect the final state, so we can just skip it. // // Likewise, if a build has both "restart" and "abort" messages, the most // recent message is controlling: we'll take whichever action a command // was most recently issued for. $is_restarting = false; $is_aborting = false; $is_pausing = false; $is_resuming = false; + $apply_messages = array(); + foreach ($this->getUnprocessedMessages() as $message_object) { $message_type = $message_object->getType(); switch ($message_type) { case HarbormasterBuildCommand::COMMAND_RESTART: $is_restarting = true; $is_aborting = false; + $apply_messages = array($message_object); break; case HarbormasterBuildCommand::COMMAND_ABORT: $is_aborting = true; $is_restarting = false; + $apply_messages = array($message_object); break; case HarbormasterBuildCommand::COMMAND_PAUSE: $is_pausing = true; $is_resuming = false; + $apply_messages = array($message_object); break; case HarbormasterBuildCommand::COMMAND_RESUME: $is_resuming = true; $is_pausing = false; + $apply_messages = array($message_object); break; } } $pending_status = null; if ($is_restarting) { $pending_status = HarbormasterBuildStatus::PENDING_RESTARTING; } else if ($is_aborting) { $pending_status = HarbormasterBuildStatus::PENDING_ABORTING; } else if ($is_pausing) { $pending_status = HarbormasterBuildStatus::PENDING_PAUSING; } else if ($is_resuming) { $pending_status = HarbormasterBuildStatus::PENDING_RESUMING; } - if ($pending_status !== null) { - return HarbormasterBuildStatus::newBuildStatusObject($pending_status); - } - - return $this->getBuildStatusObject(); - } - - protected function getBuildStatusObject() { - $status_key = $this->getBuildStatus(); - return HarbormasterBuildStatus::newBuildStatusObject($status_key); - } - - public function getObjectName() { - return pht('Build %d', $this->getID()); - } - - -/* -( Build Messages )----------------------------------------------------- */ - - - private function getUnprocessedMessages() { - return $this->assertAttached($this->unprocessedMessages); + return array($pending_status, $apply_messages); } public function attachUnprocessedMessages(array $messages) { assert_instances_of($messages, 'HarbormasterBuildMessage'); $this->unprocessedMessages = $messages; return $this; } public function canRestartBuild() { try { $this->assertCanRestartBuild(); return true; } catch (HarbormasterRestartException $ex) { return false; } } public function assertCanRestartBuild() { if ($this->isAutobuild()) { throw new HarbormasterRestartException( pht('Can Not Restart Autobuild'), pht( 'This build can not be restarted because it is an automatic '. 'build.')); } $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE; $plan = $this->getBuildPlan(); // See T13526. Users who can't see the "BuildPlan" can end up here with // no object. This is highly questionable. if (!$plan) { throw new HarbormasterRestartException( pht('No Build Plan Permission'), pht( 'You can not restart this build because you do not have '. 'permission to access the build plan.')); } $option = HarbormasterBuildPlanBehavior::getBehavior($restartable) ->getPlanOption($plan); $option_key = $option->getKey(); $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER; $is_never = ($option_key === $never_restartable); if ($is_never) { throw new HarbormasterRestartException( pht('Build Plan Prevents Restart'), pht( 'This build can not be restarted because the build plan is '. 'configured to prevent the build from restarting.')); } $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED; $is_failed = ($option_key === $failed_restartable); if ($is_failed) { if (!$this->isFailed()) { throw new HarbormasterRestartException( pht('Only Restartable if Failed'), pht( 'This build can not be restarted because the build plan is '. 'configured to prevent the build from restarting unless it '. 'has failed, and it has not failed.')); } } if ($this->isRestarting()) { throw new HarbormasterRestartException( pht('Already Restarting'), pht( 'This build is already restarting. You can not reissue a restart '. 'command to a restarting build.')); } } public function canPauseBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete() && !$this->isPaused() && !$this->isPausing() && !$this->isRestarting() && !$this->isAborting(); } public function canAbortBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete() && !$this->isAborting(); } public function canResumeBuild() { if ($this->isAutobuild()) { return false; } return $this->isPaused() && !$this->isResuming() && !$this->isRestarting() && !$this->isAborting(); } public function isPausing() { return $this->getBuildPendingStatusObject()->isPausing(); } public function isResuming() { return $this->getBuildPendingStatusObject()->isResuming(); } public function isRestarting() { return $this->getBuildPendingStatusObject()->isRestarting(); } public function isAborting() { return $this->getBuildPendingStatusObject()->isAborting(); } public function markUnprocessedMessagesAsProcessed() { foreach ($this->getUnprocessedMessages() as $key => $message_object) { $message_object ->setIsConsumed(1) ->save(); } return $this; } public function deleteUnprocessedMessages() { foreach ($this->getUnprocessedMessages() as $key => $message_object) { $message_object->delete(); unset($this->unprocessedMessages[$key]); } return $this; } public function canIssueCommand(PhabricatorUser $viewer, $command) { try { $this->assertCanIssueCommand($viewer, $command); return true; } catch (Exception $ex) { return false; } } public function assertCanIssueCommand(PhabricatorUser $viewer, $command) { $plan = $this->getBuildPlan(); // See T13526. Users without permission to access the build plan can // currently end up here with no "BuildPlan" object. if (!$plan) { return false; } $need_edit = true; switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: case HarbormasterBuildCommand::COMMAND_PAUSE: case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_ABORT: if ($plan->canRunWithoutEditCapability()) { $need_edit = false; } break; default: throw new Exception( pht( 'Invalid Harbormaster build command "%s".', $command)); } // Issuing these commands requires that you be able to edit the build, to // prevent enemy engineers from sabotaging your builds. See T9614. if ($need_edit) { PhabricatorPolicyFilter::requireCapability( $viewer, $plan, PhabricatorPolicyCapability::CAN_EDIT); } } public function sendMessage(PhabricatorUser $viewer, $message_type) { - // TODO: This should not be an editor transaction, but there are plans to - // merge BuildCommand into BuildMessage which should moot this. As this - // exists today, it can race against BuildEngine. - - // This is a bogus content source, but this whole flow should be obsolete - // soon. - $content_source = PhabricatorContentSource::newForSource( - PhabricatorConsoleContentSource::SOURCECONST); - - $editor = id(new HarbormasterBuildTransactionEditor()) - ->setActor($viewer) - ->setContentSource($content_source) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - - $viewer_phid = $viewer->getPHID(); - if (!$viewer_phid) { - $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID(); - $editor->setActingAsPHID($acting_phid); + HarbormasterBuildMessage::initializeNewMessage($viewer) + ->setReceiverPHID($this->getPHID()) + ->setType($message_type) + ->save(); + + PhabricatorWorker::scheduleTask( + 'HarbormasterBuildWorker', + array( + 'buildID' => $this->getID(), + ), + array( + 'objectPHID' => $this->getPHID(), + 'containerPHID' => $this->getBuildablePHID(), + )); + } + + public function releaseAllArtifacts(PhabricatorUser $viewer) { + $targets = id(new HarbormasterBuildTargetQuery()) + ->setViewer($viewer) + ->withBuildPHIDs(array($this->getPHID())) + ->withBuildGenerations(array($this->getBuildGeneration())) + ->execute(); + + if (!$targets) { + return; } - $xaction = id(new HarbormasterBuildTransaction()) - ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) - ->setNewValue($message_type); + $target_phids = mpull($targets, 'getPHID'); + + $artifacts = id(new HarbormasterBuildArtifactQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs($target_phids) + ->withIsReleased(false) + ->execute(); + foreach ($artifacts as $artifact) { + $artifact->releaseArtifact(); + } + } + + public function restartBuild(PhabricatorUser $viewer) { + // TODO: This should become transactional. + + // We're restarting the build, so release all previous artifacts. + $this->releaseAllArtifacts($viewer); + + // Increment the build generation counter on the build. + $this->setBuildGeneration($this->getBuildGeneration() + 1); + + // Currently running targets should periodically check their build + // generation (which won't have changed) against the build's generation. + // If it is different, they will automatically stop what they're doing + // and abort. - $editor->applyTransactions($this, array($xaction)); + // Previously we used to delete targets, logs and artifacts here. Instead, + // leave them around so users can view previous generations of this build. } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildTransactionEditor(); } public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildable()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildable()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A build inherits policies from its buildable.'); } /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { return array( id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildablePHID') ->setType('phid') ->setDescription(pht('PHID of the object this build is building.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildPlanPHID') ->setType('phid') ->setDescription(pht('PHID of the build plan being run.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('buildStatus') ->setType('map') ->setDescription(pht('The current status of this build.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('initiatorPHID') ->setType('phid') ->setDescription(pht('The person (or thing) that started this build.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('name') ->setType('string') ->setDescription(pht('The name of this build.')), ); } public function getFieldValuesForConduit() { $status = $this->getBuildStatus(); return array( 'buildablePHID' => $this->getBuildablePHID(), 'buildPlanPHID' => $this->getBuildPlanPHID(), 'buildStatus' => array( 'value' => $status, 'name' => HarbormasterBuildStatus::getBuildStatusName($status), 'color.ansi' => HarbormasterBuildStatus::getBuildStatusANSIColor($status), ), 'initiatorPHID' => nonempty($this->getInitiatorPHID(), null), 'name' => $this->getName(), ); } public function getConduitSearchAttachments() { return array( id(new HarbormasterQueryBuildsSearchEngineAttachment()) ->setAttachmentKey('querybuilds'), ); } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $viewer = $engine->getViewer(); $this->openTransaction(); $targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->withBuildPHIDs(array($this->getPHID())) ->execute(); foreach ($targets as $target) { $engine->destroyObject($target); } $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withReceiverPHIDs(array($this->getPHID())) ->execute(); foreach ($messages as $message) { $engine->destroyObject($message); } $this->delete(); $this->saveTransaction(); } }