@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+155 -153
+4 -17
src/applications/harbormaster/constants/HarbormasterBuildStatus.php
··· 53 53 return $this->getProperty('isComplete'); 54 54 } 55 55 56 - public function isPending() { 57 - return $this->getProperty('isPending'); 58 - } 59 - 60 56 public function isPassed() { 61 57 return ($this->key === self::STATUS_PASSED); 62 58 } ··· 79 75 80 76 public function isPausing() { 81 77 return ($this->key === self::PENDING_PAUSING); 78 + } 79 + 80 + public function isPending() { 81 + return ($this->key === self::STATUS_PENDING); 82 82 } 83 83 84 84 public function getIconIcon() { ··· 170 170 'color.ansi' => 'yellow', 171 171 'isBuilding' => false, 172 172 'isComplete' => false, 173 - 'isPending' => false, 174 173 ), 175 174 self::STATUS_PENDING => array( 176 175 'name' => pht('Pending'), ··· 179 178 'color.ansi' => 'yellow', 180 179 'isBuilding' => true, 181 180 'isComplete' => false, 182 - 'isPending' => false, 183 181 ), 184 182 self::STATUS_BUILDING => array( 185 183 'name' => pht('Building'), ··· 188 186 'color.ansi' => 'yellow', 189 187 'isBuilding' => true, 190 188 'isComplete' => false, 191 - 'isPending' => false, 192 189 ), 193 190 self::STATUS_PASSED => array( 194 191 'name' => pht('Passed'), ··· 197 194 'color.ansi' => 'green', 198 195 'isBuilding' => false, 199 196 'isComplete' => true, 200 - 'isPending' => false, 201 197 ), 202 198 self::STATUS_FAILED => array( 203 199 'name' => pht('Failed'), ··· 206 202 'color.ansi' => 'red', 207 203 'isBuilding' => false, 208 204 'isComplete' => true, 209 - 'isPending' => false, 210 205 ), 211 206 self::STATUS_ABORTED => array( 212 207 'name' => pht('Aborted'), ··· 215 210 'color.ansi' => 'red', 216 211 'isBuilding' => false, 217 212 'isComplete' => true, 218 - 'isPending' => false, 219 213 ), 220 214 self::STATUS_ERROR => array( 221 215 'name' => pht('Unexpected Error'), ··· 224 218 'color.ansi' => 'red', 225 219 'isBuilding' => false, 226 220 'isComplete' => true, 227 - 'isPending' => false, 228 221 ), 229 222 self::STATUS_PAUSED => array( 230 223 'name' => pht('Paused'), ··· 233 226 'color.ansi' => 'yellow', 234 227 'isBuilding' => false, 235 228 'isComplete' => false, 236 - 'isPending' => false, 237 229 ), 238 230 self::STATUS_DEADLOCKED => array( 239 231 'name' => pht('Deadlocked'), ··· 242 234 'color.ansi' => 'red', 243 235 'isBuilding' => false, 244 236 'isComplete' => true, 245 - 'isPending' => false, 246 237 ), 247 238 self::PENDING_PAUSING => array( 248 239 'name' => pht('Pausing'), ··· 251 242 'color.ansi' => 'red', 252 243 'isBuilding' => false, 253 244 'isComplete' => false, 254 - 'isPending' => true, 255 245 ), 256 246 self::PENDING_RESUMING => array( 257 247 'name' => pht('Resuming'), ··· 260 250 'color.ansi' => 'red', 261 251 'isBuilding' => false, 262 252 'isComplete' => false, 263 - 'isPending' => true, 264 253 ), 265 254 self::PENDING_RESTARTING => array( 266 255 'name' => pht('Restarting'), ··· 269 258 'color.ansi' => 'red', 270 259 'isBuilding' => false, 271 260 'isComplete' => false, 272 - 'isPending' => true, 273 261 ), 274 262 self::PENDING_ABORTING => array( 275 263 'name' => pht('Aborting'), ··· 278 266 'color.ansi' => 'red', 279 267 'isBuilding' => false, 280 268 'isComplete' => false, 281 - 'isPending' => true, 282 269 ), 283 270 ); 284 271 }
+19 -35
src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php
··· 65 65 HarbormasterBuild $build, 66 66 HarbormasterBuildTransaction $xaction) { 67 67 68 - $command = $xaction->getNewValue(); 68 + $actor = $this->getActor(); 69 + $message_type = $xaction->getNewValue(); 69 70 70 - switch ($command) { 71 + // TODO: Restore logic that tests if the command can issue without causing 72 + // anything to lapse into an invalid state. This should not be the same 73 + // as the logic which powers the web UI: for example, if an "abort" is 74 + // queued we want to disable "Abort" in the web UI, but should obviously 75 + // process it here. 76 + 77 + switch ($message_type) { 78 + case HarbormasterBuildCommand::COMMAND_ABORT: 79 + // TODO: This should move to external effects, perhaps. 80 + $build->releaseAllArtifacts($actor); 81 + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED); 82 + break; 71 83 case HarbormasterBuildCommand::COMMAND_RESTART: 72 - $issuable = $build->canRestartBuild(); 73 - break; 74 - case HarbormasterBuildCommand::COMMAND_PAUSE: 75 - $issuable = $build->canPauseBuild(); 84 + $build->restartBuild($actor); 85 + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); 76 86 break; 77 87 case HarbormasterBuildCommand::COMMAND_RESUME: 78 - $issuable = $build->canResumeBuild(); 88 + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); 79 89 break; 80 - case HarbormasterBuildCommand::COMMAND_ABORT: 81 - $issuable = $build->canAbortBuild(); 90 + case HarbormasterBuildCommand::COMMAND_PAUSE: 91 + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED); 82 92 break; 83 - default: 84 - throw new Exception(pht('Unknown command %s', $command)); 85 - } 86 - 87 - if (!$issuable) { 88 - return; 89 93 } 90 - 91 - $actor = $this->getActor(); 92 - if (!$build->canIssueCommand($actor, $command)) { 93 - return; 94 - } 95 - 96 - HarbormasterBuildMessage::initializeNewMessage($actor) 97 - ->setAuthorPHID($xaction->getAuthorPHID()) 98 - ->setReceiverPHID($build->getPHID()) 99 - ->setType($command) 100 - ->save(); 101 - 102 - PhabricatorWorker::scheduleTask( 103 - 'HarbormasterBuildWorker', 104 - array( 105 - 'buildID' => $build->getID(), 106 - ), 107 - array( 108 - 'objectPHID' => $build->getPHID(), 109 - )); 110 94 } 111 95 112 96 protected function applyCustomExternalTransaction(
+39 -59
src/applications/harbormaster/engine/HarbormasterBuildEngine.php
··· 49 49 } 50 50 51 51 public function continueBuild() { 52 + $viewer = $this->getViewer(); 52 53 $build = $this->getBuild(); 53 54 54 55 $lock_key = 'harbormaster.build:'.$build->getID(); ··· 68 69 69 70 $lock->unlock(); 70 71 71 - $this->releaseAllArtifacts($build); 72 + $build->releaseAllArtifacts($viewer); 72 73 73 74 throw $ex; 74 75 } ··· 99 100 100 101 // If we are no longer building for any reason, release all artifacts. 101 102 if (!$build->isBuilding()) { 102 - $this->releaseAllArtifacts($build); 103 + $build->releaseAllArtifacts($viewer); 103 104 } 104 105 } 105 106 106 107 private function updateBuild(HarbormasterBuild $build) { 107 - if ($build->isAborting()) { 108 - $this->releaseAllArtifacts($build); 109 - $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ABORTED); 110 - $build->save(); 108 + $viewer = $this->getViewer(); 109 + 110 + $content_source = PhabricatorContentSource::newForSource( 111 + PhabricatorDaemonContentSource::SOURCECONST); 112 + 113 + $acting_phid = $viewer->getPHID(); 114 + if (!$acting_phid) { 115 + $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID(); 111 116 } 112 117 113 - if (($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_PENDING) || 114 - ($build->isRestarting())) { 115 - $this->restartBuild($build); 116 - $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); 117 - $build->save(); 118 + $editor = $build->getApplicationTransactionEditor() 119 + ->setActor($viewer) 120 + ->setActingAsPHID($acting_phid) 121 + ->setContentSource($content_source) 122 + ->setContinueOnNoEffect(true) 123 + ->setContinueOnMissingFields(true); 124 + 125 + $xactions = array(); 126 + 127 + $messages = $build->getUnprocessedMessagesForApply(); 128 + foreach ($messages as $message) { 129 + $message_type = $message->getType(); 130 + 131 + $xactions[] = $build->getApplicationTransactionTemplate() 132 + ->setAuthorPHID($message->getAuthorPHID()) 133 + ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) 134 + ->setNewValue($message_type); 118 135 } 119 136 120 - if ($build->isResuming()) { 121 - $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); 122 - $build->save(); 137 + if (!$xactions) { 138 + if ($build->isPending()) { 139 + // TODO: This should be a transaction. 140 + 141 + $build->restartBuild($viewer); 142 + $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); 143 + $build->save(); 144 + } 123 145 } 124 146 125 - if ($build->isPausing() && !$build->isComplete()) { 126 - $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PAUSED); 127 - $build->save(); 147 + if ($xactions) { 148 + $editor->applyTransactions($build, $xactions); 149 + $build->markUnprocessedMessagesAsProcessed(); 128 150 } 129 151 130 - $build->markUnprocessedMessagesAsProcessed(); 131 - 132 152 if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) { 133 153 $this->updateBuildSteps($build); 134 154 } 135 - } 136 - 137 - private function restartBuild(HarbormasterBuild $build) { 138 - 139 - // We're restarting the build, so release all previous artifacts. 140 - $this->releaseAllArtifacts($build); 141 - 142 - // Increment the build generation counter on the build. 143 - $build->setBuildGeneration($build->getBuildGeneration() + 1); 144 - 145 - // Currently running targets should periodically check their build 146 - // generation (which won't have changed) against the build's generation. 147 - // If it is different, they will automatically stop what they're doing 148 - // and abort. 149 - 150 - // Previously we used to delete targets, logs and artifacts here. Instead, 151 - // leave them around so users can view previous generations of this build. 152 155 } 153 156 154 157 private function updateBuildSteps(HarbormasterBuild $build) { ··· 594 597 ->setActingAsPHID($harbormaster_phid) 595 598 ->setContentSource($daemon_source) 596 599 ->publishBuildable($old, $new); 597 - } 598 - 599 - private function releaseAllArtifacts(HarbormasterBuild $build) { 600 - $targets = id(new HarbormasterBuildTargetQuery()) 601 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 602 - ->withBuildPHIDs(array($build->getPHID())) 603 - ->withBuildGenerations(array($build->getBuildGeneration())) 604 - ->execute(); 605 - 606 - if (count($targets) === 0) { 607 - return; 608 - } 609 - 610 - $target_phids = mpull($targets, 'getPHID'); 611 - 612 - $artifacts = id(new HarbormasterBuildArtifactQuery()) 613 - ->setViewer(PhabricatorUser::getOmnipotentUser()) 614 - ->withBuildTargetPHIDs($target_phids) 615 - ->withIsReleased(false) 616 - ->execute(); 617 - foreach ($artifacts as $artifact) { 618 - $artifact->releaseArtifact(); 619 - } 620 600 } 621 601 622 602 private function releaseQueuedArtifacts() {
+93 -42
src/applications/harbormaster/storage/build/HarbormasterBuild.php
··· 207 207 return $this->getBuildStatusObject()->isFailed(); 208 208 } 209 209 210 + public function isPending() { 211 + return $this->getBuildstatusObject()->isPending(); 212 + } 213 + 210 214 public function getURI() { 211 215 $id = $this->getID(); 212 216 return "/harbormaster/build/{$id}/"; 213 217 } 214 218 215 219 public function getBuildPendingStatusObject() { 220 + list($pending_status) = $this->getUnprocessedMessageState(); 221 + 222 + if ($pending_status !== null) { 223 + return HarbormasterBuildStatus::newBuildStatusObject($pending_status); 224 + } 225 + 226 + return $this->getBuildStatusObject(); 227 + } 228 + 229 + protected function getBuildStatusObject() { 230 + $status_key = $this->getBuildStatus(); 231 + return HarbormasterBuildStatus::newBuildStatusObject($status_key); 232 + } 233 + 234 + public function getObjectName() { 235 + return pht('Build %d', $this->getID()); 236 + } 237 + 238 + 239 + /* -( Build Messages )----------------------------------------------------- */ 240 + 241 + 242 + private function getUnprocessedMessages() { 243 + return $this->assertAttached($this->unprocessedMessages); 244 + } 245 + 246 + public function getUnprocessedMessagesForApply() { 247 + $unprocessed_state = $this->getUnprocessedMessageState(); 248 + list($pending_status, $apply_messages) = $unprocessed_state; 216 249 250 + return $apply_messages; 251 + } 252 + 253 + private function getUnprocessedMessageState() { 217 254 // NOTE: If a build has multiple unprocessed messages, we'll ignore 218 255 // messages that are obsoleted by a later or stronger message. 219 256 // ··· 230 267 $is_pausing = false; 231 268 $is_resuming = false; 232 269 270 + $apply_messages = array(); 271 + 233 272 foreach ($this->getUnprocessedMessages() as $message_object) { 234 273 $message_type = $message_object->getType(); 235 274 switch ($message_type) { 236 275 case HarbormasterBuildCommand::COMMAND_RESTART: 237 276 $is_restarting = true; 238 277 $is_aborting = false; 278 + $apply_messages = array($message_object); 239 279 break; 240 280 case HarbormasterBuildCommand::COMMAND_ABORT: 241 281 $is_aborting = true; 242 282 $is_restarting = false; 283 + $apply_messages = array($message_object); 243 284 break; 244 285 case HarbormasterBuildCommand::COMMAND_PAUSE: 245 286 $is_pausing = true; 246 287 $is_resuming = false; 288 + $apply_messages = array($message_object); 247 289 break; 248 290 case HarbormasterBuildCommand::COMMAND_RESUME: 249 291 $is_resuming = true; 250 292 $is_pausing = false; 293 + $apply_messages = array($message_object); 251 294 break; 252 295 } 253 296 } ··· 263 306 $pending_status = HarbormasterBuildStatus::PENDING_RESUMING; 264 307 } 265 308 266 - if ($pending_status !== null) { 267 - return HarbormasterBuildStatus::newBuildStatusObject($pending_status); 268 - } 269 - 270 - return $this->getBuildStatusObject(); 271 - } 272 - 273 - protected function getBuildStatusObject() { 274 - $status_key = $this->getBuildStatus(); 275 - return HarbormasterBuildStatus::newBuildStatusObject($status_key); 276 - } 277 - 278 - public function getObjectName() { 279 - return pht('Build %d', $this->getID()); 280 - } 281 - 282 - 283 - /* -( Build Messages )----------------------------------------------------- */ 284 - 285 - 286 - private function getUnprocessedMessages() { 287 - return $this->assertAttached($this->unprocessedMessages); 309 + return array($pending_status, $apply_messages); 288 310 } 289 311 290 312 public function attachUnprocessedMessages(array $messages) { ··· 475 497 } 476 498 477 499 public function sendMessage(PhabricatorUser $viewer, $message_type) { 478 - // TODO: This should not be an editor transaction, but there are plans to 479 - // merge BuildCommand into BuildMessage which should moot this. As this 480 - // exists today, it can race against BuildEngine. 500 + HarbormasterBuildMessage::initializeNewMessage($viewer) 501 + ->setReceiverPHID($this->getPHID()) 502 + ->setType($message_type) 503 + ->save(); 504 + 505 + PhabricatorWorker::scheduleTask( 506 + 'HarbormasterBuildWorker', 507 + array( 508 + 'buildID' => $this->getID(), 509 + ), 510 + array( 511 + 'objectPHID' => $this->getPHID(), 512 + 'containerPHID' => $this->getBuildablePHID(), 513 + )); 514 + } 515 + 516 + public function releaseAllArtifacts(PhabricatorUser $viewer) { 517 + $targets = id(new HarbormasterBuildTargetQuery()) 518 + ->setViewer($viewer) 519 + ->withBuildPHIDs(array($this->getPHID())) 520 + ->withBuildGenerations(array($this->getBuildGeneration())) 521 + ->execute(); 481 522 482 - // This is a bogus content source, but this whole flow should be obsolete 483 - // soon. 484 - $content_source = PhabricatorContentSource::newForSource( 485 - PhabricatorConsoleContentSource::SOURCECONST); 523 + if (!$targets) { 524 + return; 525 + } 486 526 487 - $editor = id(new HarbormasterBuildTransactionEditor()) 488 - ->setActor($viewer) 489 - ->setContentSource($content_source) 490 - ->setContinueOnNoEffect(true) 491 - ->setContinueOnMissingFields(true); 527 + $target_phids = mpull($targets, 'getPHID'); 492 528 493 - $viewer_phid = $viewer->getPHID(); 494 - if (!$viewer_phid) { 495 - $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID(); 496 - $editor->setActingAsPHID($acting_phid); 529 + $artifacts = id(new HarbormasterBuildArtifactQuery()) 530 + ->setViewer($viewer) 531 + ->withBuildTargetPHIDs($target_phids) 532 + ->withIsReleased(false) 533 + ->execute(); 534 + foreach ($artifacts as $artifact) { 535 + $artifact->releaseArtifact(); 497 536 } 537 + } 498 538 499 - $xaction = id(new HarbormasterBuildTransaction()) 500 - ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) 501 - ->setNewValue($message_type); 539 + public function restartBuild(PhabricatorUser $viewer) { 540 + // TODO: This should become transactional. 541 + 542 + // We're restarting the build, so release all previous artifacts. 543 + $this->releaseAllArtifacts($viewer); 544 + 545 + // Increment the build generation counter on the build. 546 + $this->setBuildGeneration($this->getBuildGeneration() + 1); 547 + 548 + // Currently running targets should periodically check their build 549 + // generation (which won't have changed) against the build's generation. 550 + // If it is different, they will automatically stop what they're doing 551 + // and abort. 502 552 503 - $editor->applyTransactions($this, array($xaction)); 553 + // Previously we used to delete targets, logs and artifacts here. Instead, 554 + // leave them around so users can view previous generations of this build. 504 555 } 505 556 506 557