@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<?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}