@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
at recaptime-dev/main 618 lines 18 kB view raw
1<?php 2 3/** 4 * Moves a build forward by queuing build tasks, canceling or restarting the 5 * build, or failing it in response to task failures. 6 */ 7final class HarbormasterBuildEngine extends Phobject { 8 9 private $build; 10 private $viewer; 11 private $newBuildTargets = array(); 12 private $artifactReleaseQueue = array(); 13 private $forceBuildableUpdate; 14 15 public function setForceBuildableUpdate($force_buildable_update) { 16 $this->forceBuildableUpdate = $force_buildable_update; 17 return $this; 18 } 19 20 public function shouldForceBuildableUpdate() { 21 return $this->forceBuildableUpdate; 22 } 23 24 public function queueNewBuildTarget(HarbormasterBuildTarget $target) { 25 $this->newBuildTargets[] = $target; 26 return $this; 27 } 28 29 public function getNewBuildTargets() { 30 return $this->newBuildTargets; 31 } 32 33 public function setViewer(PhabricatorUser $viewer) { 34 $this->viewer = $viewer; 35 return $this; 36 } 37 38 public function getViewer() { 39 return $this->viewer; 40 } 41 42 public function setBuild(HarbormasterBuild $build) { 43 $this->build = $build; 44 return $this; 45 } 46 47 public function getBuild() { 48 return $this->build; 49 } 50 51 public function continueBuild() { 52 $viewer = $this->getViewer(); 53 $build = $this->getBuild(); 54 55 $lock_key = 'harbormaster.build:'.$build->getID(); 56 $lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15); 57 58 $build->reload(); 59 $old_status = $build->getBuildStatus(); 60 61 try { 62 $this->updateBuild($build); 63 } catch (Exception $ex) { 64 // If any exception is raised, the build is marked as a failure and the 65 // exception is re-thrown (this ensures we don't leave builds in an 66 // inconsistent state). 67 $build->setBuildStatus(HarbormasterBuildStatus::STATUS_ERROR); 68 $build->save(); 69 70 $lock->unlock(); 71 72 $build->releaseAllArtifacts($viewer); 73 74 throw $ex; 75 } 76 77 $lock->unlock(); 78 79 // NOTE: We queue new targets after releasing the lock so that in-process 80 // execution via `bin/harbormaster` does not reenter the locked region. 81 foreach ($this->getNewBuildTargets() as $target) { 82 $task = PhabricatorWorker::scheduleTask( 83 'HarbormasterTargetWorker', 84 array( 85 'targetID' => $target->getID(), 86 ), 87 array( 88 'objectPHID' => $target->getPHID(), 89 )); 90 } 91 92 // If the build changed status, we might need to update the overall status 93 // on the buildable. 94 $new_status = $build->getBuildStatus(); 95 if ($new_status != $old_status || $this->shouldForceBuildableUpdate()) { 96 $this->updateBuildable($build->getBuildable()); 97 } 98 99 $this->releaseQueuedArtifacts(); 100 101 // If we are no longer building for any reason, release all artifacts. 102 if (!$build->isBuilding()) { 103 $build->releaseAllArtifacts($viewer); 104 } 105 } 106 107 private function updateBuild(HarbormasterBuild $build) { 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(); 116 } 117 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 $message_xaction = 132 HarbormasterBuildMessageTransaction::getTransactionTypeForMessageType( 133 $message_type); 134 135 if (!$message_xaction) { 136 continue; 137 } 138 139 $xactions[] = $build->getApplicationTransactionTemplate() 140 ->setAuthorPHID($message->getAuthorPHID()) 141 ->setTransactionType($message_xaction) 142 ->setNewValue($message_type); 143 } 144 145 if (!$xactions) { 146 if ($build->isPending()) { 147 // TODO: This should be a transaction. 148 149 $build->restartBuild($viewer); 150 $build->setBuildStatus(HarbormasterBuildStatus::STATUS_BUILDING); 151 $build->save(); 152 } 153 } 154 155 if ($xactions) { 156 $editor->applyTransactions($build, $xactions); 157 $build->markUnprocessedMessagesAsProcessed(); 158 } 159 160 if ($build->getBuildStatus() == HarbormasterBuildStatus::STATUS_BUILDING) { 161 $this->updateBuildSteps($build); 162 } 163 } 164 165 private function updateBuildSteps(HarbormasterBuild $build) { 166 $all_targets = id(new HarbormasterBuildTargetQuery()) 167 ->setViewer($this->getViewer()) 168 ->withBuildPHIDs(array($build->getPHID())) 169 ->withBuildGenerations(array($build->getBuildGeneration())) 170 ->execute(); 171 172 $this->updateWaitingTargets($all_targets); 173 174 $targets = mgroup($all_targets, 'getBuildStepPHID'); 175 176 $steps = id(new HarbormasterBuildStepQuery()) 177 ->setViewer($this->getViewer()) 178 ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID())) 179 ->execute(); 180 $steps = mpull($steps, null, 'getPHID'); 181 182 // Identify steps which are in various states. 183 184 $queued = array(); 185 $underway = array(); 186 $waiting = array(); 187 $complete = array(); 188 $failed = array(); 189 foreach ($steps as $step) { 190 $step_targets = idx($targets, $step->getPHID(), array()); 191 192 if ($step_targets) { 193 $is_queued = false; 194 195 $is_underway = false; 196 foreach ($step_targets as $target) { 197 if ($target->isUnderway()) { 198 $is_underway = true; 199 break; 200 } 201 } 202 203 $is_waiting = false; 204 foreach ($step_targets as $target) { 205 if ($target->isWaiting()) { 206 $is_waiting = true; 207 break; 208 } 209 } 210 211 $is_complete = true; 212 foreach ($step_targets as $target) { 213 if (!$target->isComplete()) { 214 $is_complete = false; 215 break; 216 } 217 } 218 219 $is_failed = false; 220 foreach ($step_targets as $target) { 221 if ($target->isFailed()) { 222 $is_failed = true; 223 break; 224 } 225 } 226 } else { 227 $is_queued = true; 228 $is_underway = false; 229 $is_waiting = false; 230 $is_complete = false; 231 $is_failed = false; 232 } 233 234 if ($is_queued) { 235 $queued[$step->getPHID()] = true; 236 } 237 238 if ($is_underway) { 239 $underway[$step->getPHID()] = true; 240 } 241 242 if ($is_waiting) { 243 $waiting[$step->getPHID()] = true; 244 } 245 246 if ($is_complete) { 247 $complete[$step->getPHID()] = true; 248 } 249 250 if ($is_failed) { 251 $failed[$step->getPHID()] = true; 252 } 253 } 254 255 // If any step failed, fail the whole build, then bail. 256 if (count($failed)) { 257 $build->setBuildStatus(HarbormasterBuildStatus::STATUS_FAILED); 258 $build->save(); 259 return; 260 } 261 262 // If every step is complete, we're done with this build. Mark it passed 263 // and bail. 264 if (count($complete) == count($steps)) { 265 $build->setBuildStatus(HarbormasterBuildStatus::STATUS_PASSED); 266 $build->save(); 267 return; 268 } 269 270 // Release any artifacts which are not inputs to any remaining build 271 // step. We're done with these, so something else is free to use them. 272 $ongoing_phids = array_keys($queued + $waiting + $underway); 273 $ongoing_steps = array_select_keys($steps, $ongoing_phids); 274 $this->releaseUnusedArtifacts($all_targets, $ongoing_steps); 275 276 // Identify all the steps which are ready to run (because all their 277 // dependencies are complete). 278 279 $runnable = array(); 280 foreach ($steps as $step) { 281 $dependencies = $step->getStepImplementation()->getDependencies($step); 282 283 if (isset($queued[$step->getPHID()])) { 284 $can_run = true; 285 foreach ($dependencies as $dependency) { 286 if (empty($complete[$dependency])) { 287 $can_run = false; 288 break; 289 } 290 } 291 292 if ($can_run) { 293 $runnable[] = $step; 294 } 295 } 296 } 297 298 if (!$runnable && !$waiting && !$underway) { 299 // This means the build is deadlocked, and the user has configured 300 // circular dependencies. 301 $build->setBuildStatus(HarbormasterBuildStatus::STATUS_DEADLOCKED); 302 $build->save(); 303 return; 304 } 305 306 foreach ($runnable as $runnable_step) { 307 $target = HarbormasterBuildTarget::initializeNewBuildTarget( 308 $build, 309 $runnable_step, 310 $build->retrieveVariablesFromBuild()); 311 $target->save(); 312 313 $this->queueNewBuildTarget($target); 314 } 315 } 316 317 318 /** 319 * Release any artifacts which aren't used by any running or waiting steps. 320 * 321 * This releases artifacts as soon as they're no longer used. This can be 322 * particularly relevant when a build uses multiple hosts since it returns 323 * hosts to the pool more quickly. 324 * 325 * @param array<HarbormasterBuildTarget> $targets Targets in the build. 326 * @param array<HarbormasterBuildStep> $steps List of running and waiting 327 * steps. 328 * @return void 329 */ 330 private function releaseUnusedArtifacts(array $targets, array $steps) { 331 assert_instances_of($targets, HarbormasterBuildTarget::class); 332 assert_instances_of($steps, HarbormasterBuildStep::class); 333 334 if (!$targets || !$steps) { 335 return; 336 } 337 338 $target_phids = mpull($targets, 'getPHID'); 339 340 $artifacts = id(new HarbormasterBuildArtifactQuery()) 341 ->setViewer($this->getViewer()) 342 ->withBuildTargetPHIDs($target_phids) 343 ->withIsReleased(false) 344 ->execute(); 345 if (!$artifacts) { 346 return; 347 } 348 349 // Collect all the artifacts that remaining build steps accept as inputs. 350 $must_keep = array(); 351 foreach ($steps as $step) { 352 $inputs = $step->getStepImplementation()->getArtifactInputs(); 353 foreach ($inputs as $input) { 354 $artifact_key = $input['key']; 355 $must_keep[$artifact_key] = true; 356 } 357 } 358 359 // Queue unreleased artifacts which no remaining step uses for immediate 360 // release. 361 foreach ($artifacts as $artifact) { 362 $key = $artifact->getArtifactKey(); 363 if (isset($must_keep[$key])) { 364 continue; 365 } 366 367 $this->artifactReleaseQueue[] = $artifact; 368 } 369 } 370 371 372 /** 373 * Process messages which were sent to these targets, kicking applicable 374 * targets out of "Waiting" and into either "Passed" or "Failed". 375 * 376 * @param array<HarbormasterBuildTarget> $targets List of targets to process. 377 * @return void 378 */ 379 private function updateWaitingTargets(array $targets) { 380 assert_instances_of($targets, HarbormasterBuildTarget::class); 381 382 // We only care about messages for targets which are actually in a waiting 383 // state. 384 $waiting_targets = array(); 385 foreach ($targets as $target) { 386 if ($target->isWaiting()) { 387 $waiting_targets[$target->getPHID()] = $target; 388 } 389 } 390 391 if (!$waiting_targets) { 392 return; 393 } 394 395 $messages = id(new HarbormasterBuildMessageQuery()) 396 ->setViewer($this->getViewer()) 397 ->withReceiverPHIDs(array_keys($waiting_targets)) 398 ->withConsumed(false) 399 ->execute(); 400 401 foreach ($messages as $message) { 402 $target = $waiting_targets[$message->getReceiverPHID()]; 403 404 switch ($message->getType()) { 405 case HarbormasterMessageType::MESSAGE_PASS: 406 $new_status = HarbormasterBuildTarget::STATUS_PASSED; 407 break; 408 case HarbormasterMessageType::MESSAGE_FAIL: 409 $new_status = HarbormasterBuildTarget::STATUS_FAILED; 410 break; 411 case HarbormasterMessageType::MESSAGE_WORK: 412 default: 413 $new_status = null; 414 break; 415 } 416 417 if ($new_status !== null) { 418 $message->setIsConsumed(true); 419 $message->save(); 420 421 $target->setTargetStatus($new_status); 422 423 if ($target->isComplete()) { 424 $target->setDateCompleted(PhabricatorTime::getNow()); 425 } 426 427 $target->save(); 428 } 429 } 430 } 431 432 433 /** 434 * Update the overall status of the buildable this build is attached to. 435 * 436 * After a build changes state (for example, passes or fails) it may affect 437 * the overall state of the associated buildable. Compute the new aggregate 438 * state and save it on the buildable. 439 * 440 * @param HarbormasterBuildable $buildable The buildable to update. 441 * @return void 442 */ 443 public function updateBuildable(HarbormasterBuildable $buildable) { 444 $viewer = $this->getViewer(); 445 446 $lock_key = 'harbormaster.buildable:'.$buildable->getID(); 447 $lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15); 448 449 $buildable = id(new HarbormasterBuildableQuery()) 450 ->setViewer($viewer) 451 ->withIDs(array($buildable->getID())) 452 ->needBuilds(true) 453 ->executeOne(); 454 455 $messages = id(new HarbormasterBuildMessageQuery()) 456 ->setViewer($viewer) 457 ->withReceiverPHIDs(array($buildable->getPHID())) 458 ->withConsumed(false) 459 ->execute(); 460 461 $done_preparing = false; 462 $update_container = false; 463 foreach ($messages as $message) { 464 switch ($message->getType()) { 465 case HarbormasterMessageType::BUILDABLE_BUILD: 466 $done_preparing = true; 467 break; 468 case HarbormasterMessageType::BUILDABLE_CONTAINER: 469 $update_container = true; 470 break; 471 default: 472 break; 473 } 474 475 $message 476 ->setIsConsumed(true) 477 ->save(); 478 } 479 480 // If we received a "build" command, all builds are scheduled and we can 481 // move out of "preparing" into "building". 482 if ($done_preparing) { 483 if ($buildable->isPreparing()) { 484 $buildable 485 ->setBuildableStatus(HarbormasterBuildableStatus::STATUS_BUILDING) 486 ->save(); 487 } 488 } 489 490 // If we've been informed that the container for the buildable has 491 // changed, update it. 492 if ($update_container) { 493 $object = id(new PhabricatorObjectQuery()) 494 ->setViewer($viewer) 495 ->withPHIDs(array($buildable->getBuildablePHID())) 496 ->executeOne(); 497 if ($object) { 498 $buildable 499 ->setContainerPHID($object->getHarbormasterContainerPHID()) 500 ->save(); 501 } 502 } 503 504 $old = clone $buildable; 505 506 // Don't update the buildable status if we're still preparing builds: more 507 // builds may still be scheduled shortly, so even if every build we know 508 // about so far has passed, that doesn't mean the buildable has actually 509 // passed everything it needs to. 510 511 if (!$buildable->isPreparing()) { 512 $behavior_key = HarbormasterBuildPlanBehavior::BEHAVIOR_BUILDABLE; 513 $behavior = HarbormasterBuildPlanBehavior::getBehavior($behavior_key); 514 515 $key_never = HarbormasterBuildPlanBehavior::BUILDABLE_NEVER; 516 $key_building = HarbormasterBuildPlanBehavior::BUILDABLE_IF_BUILDING; 517 518 $all_pass = true; 519 $any_fail = false; 520 foreach ($buildable->getBuilds() as $build) { 521 $plan = $build->getBuildPlan(); 522 $option = $behavior->getPlanOption($plan); 523 $option_key = $option->getKey(); 524 525 $is_never = ($option_key === $key_never); 526 $is_building = ($option_key === $key_building); 527 528 // If this build "Never" affects the buildable, ignore it. 529 if ($is_never) { 530 continue; 531 } 532 533 // If this build affects the buildable "If Building", but is already 534 // complete, ignore it. 535 if ($is_building && $build->isComplete()) { 536 continue; 537 } 538 539 if (!$build->isPassed()) { 540 $all_pass = false; 541 } 542 543 if ($build->isComplete() && !$build->isPassed()) { 544 $any_fail = true; 545 } 546 } 547 548 if ($any_fail) { 549 $new_status = HarbormasterBuildableStatus::STATUS_FAILED; 550 } else if ($all_pass) { 551 $new_status = HarbormasterBuildableStatus::STATUS_PASSED; 552 } else { 553 $new_status = HarbormasterBuildableStatus::STATUS_BUILDING; 554 } 555 556 $did_update = ($old->getBuildableStatus() !== $new_status); 557 if ($did_update) { 558 $buildable->setBuildableStatus($new_status); 559 $buildable->save(); 560 } 561 } 562 563 $lock->unlock(); 564 565 // Don't publish anything if we're still preparing builds. 566 if ($buildable->isPreparing()) { 567 return; 568 } 569 570 $this->publishBuildable($old, $buildable); 571 } 572 573 public function publishBuildable( 574 HarbormasterBuildable $old, 575 HarbormasterBuildable $new) { 576 577 $viewer = $this->getViewer(); 578 579 // Publish the buildable. We publish buildables even if they haven't 580 // changed status in Harbormaster because applications may care about 581 // different things than Harbormaster does. For example, Differential 582 // does not care about local lint and unit tests when deciding whether 583 // a revision should move out of draft or not. 584 585 // NOTE: We're publishing both automatic and manual buildables. Buildable 586 // objects should generally ignore manual buildables, but it's up to them 587 // to decide. 588 589 $object = id(new PhabricatorObjectQuery()) 590 ->setViewer($viewer) 591 ->withPHIDs(array($new->getBuildablePHID())) 592 ->executeOne(); 593 if (!$object) { 594 return; 595 } 596 597 $engine = HarbormasterBuildableEngine::newForObject($object, $viewer); 598 599 $daemon_source = PhabricatorContentSource::newForSource( 600 PhabricatorDaemonContentSource::SOURCECONST); 601 602 $harbormaster_phid = id(new PhabricatorHarbormasterApplication()) 603 ->getPHID(); 604 605 $engine 606 ->setActingAsPHID($harbormaster_phid) 607 ->setContentSource($daemon_source) 608 ->publishBuildable($old, $new); 609 } 610 611 private function releaseQueuedArtifacts() { 612 foreach ($this->artifactReleaseQueue as $key => $artifact) { 613 $artifact->releaseArtifact(); 614 unset($this->artifactReleaseQueue[$key]); 615 } 616 } 617 618}