| 
 | 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 | + */  | 
 | 7 | +final class HarbormasterBuildEngine extends Phobject {  | 
 | 8 | + | 
 | 9 | +  private $build;  | 
 | 10 | +  private $viewer;  | 
 | 11 | +  private $newBuildTargets = array();  | 
 | 12 | + | 
 | 13 | +  public function queueNewBuildTarget(HarbormasterBuildTarget $target) {  | 
 | 14 | +    $this->newBuildTargets[] = $target;  | 
 | 15 | +    return $this;  | 
 | 16 | +  }  | 
 | 17 | + | 
 | 18 | +  public function getNewBuildTargets() {  | 
 | 19 | +    return $this->newBuildTargets;  | 
 | 20 | +  }  | 
 | 21 | + | 
 | 22 | +  public function setViewer(PhabricatorUser $viewer) {  | 
 | 23 | +    $this->viewer = $viewer;  | 
 | 24 | +    return $this;  | 
 | 25 | +  }  | 
 | 26 | + | 
 | 27 | +  public function getViewer() {  | 
 | 28 | +    return $this->viewer;  | 
 | 29 | +  }  | 
 | 30 | + | 
 | 31 | +  public function setBuild(HarbormasterBuild $build) {  | 
 | 32 | +    $this->build = $build;  | 
 | 33 | +    return $this;  | 
 | 34 | +  }  | 
 | 35 | + | 
 | 36 | +  public function getBuild() {  | 
 | 37 | +    return $this->build;  | 
 | 38 | +  }  | 
 | 39 | + | 
 | 40 | +  public function continueBuild() {  | 
 | 41 | +    $build = $this->getBuild();  | 
 | 42 | + | 
 | 43 | +    $lock_key = 'harbormaster.build:'.$build->getID();  | 
 | 44 | +    $lock = PhabricatorGlobalLock::newLock($lock_key)->lock(15);  | 
 | 45 | + | 
 | 46 | +    $build->reload();  | 
 | 47 | + | 
 | 48 | +    try {  | 
 | 49 | +      $this->updateBuild($build);  | 
 | 50 | +    } catch (Exception $ex) {  | 
 | 51 | +      // If any exception is raised, the build is marked as a failure and the  | 
 | 52 | +      // exception is re-thrown (this ensures we don't leave builds in an  | 
 | 53 | +      // inconsistent state).  | 
 | 54 | +      $build->setBuildStatus(HarbormasterBuild::STATUS_ERROR);  | 
 | 55 | +      $build->save();  | 
 | 56 | + | 
 | 57 | +      $lock->unlock();  | 
 | 58 | +      throw $ex;  | 
 | 59 | +    }  | 
 | 60 | + | 
 | 61 | +    $lock->unlock();  | 
 | 62 | + | 
 | 63 | +    // NOTE: We queue new targets after releasing the lock so that in-process  | 
 | 64 | +    // execution via `bin/harbormaster` does not reenter the locked region.  | 
 | 65 | +    foreach ($this->getNewBuildTargets() as $target) {  | 
 | 66 | +      $task = PhabricatorWorker::scheduleTask(  | 
 | 67 | +        'HarbormasterTargetWorker',  | 
 | 68 | +        array(  | 
 | 69 | +          'targetID' => $target->getID(),  | 
 | 70 | +        ));  | 
 | 71 | +    }  | 
 | 72 | +  }  | 
 | 73 | + | 
 | 74 | +  private function updateBuild(HarbormasterBuild $build) {  | 
 | 75 | +    // TODO: Handle cancellation and restarts.  | 
 | 76 | + | 
 | 77 | +    if ($build->getBuildStatus() == HarbormasterBuild::STATUS_PENDING) {  | 
 | 78 | +      $this->destroyBuildTargets($build);  | 
 | 79 | +      $build->setBuildStatus(HarbormasterBuild::STATUS_BUILDING);  | 
 | 80 | +      $build->save();  | 
 | 81 | +    }  | 
 | 82 | + | 
 | 83 | +    if ($build->getBuildStatus() == HarbormasterBuild::STATUS_BUILDING) {  | 
 | 84 | +      return $this->updateBuildSteps($build);  | 
 | 85 | +    }  | 
 | 86 | +  }  | 
 | 87 | + | 
 | 88 | +  private function destroyBuildTargets(HarbormasterBuild $build) {  | 
 | 89 | +    $targets = id(new HarbormasterBuildTargetQuery())  | 
 | 90 | +      ->setViewer($this->getViewer())  | 
 | 91 | +      ->withBuildPHIDs(array($build->getPHID()))  | 
 | 92 | +      ->execute();  | 
 | 93 | +    foreach ($targets as $target) {  | 
 | 94 | +      $target->delete();  | 
 | 95 | +    }  | 
 | 96 | +  }  | 
 | 97 | + | 
 | 98 | +  private function updateBuildSteps(HarbormasterBuild $build) {  | 
 | 99 | +    $targets = id(new HarbormasterBuildTargetQuery())  | 
 | 100 | +      ->setViewer($this->getViewer())  | 
 | 101 | +      ->withBuildPHIDs(array($build->getPHID()))  | 
 | 102 | +      ->execute();  | 
 | 103 | +    $targets = mgroup($targets, 'getBuildStepPHID');  | 
 | 104 | + | 
 | 105 | +    $steps = id(new HarbormasterBuildStepQuery())  | 
 | 106 | +      ->setViewer($this->getViewer())  | 
 | 107 | +      ->withBuildPlanPHIDs(array($build->getBuildPlan()->getPHID()))  | 
 | 108 | +      ->execute();  | 
 | 109 | + | 
 | 110 | +    // Identify steps which are complete.  | 
 | 111 | + | 
 | 112 | +    $complete = array();  | 
 | 113 | +    $failed = array();  | 
 | 114 | +    $waiting = array();  | 
 | 115 | +    foreach ($steps as $step) {  | 
 | 116 | +      $step_targets = idx($targets, $step->getPHID(), array());  | 
 | 117 | + | 
 | 118 | +      if ($step_targets) {  | 
 | 119 | +        $is_complete = true;  | 
 | 120 | +        foreach ($step_targets as $target) {  | 
 | 121 | +          // TODO: Move this to a top-level "status" field on BuildTarget.  | 
 | 122 | +          if (!$target->getDetail('__done__')) {  | 
 | 123 | +            $is_complete = false;  | 
 | 124 | +            break;  | 
 | 125 | +          }  | 
 | 126 | +        }  | 
 | 127 | + | 
 | 128 | +        $is_failed = false;  | 
 | 129 | +        foreach ($step_targets as $target) {  | 
 | 130 | +          // TODO: Move this to a top-level "status" field on BuildTarget.  | 
 | 131 | +          if ($target->getDetail('__failed__')) {  | 
 | 132 | +            $is_failed = true;  | 
 | 133 | +            break;  | 
 | 134 | +          }  | 
 | 135 | +        }  | 
 | 136 | + | 
 | 137 | +        $is_waiting = false;  | 
 | 138 | +      } else {  | 
 | 139 | +        $is_complete = false;  | 
 | 140 | +        $is_failed = false;  | 
 | 141 | +        $is_waiting = true;  | 
 | 142 | +      }  | 
 | 143 | + | 
 | 144 | +      if ($is_complete) {  | 
 | 145 | +        $complete[$step->getPHID()] = true;  | 
 | 146 | +      }  | 
 | 147 | + | 
 | 148 | +      if ($is_failed) {  | 
 | 149 | +        $failed[$step->getPHID()] = true;  | 
 | 150 | +      }  | 
 | 151 | + | 
 | 152 | +      if ($is_waiting) {  | 
 | 153 | +        $waiting[$step->getPHID()] = true;  | 
 | 154 | +      }  | 
 | 155 | +    }  | 
 | 156 | + | 
 | 157 | +    // If every step is complete, we're done with this build. Mark it passed  | 
 | 158 | +    // and bail.  | 
 | 159 | +    if (count($complete) == count($steps)) {  | 
 | 160 | +      $build->setBuildStatus(HarbormasterBuild::STATUS_PASSED);  | 
 | 161 | +      $build->save();  | 
 | 162 | +      return;  | 
 | 163 | +    }  | 
 | 164 | + | 
 | 165 | +    // If any step failed, fail the whole build, then bail.  | 
 | 166 | +    if (count($failed)) {  | 
 | 167 | +      $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED);  | 
 | 168 | +      $build->save();  | 
 | 169 | +      return;  | 
 | 170 | +    }  | 
 | 171 | + | 
 | 172 | +    // Identify all the steps which are ready to run (because all their  | 
 | 173 | +    // depdendencies are complete).  | 
 | 174 | + | 
 | 175 | +    $previous_step = null;  | 
 | 176 | +    $runnable = array();  | 
 | 177 | +    foreach ($steps as $step) {  | 
 | 178 | +      // TODO: For now, we're hard coding sequential dependencies into build  | 
 | 179 | +      // steps. In the future, we can be smart about this instead.  | 
 | 180 | + | 
 | 181 | +      if ($previous_step) {  | 
 | 182 | +        $dependencies = array($previous_step);  | 
 | 183 | +      } else {  | 
 | 184 | +        $dependencies = array();  | 
 | 185 | +      }  | 
 | 186 | + | 
 | 187 | +      if (isset($waiting[$step->getPHID()])) {  | 
 | 188 | +        $can_run = true;  | 
 | 189 | +        foreach ($dependencies as $dependency) {  | 
 | 190 | +          if (empty($complete[$dependency->getPHID()])) {  | 
 | 191 | +            $can_run = false;  | 
 | 192 | +            break;  | 
 | 193 | +          }  | 
 | 194 | +        }  | 
 | 195 | + | 
 | 196 | +        if ($can_run) {  | 
 | 197 | +          $runnable[] = $step;  | 
 | 198 | +        }  | 
 | 199 | +      }  | 
 | 200 | + | 
 | 201 | +      $previous_step = $step;  | 
 | 202 | +    }  | 
 | 203 | + | 
 | 204 | +    if (!$runnable) {  | 
 | 205 | +      // TODO: This means the build is deadlocked, probably? It should not  | 
 | 206 | +      // normally be possible, but we should communicate it more clearly.  | 
 | 207 | +      $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED);  | 
 | 208 | +      $build->save();  | 
 | 209 | +      return;  | 
 | 210 | +    }  | 
 | 211 | + | 
 | 212 | +    foreach ($runnable as $runnable_step) {  | 
 | 213 | +      $target = HarbormasterBuildTarget::initializeNewBuildTarget(  | 
 | 214 | +        $build,  | 
 | 215 | +        $step,  | 
 | 216 | +        $build->retrieveVariablesFromBuild());  | 
 | 217 | +      $target->save();  | 
 | 218 | + | 
 | 219 | +      $this->queueNewBuildTarget($target);  | 
 | 220 | +    }  | 
 | 221 | +  }  | 
 | 222 | + | 
 | 223 | +}  | 
0 commit comments