1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-12-22 21:40:55 +01:00

Correct the flow of edit authority when sending messages to HarbormasterBuild objects

Summary:
Ref T13072. Currently, Harbormaster builds react to messages by applying a transaction inline (which can race) that has no real effect.

Later, the BuildEngine picks up the mesasge and applies a real effect, but this isn't transactional.

This is backwards, and makes it more difficult to transition to ModularTransaction and EditEngine. The desired workflow is:

  - sending a message //just// writes to the message table (and queues a worker to process the message);
  - the BuildEngine processes the message and applies effects in a transactional way.

Force this into at least roughly the right sequence of behaviors. This paves the way for porting to ModularTransaction, which should allow further cleanup.

Test Plan: Paused, resumed, aborted, and restarted a build. Ran BuildWorkers to process the commands, saw builds update appropriately.

Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam

Maniphest Tasks: T13072

Differential Revision: https://secure.phabricator.com/D21687
This commit is contained in:
epriestley 2021-07-15 10:44:15 -07:00
parent 012af00731
commit bf83fffca1
4 changed files with 182 additions and 180 deletions

View file

@ -53,10 +53,6 @@ final class HarbormasterBuildStatus extends Phobject {
return $this->getProperty('isComplete'); return $this->getProperty('isComplete');
} }
public function isPending() {
return $this->getProperty('isPending');
}
public function isPassed() { public function isPassed() {
return ($this->key === self::STATUS_PASSED); return ($this->key === self::STATUS_PASSED);
} }
@ -81,6 +77,10 @@ final class HarbormasterBuildStatus extends Phobject {
return ($this->key === self::PENDING_PAUSING); return ($this->key === self::PENDING_PAUSING);
} }
public function isPending() {
return ($this->key === self::STATUS_PENDING);
}
public function getIconIcon() { public function getIconIcon() {
return $this->getProperty('icon'); return $this->getProperty('icon');
} }
@ -170,7 +170,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'yellow', 'color.ansi' => 'yellow',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => false, 'isComplete' => false,
'isPending' => false,
), ),
self::STATUS_PENDING => array( self::STATUS_PENDING => array(
'name' => pht('Pending'), 'name' => pht('Pending'),
@ -179,7 +178,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'yellow', 'color.ansi' => 'yellow',
'isBuilding' => true, 'isBuilding' => true,
'isComplete' => false, 'isComplete' => false,
'isPending' => false,
), ),
self::STATUS_BUILDING => array( self::STATUS_BUILDING => array(
'name' => pht('Building'), 'name' => pht('Building'),
@ -188,7 +186,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'yellow', 'color.ansi' => 'yellow',
'isBuilding' => true, 'isBuilding' => true,
'isComplete' => false, 'isComplete' => false,
'isPending' => false,
), ),
self::STATUS_PASSED => array( self::STATUS_PASSED => array(
'name' => pht('Passed'), 'name' => pht('Passed'),
@ -197,7 +194,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'green', 'color.ansi' => 'green',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => true, 'isComplete' => true,
'isPending' => false,
), ),
self::STATUS_FAILED => array( self::STATUS_FAILED => array(
'name' => pht('Failed'), 'name' => pht('Failed'),
@ -206,7 +202,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'red', 'color.ansi' => 'red',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => true, 'isComplete' => true,
'isPending' => false,
), ),
self::STATUS_ABORTED => array( self::STATUS_ABORTED => array(
'name' => pht('Aborted'), 'name' => pht('Aborted'),
@ -215,7 +210,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'red', 'color.ansi' => 'red',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => true, 'isComplete' => true,
'isPending' => false,
), ),
self::STATUS_ERROR => array( self::STATUS_ERROR => array(
'name' => pht('Unexpected Error'), 'name' => pht('Unexpected Error'),
@ -224,7 +218,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'red', 'color.ansi' => 'red',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => true, 'isComplete' => true,
'isPending' => false,
), ),
self::STATUS_PAUSED => array( self::STATUS_PAUSED => array(
'name' => pht('Paused'), 'name' => pht('Paused'),
@ -233,7 +226,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'yellow', 'color.ansi' => 'yellow',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => false, 'isComplete' => false,
'isPending' => false,
), ),
self::STATUS_DEADLOCKED => array( self::STATUS_DEADLOCKED => array(
'name' => pht('Deadlocked'), 'name' => pht('Deadlocked'),
@ -242,7 +234,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'red', 'color.ansi' => 'red',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => true, 'isComplete' => true,
'isPending' => false,
), ),
self::PENDING_PAUSING => array( self::PENDING_PAUSING => array(
'name' => pht('Pausing'), 'name' => pht('Pausing'),
@ -251,7 +242,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'red', 'color.ansi' => 'red',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => false, 'isComplete' => false,
'isPending' => true,
), ),
self::PENDING_RESUMING => array( self::PENDING_RESUMING => array(
'name' => pht('Resuming'), 'name' => pht('Resuming'),
@ -260,7 +250,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'red', 'color.ansi' => 'red',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => false, 'isComplete' => false,
'isPending' => true,
), ),
self::PENDING_RESTARTING => array( self::PENDING_RESTARTING => array(
'name' => pht('Restarting'), 'name' => pht('Restarting'),
@ -269,7 +258,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'red', 'color.ansi' => 'red',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => false, 'isComplete' => false,
'isPending' => true,
), ),
self::PENDING_ABORTING => array( self::PENDING_ABORTING => array(
'name' => pht('Aborting'), 'name' => pht('Aborting'),
@ -278,7 +266,6 @@ final class HarbormasterBuildStatus extends Phobject {
'color.ansi' => 'red', 'color.ansi' => 'red',
'isBuilding' => false, 'isBuilding' => false,
'isComplete' => false, 'isComplete' => false,
'isPending' => true,
), ),
); );
} }

View file

@ -65,48 +65,32 @@ final class HarbormasterBuildTransactionEditor
HarbormasterBuild $build, HarbormasterBuild $build,
HarbormasterBuildTransaction $xaction) { HarbormasterBuildTransaction $xaction) {
$command = $xaction->getNewValue(); $actor = $this->getActor();
$message_type = $xaction->getNewValue();
switch ($command) { // TODO: Restore logic that tests if the command can issue without causing
case HarbormasterBuildCommand::COMMAND_RESTART: // anything to lapse into an invalid state. This should not be the same
$issuable = $build->canRestartBuild(); // 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; break;
case HarbormasterBuildCommand::COMMAND_PAUSE: case HarbormasterBuildCommand::COMMAND_RESTART:
$issuable = $build->canPauseBuild(); $build->restartBuild($actor);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
break; break;
case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESUME:
$issuable = $build->canResumeBuild(); $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
break; break;
case HarbormasterBuildCommand::COMMAND_ABORT: case HarbormasterBuildCommand::COMMAND_PAUSE:
$issuable = $build->canAbortBuild(); $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED);
break; 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( protected function applyCustomExternalTransaction(

View file

@ -49,6 +49,7 @@ final class HarbormasterBuildEngine extends Phobject {
} }
public function continueBuild() { public function continueBuild() {
$viewer = $this->getViewer();
$build = $this->getBuild(); $build = $this->getBuild();
$lock_key = 'harbormaster.build:'.$build->getID(); $lock_key = 'harbormaster.build:'.$build->getID();
@ -68,7 +69,7 @@ final class HarbormasterBuildEngine extends Phobject {
$lock->unlock(); $lock->unlock();
$this->releaseAllArtifacts($build); $build->releaseAllArtifacts($viewer);
throw $ex; throw $ex;
} }
@ -99,58 +100,60 @@ final class HarbormasterBuildEngine extends Phobject {
// If we are no longer building for any reason, release all artifacts. // If we are no longer building for any reason, release all artifacts.
if (!$build->isBuilding()) { if (!$build->isBuilding()) {
$this->releaseAllArtifacts($build); $build->releaseAllArtifacts($viewer);
} }
} }
private function updateBuild(HarbormasterBuild $build) { private function updateBuild(HarbormasterBuild $build) {
if ($build->isAborting()) { $viewer = $this->getViewer();
$this->releaseAllArtifacts($build);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED); $content_source = PhabricatorContentSource::newForSource(
$build->save(); PhabricatorDaemonContentSource::SOURCECONST);
$acting_phid = $viewer->getPHID();
if (!$acting_phid) {
$acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
} }
if (($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_PENDING) || $editor = $build->getApplicationTransactionEditor()
($build->isRestarting())) { ->setActor($viewer)
$this->restartBuild($build); ->setActingAsPHID($acting_phid)
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); ->setContentSource($content_source)
$build->save(); ->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true);
$xactions = array();
$messages = $build->getUnprocessedMessagesForApply();
foreach ($messages as $message) {
$message_type = $message->getType();
$xactions[] = $build->getApplicationTransactionTemplate()
->setAuthorPHID($message->getAuthorPHID())
->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
->setNewValue($message_type);
} }
if ($build->isResuming()) { if (!$xactions) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); if ($build->isPending()) {
$build->save(); // TODO: This should be a transaction.
$build->restartBuild($viewer);
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING);
$build->save();
}
} }
if ($build->isPausing() && !$build->isComplete()) { if ($xactions) {
$build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED); $editor->applyTransactions($build, $xactions);
$build->save(); $build->markUnprocessedMessagesAsProcessed();
} }
$build->markUnprocessedMessagesAsProcessed();
if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) { if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) {
$this->updateBuildSteps($build); $this->updateBuildSteps($build);
} }
} }
private function restartBuild(HarbormasterBuild $build) {
// We're restarting the build, so release all previous artifacts.
$this->releaseAllArtifacts($build);
// Increment the build generation counter on the build.
$build->setBuildGeneration($build->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.
// Previously we used to delete targets, logs and artifacts here. Instead,
// leave them around so users can view previous generations of this build.
}
private function updateBuildSteps(HarbormasterBuild $build) { private function updateBuildSteps(HarbormasterBuild $build) {
$all_targets = id(new HarbormasterBuildTargetQuery()) $all_targets = id(new HarbormasterBuildTargetQuery())
->setViewer($this->getViewer()) ->setViewer($this->getViewer())
@ -596,29 +599,6 @@ final class HarbormasterBuildEngine extends Phobject {
->publishBuildable($old, $new); ->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() { private function releaseQueuedArtifacts() {
foreach ($this->artifactReleaseQueue as $key => $artifact) { foreach ($this->artifactReleaseQueue as $key => $artifact) {
$artifact->releaseArtifact(); $artifact->releaseArtifact();

View file

@ -207,61 +207,17 @@ final class HarbormasterBuild extends HarbormasterDAO
return $this->getBuildStatusObject()->isFailed(); return $this->getBuildStatusObject()->isFailed();
} }
public function isPending() {
return $this->getBuildstatusObject()->isPending();
}
public function getURI() { public function getURI() {
$id = $this->getID(); $id = $this->getID();
return "/harbormaster/build/{$id}/"; return "/harbormaster/build/{$id}/";
} }
public function getBuildPendingStatusObject() { public function getBuildPendingStatusObject() {
list($pending_status) = $this->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;
foreach ($this->getUnprocessedMessages() as $message_object) {
$message_type = $message_object->getType();
switch ($message_type) {
case HarbormasterBuildCommand::COMMAND_RESTART:
$is_restarting = true;
$is_aborting = false;
break;
case HarbormasterBuildCommand::COMMAND_ABORT:
$is_aborting = true;
$is_restarting = false;
break;
case HarbormasterBuildCommand::COMMAND_PAUSE:
$is_pausing = true;
$is_resuming = false;
break;
case HarbormasterBuildCommand::COMMAND_RESUME:
$is_resuming = true;
$is_pausing = false;
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) { if ($pending_status !== null) {
return HarbormasterBuildStatus::newBuildStatusObject($pending_status); return HarbormasterBuildStatus::newBuildStatusObject($pending_status);
@ -287,6 +243,72 @@ final class HarbormasterBuild extends HarbormasterDAO
return $this->assertAttached($this->unprocessedMessages); 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;
}
return array($pending_status, $apply_messages);
}
public function attachUnprocessedMessages(array $messages) { public function attachUnprocessedMessages(array $messages) {
assert_instances_of($messages, 'HarbormasterBuildMessage'); assert_instances_of($messages, 'HarbormasterBuildMessage');
$this->unprocessedMessages = $messages; $this->unprocessedMessages = $messages;
@ -475,32 +497,61 @@ final class HarbormasterBuild extends HarbormasterDAO
} }
public function sendMessage(PhabricatorUser $viewer, $message_type) { public function sendMessage(PhabricatorUser $viewer, $message_type) {
// TODO: This should not be an editor transaction, but there are plans to HarbormasterBuildMessage::initializeNewMessage($viewer)
// merge BuildCommand into BuildMessage which should moot this. As this ->setReceiverPHID($this->getPHID())
// exists today, it can race against BuildEngine. ->setType($message_type)
->save();
// This is a bogus content source, but this whole flow should be obsolete PhabricatorWorker::scheduleTask(
// soon. 'HarbormasterBuildWorker',
$content_source = PhabricatorContentSource::newForSource( array(
PhabricatorConsoleContentSource::SOURCECONST); 'buildID' => $this->getID(),
),
array(
'objectPHID' => $this->getPHID(),
'containerPHID' => $this->getBuildablePHID(),
));
}
$editor = id(new HarbormasterBuildTransactionEditor()) public function releaseAllArtifacts(PhabricatorUser $viewer) {
->setActor($viewer) $targets = id(new HarbormasterBuildTargetQuery())
->setContentSource($content_source) ->setViewer($viewer)
->setContinueOnNoEffect(true) ->withBuildPHIDs(array($this->getPHID()))
->setContinueOnMissingFields(true); ->withBuildGenerations(array($this->getBuildGeneration()))
->execute();
$viewer_phid = $viewer->getPHID(); if (!$targets) {
if (!$viewer_phid) { return;
$acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
$editor->setActingAsPHID($acting_phid);
} }
$xaction = id(new HarbormasterBuildTransaction()) $target_phids = mpull($targets, 'getPHID');
->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
->setNewValue($message_type);
$editor->applyTransactions($this, array($xaction)); $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.
// Previously we used to delete targets, logs and artifacts here. Instead,
// leave them around so users can view previous generations of this build.
} }