diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 25421bafe6..7b3a31635a 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -1,519 +1,530 @@ true, self::CONFIG_COLUMN_SCHEMA => array( 'revisionID' => 'id?', 'authorPHID' => 'phid?', 'repositoryPHID' => 'phid?', 'sourceMachine' => 'text255?', 'sourcePath' => 'text255?', 'sourceControlSystem' => 'text64?', 'sourceControlBaseRevision' => 'text255?', 'sourceControlPath' => 'text255?', 'lintStatus' => 'uint32', 'unitStatus' => 'uint32', 'lineCount' => 'uint32', 'branch' => 'text255?', 'bookmark' => 'text255?', 'repositoryUUID' => 'text64?', // T6203/NULLABILITY // These should be non-null; all diffs should have a creation method // and the description should just be empty. 'creationMethod' => 'text255?', 'description' => 'text255?', ), self::CONFIG_KEY_SCHEMA => array( 'revisionID' => array( 'columns' => array('revisionID'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DifferentialDiffPHIDType::TYPECONST); } public function addUnsavedChangeset(DifferentialChangeset $changeset) { if ($this->changesets === null) { $this->changesets = array(); } $this->unsavedChangesets[] = $changeset; $this->changesets[] = $changeset; return $this; } public function attachChangesets(array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $this->changesets = $changesets; return $this; } public function getChangesets() { return $this->assertAttached($this->changesets); } public function loadChangesets() { if (!$this->getID()) { return array(); } $changesets = id(new DifferentialChangeset())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($changesets as $changeset) { $changeset->attachDiff($this); } return $changesets; } public function save() { $this->openTransaction(); $ret = parent::save(); foreach ($this->unsavedChangesets as $changeset) { $changeset->setDiffID($this->getID()); $changeset->save(); } $this->saveTransaction(); return $ret; } public static function initializeNewDiff(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorDifferentialApplication')) ->executeOne(); $view_policy = $app->getPolicy( DifferentialDefaultViewCapability::CAPABILITY); $diff = id(new DifferentialDiff()) ->setViewPolicy($view_policy); return $diff; } public static function newFromRawChanges( PhabricatorUser $actor, array $changes) { assert_instances_of($changes, 'ArcanistDiffChange'); $diff = self::initializeNewDiff($actor); return self::buildChangesetsFromRawChanges($diff, $changes); } public static function newEphemeralFromRawChanges(array $changes) { assert_instances_of($changes, 'ArcanistDiffChange'); $diff = id(new DifferentialDiff())->makeEphemeral(); return self::buildChangesetsFromRawChanges($diff, $changes); } private static function buildChangesetsFromRawChanges( DifferentialDiff $diff, array $changes) { // There may not be any changes; initialize the changesets list so that // we don't throw later when accessing it. $diff->attachChangesets(array()); $lines = 0; foreach ($changes as $change) { if ($change->getType() == ArcanistDiffChangeType::TYPE_MESSAGE) { // If a user pastes a diff into Differential which includes a commit // message (e.g., they ran `git show` to generate it), discard that // change when constructing a DifferentialDiff. continue; } $changeset = new DifferentialChangeset(); $add_lines = 0; $del_lines = 0; $first_line = PHP_INT_MAX; $hunks = $change->getHunks(); if ($hunks) { foreach ($hunks as $hunk) { $dhunk = new DifferentialModernHunk(); $dhunk->setOldOffset($hunk->getOldOffset()); $dhunk->setOldLen($hunk->getOldLength()); $dhunk->setNewOffset($hunk->getNewOffset()); $dhunk->setNewLen($hunk->getNewLength()); $dhunk->setChanges($hunk->getCorpus()); $changeset->addUnsavedHunk($dhunk); $add_lines += $hunk->getAddLines(); $del_lines += $hunk->getDelLines(); $added_lines = $hunk->getChangedLines('new'); if ($added_lines) { $first_line = min($first_line, head_key($added_lines)); } } $lines += $add_lines + $del_lines; } else { // This happens when you add empty files. $changeset->attachHunks(array()); } $metadata = $change->getAllMetadata(); if ($first_line != PHP_INT_MAX) { $metadata['line:first'] = $first_line; } $changeset->setOldFile($change->getOldPath()); $changeset->setFilename($change->getCurrentPath()); $changeset->setChangeType($change->getType()); $changeset->setFileType($change->getFileType()); $changeset->setMetadata($metadata); $changeset->setOldProperties($change->getOldProperties()); $changeset->setNewProperties($change->getNewProperties()); $changeset->setAwayPaths($change->getAwayPaths()); $changeset->setAddLines($add_lines); $changeset->setDelLines($del_lines); $diff->addUnsavedChangeset($changeset); } $diff->setLineCount($lines); $parser = new DifferentialChangesetParser(); $changesets = $parser->detectCopiedCode( $diff->getChangesets(), $min_width = 30, $min_lines = 3); $diff->attachChangesets($changesets); return $diff; } public function getDiffDict() { $dict = array( 'id' => $this->getID(), 'revisionID' => $this->getRevisionID(), 'dateCreated' => $this->getDateCreated(), 'dateModified' => $this->getDateModified(), 'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(), 'sourceControlPath' => $this->getSourceControlPath(), 'sourceControlSystem' => $this->getSourceControlSystem(), 'branch' => $this->getBranch(), 'bookmark' => $this->getBookmark(), 'creationMethod' => $this->getCreationMethod(), 'description' => $this->getDescription(), 'unitStatus' => $this->getUnitStatus(), 'lintStatus' => $this->getLintStatus(), 'changes' => array(), 'properties' => array(), ); $dict['changes'] = $this->buildChangesList(); $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $property) { $dict['properties'][$property->getName()] = $property->getData(); if ($property->getName() == 'local:commits') { foreach ($property->getData() as $commit) { $dict['authorName'] = $commit['author']; $dict['authorEmail'] = idx($commit, 'authorEmail'); break; } } } return $dict; } public function buildChangesList() { $changes = array(); foreach ($this->getChangesets() as $changeset) { $hunks = array(); foreach ($changeset->getHunks() as $hunk) { $hunks[] = array( 'oldOffset' => $hunk->getOldOffset(), 'newOffset' => $hunk->getNewOffset(), 'oldLength' => $hunk->getOldLen(), 'newLength' => $hunk->getNewLen(), 'addLines' => null, 'delLines' => null, 'isMissingOldNewline' => null, 'isMissingNewNewline' => null, 'corpus' => $hunk->getChanges(), ); } $change = array( 'id' => $changeset->getID(), 'metadata' => $changeset->getMetadata(), 'oldPath' => $changeset->getOldFile(), 'currentPath' => $changeset->getFilename(), 'awayPaths' => $changeset->getAwayPaths(), 'oldProperties' => $changeset->getOldProperties(), 'newProperties' => $changeset->getNewProperties(), 'type' => $changeset->getChangeType(), 'fileType' => $changeset->getFileType(), 'commitHash' => null, 'addLines' => $changeset->getAddLines(), 'delLines' => $changeset->getDelLines(), 'hunks' => $hunks, ); $changes[] = $change; } return $changes; } public function hasRevision() { return $this->revision !== self::ATTACHABLE; } public function getRevision() { return $this->assertAttached($this->revision); } public function attachRevision(DifferentialRevision $revision = null) { $this->revision = $revision; return $this; } public function attachProperty($key, $value) { $this->properties[$key] = $value; return $this; } public function getProperty($key) { return $this->assertAttachedKey($this->properties, $key); } public function attachBuildable(HarbormasterBuildable $buildable = null) { $this->buildable = $buildable; return $this; } public function getBuildable() { return $this->assertAttached($this->buildable); } public function getBuildTargetPHIDs() { $buildable = $this->getBuildable(); if (!$buildable) { return array(); } $target_phids = array(); foreach ($buildable->getBuilds() as $build) { foreach ($build->getBuildTargets() as $target) { $target_phids[] = $target->getPHID(); } } return $target_phids; } public function loadCoverageMap(PhabricatorUser $viewer) { $target_phids = $this->getBuildTargetPHIDs(); if (!$target_phids) { return array(); } $unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere( 'buildTargetPHID IN (%Ls)', $target_phids); $map = array(); foreach ($unit as $message) { $coverage = $message->getProperty('coverage', array()); foreach ($coverage as $path => $coverage_data) { $map[$path][] = $coverage_data; } } foreach ($map as $path => $coverage_items) { $map[$path] = ArcanistUnitTestResult::mergeCoverage($coverage_items); } return $map; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { if ($this->hasRevision()) { return $this->getRevision()->getPolicy($capability); } return $this->viewPolicy; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->hasRevision()) { return $this->getRevision()->hasAutomaticCapability($capability, $viewer); } return ($this->getAuthorPHID() == $viewer->getPhid()); } public function describeAutomaticCapability($capability) { if ($this->hasRevision()) { return pht( 'This diff is attached to a revision, and inherits its policies.'); } return pht('The author of a diff can see it.'); } /* -( HarbormasterBuildableInterface )------------------------------------- */ public function getHarbormasterBuildablePHID() { return $this->getPHID(); } public function getHarbormasterContainerPHID() { if ($this->getRevisionID()) { $revision = id(new DifferentialRevision())->load($this->getRevisionID()); if ($revision) { return $revision->getPHID(); } } return null; } public function getBuildVariables() { $results = array(); $results['buildable.diff'] = $this->getID(); if ($this->revisionID) { $revision = $this->getRevision(); $results['buildable.revision'] = $revision->getID(); $repo = $revision->getRepository(); if ($repo) { $results['repository.callsign'] = $repo->getCallsign(); $results['repository.phid'] = $repo->getPHID(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); + + // TODO: We're just hoping to get lucky. Instead, `arc` should store + // where it sent changes and we should only provide staging details + // if we reasonably believe they are accurate. + $staging_ref = 'refs/tags/phabricator/diff/'.$this->getID(); + $results['repository.staging.uri'] = $repo->getStagingURI(); + $results['repository.staging.ref'] = $staging_ref; } } return $results; } public function getAvailableBuildVariables() { return array( 'buildable.diff' => pht('The differential diff ID, if applicable.'), 'buildable.revision' => pht('The differential revision ID, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), 'repository.phid' => pht('The PHID of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => pht('The URI to clone or checkout the repository from.'), + 'repository.staging.uri' => + pht('The URI of the staging repository.'), + 'repository.staging.ref' => + pht('The ref name for this change in the staging repository.'), ); } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DifferentialDiffEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DifferentialDiffTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); $this->delete(); foreach ($this->loadChangesets() as $changeset) { $changeset->delete(); } $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); foreach ($properties as $prop) { $prop->delete(); } $this->saveTransaction(); } } diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php index 5ec2d60ae5..513d7b37f9 100644 --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -1,309 +1,313 @@ setAncestorClass(__CLASS__) ->execute(); } public static function getNamedImplementation($class) { return idx(self::getAllBlueprintImplementations(), $class); } protected function newResourceTemplate(DrydockBlueprint $blueprint) { $resource = id(new DrydockResource()) ->setBlueprintPHID($blueprint->getPHID()) ->attachBlueprint($blueprint) ->setType($this->getType()) ->setStatus(DrydockResourceStatus::STATUS_PENDING); // Pre-allocate the resource PHID. $resource->setPHID($resource->generatePHID()); return $resource; } protected function newLease(DrydockBlueprint $blueprint) { return id(new DrydockLease()); } protected function requireActiveLease(DrydockLease $lease) { $lease_status = $lease->getStatus(); switch ($lease_status) { + case DrydockLeaseStatus::STATUS_PENDING: case DrydockLeaseStatus::STATUS_ACQUIRED: - // TODO: Temporary failure. - throw new Exception(pht('Lease still activating.')); + throw new PhabricatorWorkerYieldException(15); case DrydockLeaseStatus::STATUS_ACTIVE: return; default: - // TODO: Permanent failure. - throw new Exception(pht('Lease in bad state.')); + throw new Exception( + pht( + 'Lease ("%s") is in bad state ("%s"), expected "%s".', + $lease->getPHID(), + $lease_status, + DrydockLeaseStatus::STATUS_ACTIVE)); } } } diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index 1c6b71a298..7a34e2d3bc 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -1,349 +1,365 @@ getAttribute('repositories.map'); if (!is_array($need_map)) { return false; } $have_map = $resource->getAttribute('repositories.map'); if (!is_array($have_map)) { return false; } $have_as = ipull($have_map, 'phid'); $need_as = ipull($need_map, 'phid'); foreach ($need_as as $need_directory => $need_phid) { if (empty($have_as[$need_directory])) { // This resource is missing a required working copy. return false; } if ($have_as[$need_directory] != $need_phid) { // This resource has a required working copy, but it contains // the wrong repository. return false; } unset($have_as[$need_directory]); } if ($have_as && $lease->getAttribute('repositories.strict')) { // This resource has extra repositories, but the lease is strict about // which repositories are allowed to exist. return false; } if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) { return false; } return true; } public function acquireLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $lease ->needSlotLock($this->getLeaseSlotLock($resource)) ->acquireOnResource($resource); } private function getLeaseSlotLock(DrydockResource $resource) { $resource_phid = $resource->getPHID(); return "workingcopy.lease({$resource_phid})"; } public function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease) { $resource = $this->newResourceTemplate($blueprint); $resource_phid = $resource->getPHID(); $host_lease = $this->newLease($blueprint) ->setResourceType('host') ->setOwnerPHID($resource_phid) ->setAttribute('workingcopy.resourcePHID', $resource_phid) ->queueForActivation(); // TODO: Add some limits to the number of working copies we can have at // once? $map = $lease->getAttribute('repositories.map'); foreach ($map as $key => $value) { $map[$key] = array_select_keys( $value, array( 'phid', )); } return $resource ->setAttribute('repositories.map', $map) ->setAttribute('host.leasePHID', $host_lease->getPHID()) ->allocateResource(); } public function activateResource( DrydockBlueprint $blueprint, DrydockResource $resource) { $lease = $this->loadHostLease($resource); $this->requireActiveLease($lease); $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $lease->getInterface($command_type); // TODO: Make this configurable. $resource_id = $resource->getID(); $root = "/var/drydock/workingcopy-{$resource_id}"; $map = $resource->getAttribute('repositories.map'); $repositories = $this->loadRepositories(ipull($map, 'phid')); foreach ($map as $directory => $spec) { // TODO: Validate directory isn't goofy like "/etc" or "../../lol" // somewhere? $repository = $repositories[$spec['phid']]; $path = "{$root}/repo/{$directory}/"; // TODO: Run these in parallel? $interface->execx( 'git clone -- %s %s', (string)$repository->getCloneURIObject(), $path); } $resource ->setAttribute('workingcopy.root', $root) ->activateResource(); } public function destroyResource( DrydockBlueprint $blueprint, DrydockResource $resource) { $lease = $this->loadHostLease($resource); // Destroy the lease on the host. $lease->releaseOnDestruction(); if ($lease->isActive()) { // Destroy the working copy on disk. $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $lease->getInterface($command_type); $root_key = 'workingcopy.root'; $root = $resource->getAttribute($root_key); if (strlen($root)) { $interface->execx('rm -rf -- %s', $root); } } } public function getResourceName( DrydockBlueprint $blueprint, DrydockResource $resource) { return pht('Working Copy'); } public function activateLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { $host_lease = $this->loadHostLease($resource); $command_type = DrydockCommandInterface::INTERFACE_TYPE; $interface = $host_lease->getInterface($command_type); $map = $lease->getAttribute('repositories.map'); $root = $resource->getAttribute('workingcopy.root'); $default = null; foreach ($map as $directory => $spec) { $cmd = array(); $arg = array(); $cmd[] = 'cd %s'; $arg[] = "{$root}/repo/{$directory}/"; $cmd[] = 'git clean -d --force'; $cmd[] = 'git fetch'; $commit = idx($spec, 'commit'); $branch = idx($spec, 'branch'); + $ref = idx($spec, 'ref'); + if ($commit !== null) { $cmd[] = 'git reset --hard %s'; $arg[] = $commit; } else if ($branch !== null) { $cmd[] = 'git checkout %s'; $arg[] = $branch; $cmd[] = 'git reset --hard origin/%s'; $arg[] = $branch; + } else if ($ref) { + $ref_uri = $ref['uri']; + $ref_ref = $ref['ref']; + + $cmd[] = 'git fetch --no-tags -- %s +%s:%s'; + $arg[] = $ref_uri; + $arg[] = $ref_ref; + $arg[] = $ref_ref; + + $cmd[] = 'git checkout %s'; + $arg[] = $ref_ref; + + $cmd[] = 'git reset --hard %s'; + $arg[] = $ref_ref; } else { $cmd[] = 'git reset --hard HEAD'; } $cmd = implode(' && ', $cmd); $argv = array_merge(array($cmd), $arg); $result = call_user_func_array( array($interface, 'execx'), $argv); if (idx($spec, 'default')) { $default = $directory; } } if ($default === null) { $default = head_key($map); } // TODO: Use working storage? $lease->setAttribute('workingcopy.default', "{$root}/repo/{$default}/"); $lease->activateOnResource($resource); } public function didReleaseLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // We leave working copies around even if there are no leases on them, // since the cost to maintain them is nearly zero but rebuilding them is // moderately expensive and it's likely that they'll be reused. return; } public function destroyLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // When we activate a lease we just reset the working copy state and do // not create any new state, so we don't need to do anything special when // destroying a lease. return; } public function getType() { return 'working-copy'; } public function getInterface( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type) { switch ($type) { case DrydockCommandInterface::INTERFACE_TYPE: $host_lease = $this->loadHostLease($resource); $command_interface = $host_lease->getInterface($type); $path = $lease->getAttribute('workingcopy.default'); $command_interface->setWorkingDirectory($path); return $command_interface; } } private function loadRepositories(array $phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs($phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($phids as $phid) { if (empty($repositories[$phid])) { throw new Exception( pht( 'Repository PHID "%s" does not exist.', $phid)); } } foreach ($repositories as $repository) { $repository_vcs = $repository->getVersionControlSystem(); switch ($repository_vcs) { case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: break; default: throw new Exception( pht( 'Repository ("%s") has unsupported VCS ("%s").', $repository->getPHID(), $repository_vcs)); } } return $repositories; } private function loadHostLease(DrydockResource $resource) { $viewer = PhabricatorUser::getOmnipotentUser(); $lease_phid = $resource->getAttribute('host.leasePHID'); $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withPHIDs(array($lease_phid)) ->executeOne(); if (!$lease) { throw new Exception( pht( 'Unable to load lease ("%s").', $lease_phid)); } return $lease; } } diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index a0d440e4ea..4bc3dbe86d 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -1,335 +1,346 @@ setViewer($actor) ->withClasses(array('PhabricatorDrydockApplication')) ->executeOne(); $view_policy = $app->getPolicy( DrydockDefaultViewCapability::CAPABILITY); $edit_policy = $app->getPolicy( DrydockDefaultEditCapability::CAPABILITY); return id(new DrydockBlueprint()) ->setViewPolicy($view_policy) ->setEditPolicy($edit_policy) ->setBlueprintName('') ->setIsDisabled(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'className' => 'text255', 'blueprintName' => 'sort255', 'isDisabled' => 'bool', ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DrydockBlueprintPHIDType::TYPECONST); } public function getImplementation() { return $this->assertAttached($this->implementation); } public function attachImplementation(DrydockBlueprintImplementation $impl) { $this->implementation = $impl; return $this; } public function hasImplementation() { return ($this->implementation !== self::ATTACHABLE); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getFieldValue($key) { $key = "std:drydock:core:{$key}"; $fields = $this->loadCustomFields(); $field = idx($fields, $key); if (!$field) { throw new Exception( pht( 'Unknown blueprint field "%s"!', $key)); } return $field->getBlueprintFieldValue(); } private function loadCustomFields() { if ($this->fields === null) { $field_list = PhabricatorCustomField::getObjectFields( $this, PhabricatorCustomField::ROLE_VIEW); $field_list->readFieldsFromStorage($this); $this->fields = $field_list->getFields(); } return $this->fields; } + public function logEvent($type, array $data = array()) { + $log = id(new DrydockLog()) + ->setEpoch(PhabricatorTime::getNow()) + ->setType($type) + ->setData($data); + + $log->setBlueprintPHID($this->getPHID()); + + return $log->save(); + } + /* -( Allocating Resources )----------------------------------------------- */ /** * @task resource */ public function canEverAllocateResourceForLease(DrydockLease $lease) { return $this->getImplementation()->canEverAllocateResourceForLease( $this, $lease); } /** * @task resource */ public function canAllocateResourceForLease(DrydockLease $lease) { return $this->getImplementation()->canAllocateResourceForLease( $this, $lease); } /** * @task resource */ public function allocateResource(DrydockLease $lease) { return $this->getImplementation()->allocateResource( $this, $lease); } /** * @task resource */ public function activateResource(DrydockResource $resource) { return $this->getImplementation()->activateResource( $this, $resource); } /** * @task resource */ public function destroyResource(DrydockResource $resource) { $this->getImplementation()->destroyResource( $this, $resource); return $this; } /** * @task resource */ public function getResourceName(DrydockResource $resource) { return $this->getImplementation()->getResourceName( $this, $resource); } /* -( Acquiring Leases )--------------------------------------------------- */ /** * @task lease */ public function canAcquireLeaseOnResource( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->canAcquireLeaseOnResource( $this, $resource, $lease); } /** * @task lease */ public function acquireLease( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->acquireLease( $this, $resource, $lease); } /** * @task lease */ public function activateLease( DrydockResource $resource, DrydockLease $lease) { return $this->getImplementation()->activateLease( $this, $resource, $lease); } /** * @task lease */ public function didReleaseLease( DrydockResource $resource, DrydockLease $lease) { $this->getImplementation()->didReleaseLease( $this, $resource, $lease); return $this; } /** * @task lease */ public function destroyLease( DrydockResource $resource, DrydockLease $lease) { $this->getImplementation()->destroyLease( $this, $resource, $lease); return $this; } public function getInterface( DrydockResource $resource, DrydockLease $lease, $type) { $interface = $this->getImplementation() ->getInterface($this, $resource, $lease, $type); if (!$interface) { throw new Exception( pht( 'Unable to build resource interface of type "%s".', $type)); } return $interface; } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new DrydockBlueprintEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new DrydockBlueprintTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: return $this->getEditPolicy(); } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } public function describeAutomaticCapability($capability) { return null; } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'DrydockBlueprintCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } } diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index a734137653..aa2ea753f0 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -1,439 +1,442 @@ releaseOnDestruction = true; return $this; } public function __destruct() { if (!$this->releaseOnDestruction) { return; } if (!$this->canRelease()) { return; } $actor = PhabricatorUser::getOmnipotentUser(); $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); $command = DrydockCommand::initializeNewCommand($actor) ->setTargetPHID($this->getPHID()) ->setAuthorPHID($drydock_phid) ->setCommand(DrydockCommand::COMMAND_RELEASE) ->save(); $this->scheduleUpdate(); } public function getLeaseName() { return pht('Lease %d', $this->getID()); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'attributes' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'status' => 'text32', 'until' => 'epoch?', 'resourceType' => 'text128', 'ownerPHID' => 'phid?', 'resourcePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_resource' => array( 'columns' => array('resourcePHID', 'status'), ), ), ) + parent::getConfiguration(); } public function setAttribute($key, $value) { $this->attributes[$key] = $value; return $this; } public function getAttribute($key, $default = null) { return idx($this->attributes, $key, $default); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DrydockLeasePHIDType::TYPECONST); } public function getInterface($type) { return $this->getResource()->getInterface($this, $type); } public function getResource() { return $this->assertAttached($this->resource); } public function attachResource(DrydockResource $resource = null) { $this->resource = $resource; return $this; } public function hasAttachedResource() { return ($this->resource !== null); } public function getUnconsumedCommands() { return $this->assertAttached($this->unconsumedCommands); } public function attachUnconsumedCommands(array $commands) { $this->unconsumedCommands = $commands; return $this; } public function isReleasing() { foreach ($this->getUnconsumedCommands() as $command) { if ($command->getCommand() == DrydockCommand::COMMAND_RELEASE) { return true; } } return false; } public function queueForActivation() { if ($this->getID()) { throw new Exception( pht('Only new leases may be queued for activation!')); } $this ->setStatus(DrydockLeaseStatus::STATUS_PENDING) ->save(); $this->scheduleUpdate(); $this->logEvent(DrydockLeaseQueuedLogType::LOGCONST); return $this; } public function isActivating() { switch ($this->getStatus()) { case DrydockLeaseStatus::STATUS_PENDING: case DrydockLeaseStatus::STATUS_ACQUIRED: return true; } return false; } public function isActive() { switch ($this->getStatus()) { case DrydockLeaseStatus::STATUS_ACTIVE: return true; } return false; } public function waitUntilActive() { while (true) { $lease = $this->reload(); if (!$lease) { throw new Exception(pht('Failed to reload lease.')); } $status = $lease->getStatus(); switch ($status) { case DrydockLeaseStatus::STATUS_ACTIVE: return; case DrydockLeaseStatus::STATUS_RELEASED: throw new Exception(pht('Lease has already been released!')); case DrydockLeaseStatus::STATUS_DESTROYED: throw new Exception(pht('Lease has already been destroyed!')); case DrydockLeaseStatus::STATUS_BROKEN: throw new Exception(pht('Lease has been broken!')); case DrydockLeaseStatus::STATUS_PENDING: case DrydockLeaseStatus::STATUS_ACQUIRED: break; default: throw new Exception( pht( 'Lease has unknown status "%s".', $status)); } sleep(1); } } public function setActivateWhenAcquired($activate) { $this->activateWhenAcquired = true; return $this; } public function needSlotLock($key) { $this->slotLocks[] = $key; return $this; } public function acquireOnResource(DrydockResource $resource) { $expect_status = DrydockLeaseStatus::STATUS_PENDING; $actual_status = $this->getStatus(); if ($actual_status != $expect_status) { throw new Exception( pht( 'Trying to acquire a lease on a resource which is in the wrong '. 'state: status must be "%s", actually "%s".', $expect_status, $actual_status)); } if ($this->activateWhenAcquired) { $new_status = DrydockLeaseStatus::STATUS_ACTIVE; } else { $new_status = DrydockLeaseStatus::STATUS_ACQUIRED; } if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) { if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { throw new Exception( pht( 'Trying to acquire an active lease on a pending resource. '. 'You can not immediately activate leases on resources which '. 'need time to start up.')); } } $this->openTransaction(); try { - try { - DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); - $this->slotLocks = array(); - } catch (DrydockSlotLockException $ex) { - $this->logEvent( - DrydockSlotLockFailureLogType::LOGCONST, - array( - 'locks' => $ex->getLockMap(), - )); - throw $ex; - } + DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); + $this->slotLocks = array(); + } catch (DrydockSlotLockException $ex) { + $this->killTransaction(); + + $this->logEvent( + DrydockSlotLockFailureLogType::LOGCONST, + array( + 'locks' => $ex->getLockMap(), + )); + throw $ex; + } + + try { $this ->setResourcePHID($resource->getPHID()) ->attachResource($resource) ->setStatus($new_status) ->save(); } catch (Exception $ex) { $this->killTransaction(); throw $ex; } $this->saveTransaction(); $this->isAcquired = true; $this->logEvent(DrydockLeaseAcquiredLogType::LOGCONST); if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) { $this->didActivate(); } return $this; } public function isAcquiredLease() { return $this->isAcquired; } public function activateOnResource(DrydockResource $resource) { $expect_status = DrydockLeaseStatus::STATUS_ACQUIRED; $actual_status = $this->getStatus(); if ($actual_status != $expect_status) { throw new Exception( pht( 'Trying to activate a lease which has the wrong status: status '. 'must be "%s", actually "%s".', $expect_status, $actual_status)); } if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { // TODO: Be stricter about this? throw new Exception( pht( 'Trying to activate a lease on a pending resource.')); } $this->openTransaction(); $this ->setStatus(DrydockLeaseStatus::STATUS_ACTIVE) ->save(); DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); $this->slotLocks = array(); $this->saveTransaction(); $this->isActivated = true; $this->didActivate(); return $this; } public function isActivatedLease() { return $this->isActivated; } public function canRelease() { if (!$this->getID()) { return false; } switch ($this->getStatus()) { case DrydockLeaseStatus::STATUS_RELEASED: case DrydockLeaseStatus::STATUS_DESTROYED: return false; default: return true; } } public function canReceiveCommands() { switch ($this->getStatus()) { case DrydockLeaseStatus::STATUS_RELEASED: case DrydockLeaseStatus::STATUS_DESTROYED: return false; default: return true; } } public function scheduleUpdate($epoch = null) { PhabricatorWorker::scheduleTask( 'DrydockLeaseUpdateWorker', array( 'leasePHID' => $this->getPHID(), 'isExpireTask' => ($epoch !== null), ), array( 'objectPHID' => $this->getPHID(), 'delayUntil' => ($epoch ? (int)$epoch : null), )); } public function setAwakenTaskIDs(array $ids) { $this->setAttribute('internal.awakenTaskIDs', $ids); return $this; } private function didActivate() { $viewer = PhabricatorUser::getOmnipotentUser(); $need_update = false; $this->logEvent(DrydockLeaseActivatedLogType::LOGCONST); $commands = id(new DrydockCommandQuery()) ->setViewer($viewer) ->withTargetPHIDs(array($this->getPHID())) ->withConsumed(false) ->execute(); if ($commands) { $need_update = true; } if ($need_update) { $this->scheduleUpdate(); } $expires = $this->getUntil(); if ($expires) { $this->scheduleUpdate($expires); } $awaken_ids = $this->getAttribute('internal.awakenTaskIDs'); if (is_array($awaken_ids) && $awaken_ids) { PhabricatorWorker::awakenTaskIDs($awaken_ids); } } public function logEvent($type, array $data = array()) { $log = id(new DrydockLog()) ->setEpoch(PhabricatorTime::getNow()) ->setType($type) ->setData($data); $log->setLeasePHID($this->getPHID()); $resource_phid = $this->getResourcePHID(); if ($resource_phid) { $resource = $this->getResource(); $log->setResourcePHID($resource->getPHID()); $log->setBlueprintPHID($resource->getBlueprintPHID()); } return $log->save(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { if ($this->getResource()) { return $this->getResource()->getPolicy($capability); } // TODO: Implement reasonable policies. return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if ($this->getResource()) { return $this->getResource()->hasAutomaticCapability($capability, $viewer); } return false; } public function describeAutomaticCapability($capability) { return pht('Leases inherit policies from the resources they lease.'); } } diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php index 7aad1064f4..da97a089d9 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -1,320 +1,327 @@ true, self::CONFIG_SERIALIZATION => array( 'attributes' => self::SERIALIZATION_JSON, 'capabilities' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', 'status' => 'text32', 'type' => 'text64', 'until' => 'epoch?', ), self::CONFIG_KEY_SCHEMA => array( 'key_type' => array( 'columns' => array('type', 'status'), ), 'key_blueprint' => array( 'columns' => array('blueprintPHID', 'status'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID(DrydockResourcePHIDType::TYPECONST); } public function getResourceName() { return $this->getBlueprint()->getResourceName($this); } public function getAttribute($key, $default = null) { return idx($this->attributes, $key, $default); } public function getAttributesForTypeSpec(array $attribute_names) { return array_select_keys($this->attributes, $attribute_names); } public function setAttribute($key, $value) { $this->attributes[$key] = $value; return $this; } public function getCapability($key, $default = null) { return idx($this->capbilities, $key, $default); } public function getInterface(DrydockLease $lease, $type) { return $this->getBlueprint()->getInterface($this, $lease, $type); } public function getBlueprint() { return $this->assertAttached($this->blueprint); } public function attachBlueprint(DrydockBlueprint $blueprint) { $this->blueprint = $blueprint; return $this; } public function getUnconsumedCommands() { return $this->assertAttached($this->unconsumedCommands); } public function attachUnconsumedCommands(array $commands) { $this->unconsumedCommands = $commands; return $this; } public function isReleasing() { foreach ($this->getUnconsumedCommands() as $command) { if ($command->getCommand() == DrydockCommand::COMMAND_RELEASE) { return true; } } return false; } public function setActivateWhenAllocated($activate) { $this->activateWhenAllocated = $activate; return $this; } public function needSlotLock($key) { $this->slotLocks[] = $key; return $this; } public function allocateResource() { if ($this->getID()) { throw new Exception( pht( 'Trying to allocate a resource which has already been persisted. '. 'Only new resources may be allocated.')); } // We expect resources to have a pregenerated PHID, as they should have // been created by a call to DrydockBlueprint->newResourceTemplate(). if (!$this->getPHID()) { throw new Exception( pht( 'Trying to allocate a resource with no generated PHID. Use "%s" to '. 'create new resource templates.', 'newResourceTemplate()')); } $expect_status = DrydockResourceStatus::STATUS_PENDING; $actual_status = $this->getStatus(); if ($actual_status != $expect_status) { throw new Exception( pht( 'Trying to allocate a resource from the wrong status. Status must '. 'be "%s", actually "%s".', $expect_status, $actual_status)); } if ($this->activateWhenAllocated) { $new_status = DrydockResourceStatus::STATUS_ACTIVE; } else { $new_status = DrydockResourceStatus::STATUS_PENDING; } $this->openTransaction(); + try { - try { - DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); - $this->slotLocks = array(); - } catch (DrydockSlotLockException $ex) { - $this->logEvent( - DrydockSlotLockFailureLogType::LOGCONST, - array( - 'locks' => $ex->getLockMap(), - )); - throw $ex; - } + DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); + $this->slotLocks = array(); + } catch (DrydockSlotLockException $ex) { + $this->killTransaction(); + + // NOTE: We have to log this on the blueprint, as the resource is not + // going to be saved so the PHID will vanish. + $this->getBlueprint()->logEvent( + DrydockSlotLockFailureLogType::LOGCONST, + array( + 'locks' => $ex->getLockMap(), + )); + throw $ex; + } + + try { $this ->setStatus($new_status) ->save(); } catch (Exception $ex) { $this->killTransaction(); throw $ex; } + $this->saveTransaction(); $this->isAllocated = true; if ($new_status == DrydockResourceStatus::STATUS_ACTIVE) { $this->didActivate(); } return $this; } public function isAllocatedResource() { return $this->isAllocated; } public function activateResource() { if (!$this->getID()) { throw new Exception( pht( 'Trying to activate a resource which has not yet been persisted.')); } $expect_status = DrydockResourceStatus::STATUS_PENDING; $actual_status = $this->getStatus(); if ($actual_status != $expect_status) { throw new Exception( pht( 'Trying to activate a resource from the wrong status. Status must '. 'be "%s", actually "%s".', $expect_status, $actual_status)); } $this->openTransaction(); $this ->setStatus(DrydockResourceStatus::STATUS_ACTIVE) ->save(); DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); $this->slotLocks = array(); $this->saveTransaction(); $this->isActivated = true; $this->didActivate(); return $this; } public function isActivatedResource() { return $this->isActivated; } public function canRelease() { switch ($this->getStatus()) { case DrydockResourceStatus::STATUS_RELEASED: case DrydockResourceStatus::STATUS_DESTROYED: return false; default: return true; } } public function scheduleUpdate($epoch = null) { PhabricatorWorker::scheduleTask( 'DrydockResourceUpdateWorker', array( 'resourcePHID' => $this->getPHID(), 'isExpireTask' => ($epoch !== null), ), array( 'objectPHID' => $this->getPHID(), 'delayUntil' => ($epoch ? (int)$epoch : null), )); } private function didActivate() { $viewer = PhabricatorUser::getOmnipotentUser(); $need_update = false; $commands = id(new DrydockCommandQuery()) ->setViewer($viewer) ->withTargetPHIDs(array($this->getPHID())) ->withConsumed(false) ->execute(); if ($commands) { $need_update = true; } if ($need_update) { $this->scheduleUpdate(); } $expires = $this->getUntil(); if ($expires) { $this->scheduleUpdate($expires); } } public function canReceiveCommands() { switch ($this->getStatus()) { case DrydockResourceStatus::STATUS_RELEASED: case DrydockResourceStatus::STATUS_BROKEN: case DrydockResourceStatus::STATUS_DESTROYED: return false; default: return true; } } public function logEvent($type, array $data = array()) { $log = id(new DrydockLog()) ->setEpoch(PhabricatorTime::getNow()) ->setType($type) ->setData($data); $log->setResourcePHID($this->getPHID()); $log->setBlueprintPHID($this->getBlueprintPHID()); return $log->save(); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBlueprint()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBlueprint()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('Resources inherit the policies of their blueprints.'); } } diff --git a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php index 4ac6a9775c..b6ac6cec52 100644 --- a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php +++ b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php @@ -1,733 +1,734 @@ getTaskDataValue('leasePHID'); $hash = PhabricatorHash::digestForIndex($lease_phid); $lock_key = 'drydock.lease:'.$hash; $lock = PhabricatorGlobalLock::newLock($lock_key) ->lock(1); try { $lease = $this->loadLease($lease_phid); $this->handleUpdate($lease); } catch (Exception $ex) { $lock->unlock(); throw $ex; } $lock->unlock(); } /* -( Updating Leases )---------------------------------------------------- */ /** * @task update */ private function handleUpdate(DrydockLease $lease) { try { $this->updateLease($lease); } catch (Exception $ex) { if ($this->isTemporaryException($ex)) { $this->yieldLease($lease, $ex); } else { $this->breakLease($lease, $ex); } } } /** * @task update */ private function updateLease(DrydockLease $lease) { $this->processLeaseCommands($lease); $lease_status = $lease->getStatus(); switch ($lease_status) { case DrydockLeaseStatus::STATUS_PENDING: $this->executeAllocator($lease); break; case DrydockLeaseStatus::STATUS_ACQUIRED: $this->activateLease($lease); break; case DrydockLeaseStatus::STATUS_ACTIVE: // Nothing to do. break; case DrydockLeaseStatus::STATUS_RELEASED: case DrydockLeaseStatus::STATUS_BROKEN: $this->destroyLease($lease); break; case DrydockLeaseStatus::STATUS_DESTROYED: break; } $this->yieldIfExpiringLease($lease); } /** * @task update */ private function yieldLease(DrydockLease $lease, Exception $ex) { $duration = $this->getYieldDurationFromException($ex); $lease->logEvent( DrydockLeaseActivationYieldLogType::LOGCONST, array( 'duration' => $duration, )); throw new PhabricatorWorkerYieldException($duration); } /* -( Processing Commands )------------------------------------------------ */ /** * @task command */ private function processLeaseCommands(DrydockLease $lease) { if (!$lease->canReceiveCommands()) { return; } $this->checkLeaseExpiration($lease); $commands = $this->loadCommands($lease->getPHID()); foreach ($commands as $command) { if (!$lease->canReceiveCommands()) { break; } $this->processLeaseCommand($lease, $command); $command ->setIsConsumed(true) ->save(); } } /** * @task command */ private function processLeaseCommand( DrydockLease $lease, DrydockCommand $command) { switch ($command->getCommand()) { case DrydockCommand::COMMAND_RELEASE: $this->releaseLease($lease); break; } } /* -( Drydock Allocator )-------------------------------------------------- */ /** * Find or build a resource which can satisfy a given lease request, then * acquire the lease. * * @param DrydockLease Requested lease. * @return void * @task allocator */ private function executeAllocator(DrydockLease $lease) { $blueprints = $this->loadBlueprintsForAllocatingLease($lease); // If we get nothing back, that means no blueprint is defined which can // ever build the requested resource. This is a permanent failure, since // we don't expect to succeed no matter how many times we try. if (!$blueprints) { throw new PhabricatorWorkerPermanentFailureException( pht( 'No active Drydock blueprint exists which can ever allocate a '. 'resource for lease "%s".', $lease->getPHID())); } // First, try to find a suitable open resource which we can acquire a new // lease on. $resources = $this->loadResourcesForAllocatingLease($blueprints, $lease); // If no resources exist yet, see if we can build one. if (!$resources) { $usable_blueprints = $this->removeOverallocatedBlueprints( $blueprints, $lease); // If we get nothing back here, some blueprint claims it can eventually // satisfy the lease, just not right now. This is a temporary failure, // and we expect allocation to succeed eventually. if (!$usable_blueprints) { $lease->logEvent( DrydockLeaseWaitingForResourcesLogType::LOGCONST, array( 'blueprintPHIDs' => mpull($blueprints, 'getPHID'), )); throw new PhabricatorWorkerYieldException(15); } $usable_blueprints = $this->rankBlueprints($usable_blueprints, $lease); $exceptions = array(); foreach ($usable_blueprints as $blueprint) { try { $resources[] = $this->allocateResource($blueprint, $lease); // Bail after allocating one resource, we don't need any more than // this. break; } catch (Exception $ex) { $exceptions[] = $ex; } } if (!$resources) { throw new PhutilAggregateException( pht( 'All blueprints failed to allocate a suitable new resource when '. 'trying to allocate lease "%s".', $lease->getPHID()), $exceptions); } // NOTE: We have not acquired the lease yet, so it is possible that the // resource we just built will be snatched up by some other lease before // we can. This is not problematic: we'll retry a little later and should // suceed eventually. } $resources = $this->rankResources($resources, $lease); $exceptions = array(); $allocated = false; foreach ($resources as $resource) { try { $this->acquireLease($resource, $lease); $allocated = true; break; } catch (Exception $ex) { $exceptions[] = $ex; } } if (!$allocated) { throw new PhutilAggregateException( pht( 'Unable to acquire lease "%s" on any resouce.', $lease->getPHID()), $exceptions); } } /** * Get all the @{class:DrydockBlueprintImplementation}s which can possibly * build a resource to satisfy a lease. * * This method returns blueprints which might, at some time, be able to * build a resource which can satisfy the lease. They may not be able to * build that resource right now. * * @param DrydockLease Requested lease. * @return list List of qualifying blueprint * implementations. * @task allocator */ private function loadBlueprintImplementationsForAllocatingLease( DrydockLease $lease) { $impls = DrydockBlueprintImplementation::getAllBlueprintImplementations(); $keep = array(); foreach ($impls as $key => $impl) { // Don't use disabled blueprint types. if (!$impl->isEnabled()) { continue; } // Don't use blueprint types which can't allocate the correct kind of // resource. if ($impl->getType() != $lease->getResourceType()) { continue; } if (!$impl->canAnyBlueprintEverAllocateResourceForLease($lease)) { continue; } $keep[$key] = $impl; } return $keep; } /** * Get all the concrete @{class:DrydockBlueprint}s which can possibly * build a resource to satisfy a lease. * * @param DrydockLease Requested lease. * @return list List of qualifying blueprints. * @task allocator */ private function loadBlueprintsForAllocatingLease( DrydockLease $lease) { $viewer = $this->getViewer(); $impls = $this->loadBlueprintImplementationsForAllocatingLease($lease); if (!$impls) { return array(); } $blueprints = id(new DrydockBlueprintQuery()) ->setViewer($viewer) ->withBlueprintClasses(array_keys($impls)) ->withDisabled(false) ->execute(); $keep = array(); foreach ($blueprints as $key => $blueprint) { if (!$blueprint->canEverAllocateResourceForLease($lease)) { continue; } $keep[$key] = $blueprint; } return $keep; } /** * Load a list of all resources which a given lease can possibly be * allocated against. * * @param list Blueprints which may produce suitable * resources. * @param DrydockLease Requested lease. * @return list Resources which may be able to allocate * the lease. * @task allocator */ private function loadResourcesForAllocatingLease( array $blueprints, DrydockLease $lease) { assert_instances_of($blueprints, 'DrydockBlueprint'); $viewer = $this->getViewer(); $resources = id(new DrydockResourceQuery()) ->setViewer($viewer) ->withBlueprintPHIDs(mpull($blueprints, 'getPHID')) ->withTypes(array($lease->getResourceType())) ->withStatuses( array( DrydockResourceStatus::STATUS_PENDING, DrydockResourceStatus::STATUS_ACTIVE, )) ->execute(); $keep = array(); foreach ($resources as $key => $resource) { $blueprint = $resource->getBlueprint(); if (!$blueprint->canAcquireLeaseOnResource($resource, $lease)) { continue; } $keep[$key] = $resource; } return $keep; } /** * Remove blueprints which are too heavily allocated to build a resource for * a lease from a list of blueprints. * * @param list List of blueprints. * @return list List with blueprints that can not allocate * a resource for the lease right now removed. * @task allocator */ private function removeOverallocatedBlueprints( array $blueprints, DrydockLease $lease) { assert_instances_of($blueprints, 'DrydockBlueprint'); $keep = array(); foreach ($blueprints as $key => $blueprint) { if (!$blueprint->canAllocateResourceForLease($lease)) { continue; } $keep[$key] = $blueprint; } return $keep; } /** * Rank blueprints by suitability for building a new resource for a * particular lease. * * @param list List of blueprints. * @param DrydockLease Requested lease. * @return list Ranked list of blueprints. * @task allocator */ private function rankBlueprints(array $blueprints, DrydockLease $lease) { assert_instances_of($blueprints, 'DrydockBlueprint'); // TODO: Implement improvements to this ranking algorithm if they become // available. shuffle($blueprints); return $blueprints; } /** * Rank resources by suitability for allocating a particular lease. * * @param list List of resources. * @param DrydockLease Requested lease. * @return list Ranked list of resources. * @task allocator */ private function rankResources(array $resources, DrydockLease $lease) { assert_instances_of($resources, 'DrydockResource'); // TODO: Implement improvements to this ranking algorithm if they become // available. shuffle($resources); return $resources; } /** * Perform an actual resource allocation with a particular blueprint. * * @param DrydockBlueprint The blueprint to allocate a resource from. * @param DrydockLease Requested lease. * @return DrydockResource Allocated resource. * @task allocator */ private function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease) { $resource = $blueprint->allocateResource($lease); $this->validateAllocatedResource($blueprint, $resource, $lease); // If this resource was allocated as a pending resource, queue a task to // activate it. if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { PhabricatorWorker::scheduleTask( 'DrydockResourceUpdateWorker', array( 'resourcePHID' => $resource->getPHID(), ), array( 'objectPHID' => $resource->getPHID(), )); } return $resource; } /** * Check that the resource a blueprint allocated is roughly the sort of * object we expect. * * @param DrydockBlueprint Blueprint which built the resource. * @param wild Thing which the blueprint claims is a valid resource. * @param DrydockLease Lease the resource was allocated for. * @return void * @task allocator */ private function validateAllocatedResource( DrydockBlueprint $blueprint, $resource, DrydockLease $lease) { if (!($resource instanceof DrydockResource)) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: %s must '. 'return an object of type %s or throw, but returned something else.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'allocateResource()', 'DrydockResource')); } if (!$resource->isAllocatedResource()) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: %s '. 'must actually allocate the resource it returns.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'allocateResource()')); } $resource_type = $resource->getType(); $lease_type = $lease->getResourceType(); if ($resource_type !== $lease_type) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: it '. 'built a resource of type "%s" to satisfy a lease requesting a '. 'resource of type "%s".', $blueprint->getBlueprintName(), $blueprint->getClassName(), $resource_type, $lease_type)); } } /* -( Acquiring Leases )--------------------------------------------------- */ /** * Perform an actual lease acquisition on a particular resource. * * @param DrydockResource Resource to acquire a lease on. * @param DrydockLease Lease to acquire. * @return void * @task acquire */ private function acquireLease( DrydockResource $resource, DrydockLease $lease) { $blueprint = $resource->getBlueprint(); $blueprint->acquireLease($resource, $lease); $this->validateAcquiredLease($blueprint, $resource, $lease); // If this lease has been acquired but not activated, queue a task to // activate it. if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACQUIRED) { PhabricatorWorker::scheduleTask( __CLASS__, array( 'leasePHID' => $lease->getPHID(), ), array( 'objectPHID' => $lease->getPHID(), )); } } /** * Make sure that a lease was really acquired properly. * * @param DrydockBlueprint Blueprint which created the resource. * @param DrydockResource Resource which was acquired. * @param DrydockLease The lease which was supposedly acquired. * @return void * @task acquire */ private function validateAcquiredLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { if (!$lease->isAcquiredLease()) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: it '. 'returned from "%s" without acquiring a lease.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'acquireLease()')); } $lease_phid = $lease->getResourcePHID(); $resource_phid = $resource->getPHID(); if ($lease_phid !== $resource_phid) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: it '. 'returned from "%s" with a lease acquired on the wrong resource.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'acquireLease()')); } } /* -( Activating Leases )-------------------------------------------------- */ /** * @task activate */ private function activateLease(DrydockLease $lease) { $resource = $lease->getResource(); if (!$resource) { throw new Exception( pht('Trying to activate lease with no resource.')); } $resource_status = $resource->getStatus(); if ($resource_status == DrydockResourceStatus::STATUS_PENDING) { throw new PhabricatorWorkerYieldException(15); } if ($resource_status != DrydockResourceStatus::STATUS_ACTIVE) { throw new Exception( pht( 'Trying to activate lease on a dead resource (in status "%s").', $resource_status)); } // NOTE: We can race resource destruction here. Between the time we // performed the read above and now, the resource might have closed, so // we may activate leases on dead resources. At least for now, this seems // fine: a resource dying right before we activate a lease on it should not // be distinguisahble from a resource dying right after we activate a lease // on it. We end up with an active lease on a dead resource either way, and // can not prevent resources dying from lightning strikes. $blueprint = $resource->getBlueprint(); $blueprint->activateLease($resource, $lease); $this->validateActivatedLease($blueprint, $resource, $lease); } /** * @task activate */ private function validateActivatedLease( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { if (!$lease->isActivatedLease()) { throw new Exception( pht( 'Blueprint "%s" (of type "%s") is not properly implemented: it '. 'returned from "%s" without activating a lease.', $blueprint->getBlueprintName(), $blueprint->getClassName(), 'acquireLease()')); } } /* -( Releasing Leases )--------------------------------------------------- */ /** * @task release */ private function releaseLease(DrydockLease $lease) { $lease ->setStatus(DrydockLeaseStatus::STATUS_RELEASED) ->save(); $lease->logEvent(DrydockLeaseReleasedLogType::LOGCONST); $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - - $blueprint->didReleaseLease($resource, $lease); + if ($resource) { + $blueprint = $resource->getBlueprint(); + $blueprint->didReleaseLease($resource, $lease); + } $this->destroyLease($lease); } /* -( Breaking Leases )---------------------------------------------------- */ /** * @task break */ protected function breakLease(DrydockLease $lease, Exception $ex) { switch ($lease->getStatus()) { case DrydockLeaseStatus::STATUS_BROKEN: case DrydockLeaseStatus::STATUS_RELEASED: case DrydockLeaseStatus::STATUS_DESTROYED: throw new PhutilProxyException( pht( 'Unexpected failure while destroying lease ("%s").', $lease->getPHID()), $ex); } $lease ->setStatus(DrydockLeaseStatus::STATUS_BROKEN) ->save(); $lease->scheduleUpdate(); $lease->logEvent( DrydockLeaseActivationFailureLogType::LOGCONST, array( 'class' => get_class($ex), 'message' => $ex->getMessage(), )); throw new PhabricatorWorkerPermanentFailureException( pht( 'Permanent failure while activating lease ("%s"): %s', $lease->getPHID(), $ex->getMessage())); } /* -( Destroying Leases )-------------------------------------------------- */ /** * @task destroy */ private function destroyLease(DrydockLease $lease) { $resource = $lease->getResource(); if ($resource) { $blueprint = $resource->getBlueprint(); $blueprint->destroyLease($resource, $lease); } DrydockSlotLock::releaseLocks($lease->getPHID()); $lease ->setStatus(DrydockLeaseStatus::STATUS_DESTROYED) ->save(); $lease->logEvent(DrydockLeaseDestroyedLogType::LOGCONST); } } diff --git a/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php index f10f1a4a50..a1486af257 100644 --- a/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php @@ -1,160 +1,186 @@ getSettings(); // TODO: We should probably have a separate temporary storage area for // execution stuff that doesn't step on configuration state? $lease_phid = $build_target->getDetail('exec.leasePHID'); if ($lease_phid) { $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withPHIDs(array($lease_phid)) ->executeOne(); if (!$lease) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Lease "%s" could not be loaded.', $lease_phid)); } } else { $working_copy_type = id(new DrydockWorkingCopyBlueprintImplementation()) ->getType(); $lease = id(new DrydockLease()) ->setResourceType($working_copy_type) ->setOwnerPHID($build_target->getPHID()); $map = $this->buildRepositoryMap($build_target); $lease->setAttribute('repositories.map', $map); $task_id = $this->getCurrentWorkerTaskID(); if ($task_id) { $lease->setAwakenTaskIDs(array($task_id)); } $lease->queueForActivation(); $build_target ->setDetail('exec.leasePHID', $lease->getPHID()) ->save(); } if ($lease->isActivating()) { // TODO: Smart backoff? throw new PhabricatorWorkerYieldException(15); } if (!$lease->isActive()) { // TODO: We could just forget about this lease and retry? throw new PhabricatorWorkerPermanentFailureException( pht( 'Lease "%s" never activated.', $lease->getPHID())); } $artifact = $build_target->createArtifact( $viewer, $settings['name'], HarbormasterWorkingCopyArtifact::ARTIFACTCONST, array( 'drydockLeasePHID' => $lease->getPHID(), )); } public function getArtifactOutputs() { return array( array( 'name' => pht('Working Copy'), 'key' => $this->getSetting('name'), 'type' => HarbormasterWorkingCopyArtifact::ARTIFACTCONST, ), ); } public function getFieldSpecifications() { return array( 'name' => array( 'name' => pht('Artifact Name'), 'type' => 'text', 'required' => true, ), 'repositoryPHIDs' => array( 'name' => pht('Also Clone'), 'type' => 'datasource', 'datasource.class' => 'DiffusionRepositoryDatasource', ), ); } private function buildRepositoryMap(HarbormasterBuildTarget $build_target) { $viewer = PhabricatorUser::getOmnipotentUser(); $variables = $build_target->getVariables(); $repository_phid = idx($variables, 'repository.phid'); + if (!$repository_phid) { + throw new Exception( + pht( + 'Unable to determine how to clone the repository for this '. + 'buildable: it is not associated with a tracked repository.')); + } + $also_phids = $build_target->getFieldValue('repositoryPHIDs'); $all_phids = $also_phids; $all_phids[] = $repository_phid; $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs($all_phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); foreach ($all_phids as $phid) { if (empty($repositories[$phid])) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Unable to load repository with PHID "%s".', $phid)); } } - $commit = idx($variables, 'repository.commit'); - $map = array(); foreach ($also_phids as $also_phid) { $also_repo = $repositories[$also_phid]; $map[$also_repo->getCloneName()] = array( 'phid' => $also_repo->getPHID(), 'branch' => 'master', ); } $repository = $repositories[$repository_phid]; + $commit = idx($variables, 'repository.commit'); + $ref_uri = idx($variables, 'repository.staging.uri'); + $ref_ref = idx($variables, 'repository.staging.ref'); + if ($commit) { + $spec = array( + 'commit' => $commit, + ); + } else if ($ref_uri && $ref_ref) { + $spec = array( + 'ref' => array( + 'uri' => $ref_uri, + 'ref' => $ref_ref, + ), + ); + } else { + throw new Exception( + pht( + 'Unable to determine how to fetch changes: this buildable does not '. + 'identify a commit or a staging ref. You may need to configure a '. + 'repository staging area.')); + } + $directory = $repository->getCloneName(); $map[$directory] = array( 'phid' => $repository->getPHID(), - 'commit' => $commit, 'default' => true, - ); + ) + $spec; return $map; } } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php index 069b237e4d..6c2f5b4461 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php @@ -1,225 +1,233 @@ self::IDS_COUNTER, self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( 'dataID' => array( 'columns' => array('dataID'), 'unique' => true, ), 'taskClass' => array( 'columns' => array('taskClass'), ), 'leaseExpires' => array( 'columns' => array('leaseExpires'), ), 'leaseOwner' => array( 'columns' => array('leaseOwner(16)'), ), 'key_failuretime' => array( 'columns' => array('failureTime'), ), 'leaseOwner_2' => array( 'columns' => array('leaseOwner', 'priority', 'id'), ), ) + $parent[self::CONFIG_KEY_SCHEMA], ); $config[self::CONFIG_COLUMN_SCHEMA] = array( // T6203/NULLABILITY // This isn't nullable in the archive table, so at a minimum these // should be the same. 'dataID' => 'uint32?', ) + $parent[self::CONFIG_COLUMN_SCHEMA]; return $config + $parent; } public function setServerTime($server_time) { $this->serverTime = $server_time; $this->localTime = time(); return $this; } public function setLeaseDuration($lease_duration) { $this->checkLease(); $server_lease_expires = $this->serverTime + $lease_duration; $this->setLeaseExpires($server_lease_expires); // NOTE: This is primarily to allow unit tests to set negative lease // durations so they don't have to wait around for leases to expire. We // check that the lease is valid above. return $this->forceSaveWithoutLease(); } public function save() { $this->checkLease(); return $this->forceSaveWithoutLease(); } public function forceSaveWithoutLease() { $is_new = !$this->getID(); if ($is_new) { $this->failureCount = 0; } if ($is_new && ($this->getData() !== null)) { $data = new PhabricatorWorkerTaskData(); $data->setData($this->getData()); $data->save(); $this->setDataID($data->getID()); } return parent::save(); } protected function checkLease() { - if ($this->leaseOwner) { - $current_server_time = $this->serverTime + (time() - $this->localTime); - if ($current_server_time >= $this->leaseExpires) { - throw new Exception( - pht( - 'Trying to update Task %d (%s) after lease expiration!', - $this->getID(), - $this->getTaskClass())); - } + $owner = $this->leaseOwner; + + if (!$owner) { + return; + } + + if ($owner == PhabricatorWorker::YIELD_OWNER) { + return; + } + + $current_server_time = $this->serverTime + (time() - $this->localTime); + if ($current_server_time >= $this->leaseExpires) { + throw new Exception( + pht( + 'Trying to update Task %d (%s) after lease expiration!', + $this->getID(), + $this->getTaskClass())); } } public function delete() { throw new Exception( pht( 'Active tasks can not be deleted directly. '. 'Use %s to move tasks to the archive.', 'archiveTask()')); } public function archiveTask($result, $duration) { if ($this->getID() === null) { throw new Exception( pht("Attempting to archive a task which hasn't been saved!")); } $this->checkLease(); $archive = id(new PhabricatorWorkerArchiveTask()) ->setID($this->getID()) ->setTaskClass($this->getTaskClass()) ->setLeaseOwner($this->getLeaseOwner()) ->setLeaseExpires($this->getLeaseExpires()) ->setFailureCount($this->getFailureCount()) ->setDataID($this->getDataID()) ->setPriority($this->getPriority()) ->setObjectPHID($this->getObjectPHID()) ->setResult($result) ->setDuration($duration); // NOTE: This deletes the active task (this object)! $archive->save(); return $archive; } public function executeTask() { // We do this outside of the try .. catch because we don't have permission // to release the lease otherwise. $this->checkLease(); $did_succeed = false; $worker = null; try { $worker = $this->getWorkerInstance(); $worker->setCurrentWorkerTask($this); $maximum_failures = $worker->getMaximumRetryCount(); if ($maximum_failures !== null) { if ($this->getFailureCount() > $maximum_failures) { throw new PhabricatorWorkerPermanentFailureException( pht( 'Task % has exceeded the maximum number of failures (%d).', $this->getID(), $maximum_failures)); } } $lease = $worker->getRequiredLeaseTime(); if ($lease !== null) { $this->setLeaseDuration($lease); } $t_start = microtime(true); $worker->executeTask(); $t_end = microtime(true); $duration = (int)(1000000 * ($t_end - $t_start)); $result = $this->archiveTask( PhabricatorWorkerArchiveTask::RESULT_SUCCESS, $duration); $did_succeed = true; } catch (PhabricatorWorkerPermanentFailureException $ex) { $result = $this->archiveTask( PhabricatorWorkerArchiveTask::RESULT_FAILURE, 0); $result->setExecutionException($ex); } catch (PhabricatorWorkerYieldException $ex) { $this->setExecutionException($ex); $this->setLeaseOwner(PhabricatorWorker::YIELD_OWNER); $retry = $ex->getDuration(); $retry = max($retry, 5); // NOTE: As a side effect, this saves the object. $this->setLeaseDuration($retry); $result = $this; } catch (Exception $ex) { $this->setExecutionException($ex); $this->setFailureCount($this->getFailureCount() + 1); $this->setFailureTime(time()); $retry = null; if ($worker) { $retry = $worker->getWaitBeforeRetry($this); } $retry = coalesce( $retry, PhabricatorWorkerLeaseQuery::getDefaultWaitBeforeRetry()); // NOTE: As a side effect, this saves the object. $this->setLeaseDuration($retry); $result = $this; } // NOTE: If this throws, we don't want it to cause the task to fail again, // so execute it out here and just let the exception escape. if ($did_succeed) { foreach ($worker->getQueuedTasks() as $task) { list($class, $data) = $task; PhabricatorWorker::scheduleTask( $class, $data, array( 'priority' => (int)$this->getPriority(), )); } } return $result; } }