diff --git a/src/applications/drydock/operation/DrydockRepositoryOperationType.php b/src/applications/drydock/operation/DrydockRepositoryOperationType.php index 7ec3689aff..32b562a303 100644 --- a/src/applications/drydock/operation/DrydockRepositoryOperationType.php +++ b/src/applications/drydock/operation/DrydockRepositoryOperationType.php @@ -1,43 +1,66 @@ viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } + final public function setOperation(DrydockRepositoryOperation $operation) { + $this->operation = $operation; + return $this; + } + + final public function getOperation() { + return $this->operation; + } + + final public function setInterface(DrydockInterface $interface) { + $this->interface = $interface; + return $this; + } + + final public function getInterface() { + if (!$this->interface) { + throw new PhutilInvalidStateException('setInterface'); + } + return $this->interface; + } + final public function getOperationConstant() { return $this->getPhobjectClassConstant('OPCONST', 32); } final public static function getAllOperationTypes() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getOperationConstant') ->execute(); } } diff --git a/src/applications/drydock/query/DrydockRepositoryOperationQuery.php b/src/applications/drydock/query/DrydockRepositoryOperationQuery.php index fb1f6583a8..a5fbe0acc9 100644 --- a/src/applications/drydock/query/DrydockRepositoryOperationQuery.php +++ b/src/applications/drydock/query/DrydockRepositoryOperationQuery.php @@ -1,184 +1,189 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withObjectPHIDs(array $object_phids) { $this->objectPHIDs = $object_phids; return $this; } public function withRepositoryPHIDs(array $repository_phids) { $this->repositoryPHIDs = $repository_phids; return $this; } public function withOperationStates(array $states) { $this->operationStates = $states; return $this; } public function withOperationTypes(array $types) { $this->operationTypes = $types; return $this; } public function withIsDismissed($dismissed) { $this->isDismissed = $dismissed; return $this; } public function withAuthorPHIDs(array $phids) { $this->authorPHIDs = $phids; return $this; } public function newResultObject() { return new DrydockRepositoryOperation(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $operations) { $implementations = DrydockRepositoryOperationType::getAllOperationTypes(); + $viewer = $this->getViewer(); + foreach ($operations as $key => $operation) { $impl = idx($implementations, $operation->getOperationType()); if (!$impl) { $this->didRejectResult($operation); unset($operations[$key]); continue; } - $impl = clone $impl; + $impl = id(clone $impl) + ->setViewer($viewer) + ->setOperation($operation); + $operation->attachImplementation($impl); } $repository_phids = mpull($operations, 'getRepositoryPHID'); if ($repository_phids) { $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($repository_phids) ->execute(); $repositories = mpull($repositories, null, 'getPHID'); } else { $repositories = array(); } foreach ($operations as $key => $operation) { $repository = idx($repositories, $operation->getRepositoryPHID()); if (!$repository) { $this->didRejectResult($operation); unset($operations[$key]); continue; } $operation->attachRepository($repository); } return $operations; } protected function didFilterPage(array $operations) { $object_phids = mpull($operations, 'getObjectPHID'); if ($object_phids) { $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); } else { $objects = array(); } foreach ($operations as $key => $operation) { $object = idx($objects, $operation->getObjectPHID()); $operation->attachObject($object); } return $operations; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, 'phid IN (%Ls)', $this->phids); } if ($this->objectPHIDs !== null) { $where[] = qsprintf( $conn, 'objectPHID IN (%Ls)', $this->objectPHIDs); } if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } if ($this->operationStates !== null) { $where[] = qsprintf( $conn, 'operationState IN (%Ls)', $this->operationStates); } if ($this->operationTypes !== null) { $where[] = qsprintf( $conn, 'operationType IN (%Ls)', $this->operationTypes); } if ($this->isDismissed !== null) { $where[] = qsprintf( $conn, 'isDismissed = %d', (int)$this->isDismissed); } if ($this->authorPHIDs !== null) { $where[] = qsprintf( $conn, 'authorPHID IN (%Ls)', $this->authorPHIDs); } return $where; } } diff --git a/src/applications/drydock/storage/DrydockRepositoryOperation.php b/src/applications/drydock/storage/DrydockRepositoryOperation.php index 4e25280c3f..809a10a116 100644 --- a/src/applications/drydock/storage/DrydockRepositoryOperation.php +++ b/src/applications/drydock/storage/DrydockRepositoryOperation.php @@ -1,237 +1,237 @@ setOperationState(self::STATE_WAIT) ->setOperationType($op->getOperationConstant()) ->setIsDismissed(0); } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'repositoryTarget' => 'bytes', 'operationType' => 'text32', 'operationState' => 'text32', 'isDismissed' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_object' => array( 'columns' => array('objectPHID'), ), 'key_repository' => array( 'columns' => array('repositoryPHID', 'operationState'), ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( DrydockRepositoryOperationPHIDType::TYPECONST); } public function attachRepository(PhabricatorRepository $repository) { $this->repository = $repository; return $this; } public function getRepository() { return $this->assertAttached($this->repository); } public function attachObject($object) { $this->object = $object; return $this; } public function getObject() { return $this->assertAttached($this->object); } public function attachImplementation(DrydockRepositoryOperationType $impl) { $this->implementation = $impl; return $this; } public function getImplementation() { return $this->implementation; } public function getProperty($key, $default = null) { return idx($this->properties, $key, $default); } public function setProperty($key, $value) { $this->properties[$key] = $value; return $this; } public static function getOperationStateNameMap() { return array( self::STATE_WAIT => pht('Waiting'), self::STATE_WORK => pht('Working'), self::STATE_DONE => pht('Done'), self::STATE_FAIL => pht('Failed'), ); } public static function getOperationStateIcon($state) { $map = array( self::STATE_WAIT => 'fa-clock-o', self::STATE_WORK => 'fa-plane ph-spin blue', self::STATE_DONE => 'fa-check green', self::STATE_FAIL => 'fa-times red', ); return idx($map, $state, null); } public static function getOperationStateName($state) { $map = self::getOperationStateNameMap(); return idx($map, $state, pht('', $state)); } public function scheduleUpdate() { PhabricatorWorker::scheduleTask( 'DrydockRepositoryOperationUpdateWorker', array( 'operationPHID' => $this->getPHID(), ), array( 'objectPHID' => $this->getPHID(), 'priority' => PhabricatorWorker::PRIORITY_ALERTS, )); } public function applyOperation(DrydockInterface $interface) { - return $this->getImplementation()->applyOperation( - $this, - $interface); + $impl = $this->getImplementation(); + $impl->setInterface($interface); + return $impl->applyOperation($this, $interface); } public function getOperationDescription(PhabricatorUser $viewer) { return $this->getImplementation()->getOperationDescription( $this, $viewer); } public function getOperationCurrentStatus(PhabricatorUser $viewer) { return $this->getImplementation()->getOperationCurrentStatus( $this, $viewer); } public function isUnderway() { switch ($this->getOperationState()) { case self::STATE_WAIT: case self::STATE_WORK: return true; } return false; } public function isDone() { return ($this->getOperationState() === self::STATE_DONE); } public function getWorkingCopyMerges() { return $this->getImplementation()->getWorkingCopyMerges( $this); } public function setWorkingCopyLeasePHID($lease_phid) { return $this->setProperty('exec.leasePHID', $lease_phid); } public function getWorkingCopyLeasePHID() { return $this->getProperty('exec.leasePHID'); } public function setCommandError(array $error) { return $this->setProperty('exec.workingcopy.error', $error); } public function getCommandError() { return $this->getProperty('exec.workingcopy.error'); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { $need_capability = $this->getRequiredRepositoryCapability($capability); return $this->getRepository() ->getPolicy($need_capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { $need_capability = $this->getRequiredRepositoryCapability($capability); return $this->getRepository() ->hasAutomaticCapability($need_capability, $viewer); } public function describeAutomaticCapability($capability) { return pht( 'A repository operation inherits the policies of the repository it '. 'affects.'); } private function getRequiredRepositoryCapability($capability) { // To edit a RepositoryOperation, require that the user be able to push // to the repository. $map = array( PhabricatorPolicyCapability::CAN_EDIT => DiffusionPushCapability::CAPABILITY, ); return idx($map, $capability, $capability); } } diff --git a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php index dfd603fd12..2ee9324fb6 100644 --- a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php +++ b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php @@ -1,189 +1,184 @@ getTaskDataValue('operationPHID'); $hash = PhabricatorHash::digestForIndex($operation_phid); $lock_key = 'drydock.operation:'.$hash; $lock = PhabricatorGlobalLock::newLock($lock_key) ->lock(1); try { $operation = $this->loadOperation($operation_phid); $this->handleUpdate($operation); } catch (Exception $ex) { $lock->unlock(); throw $ex; } $lock->unlock(); } private function handleUpdate(DrydockRepositoryOperation $operation) { - $viewer = $this->getViewer(); - $operation_state = $operation->getOperationState(); switch ($operation_state) { case DrydockRepositoryOperation::STATE_WAIT: $operation ->setOperationState(DrydockRepositoryOperation::STATE_WORK) ->save(); break; case DrydockRepositoryOperation::STATE_WORK: break; case DrydockRepositoryOperation::STATE_DONE: case DrydockRepositoryOperation::STATE_FAIL: // No more processing for these requests. return; } // TODO: We should probably check for other running operations with lower // IDs and the same repository target and yield to them here? That is, // enforce sequential evaluation of operations against the same target so // that if you land "A" and then land "B", we always finish "A" first. // For now, just let stuff happen in any order. We can't lease until // we know we're good to move forward because we might deadlock if we do: // we're waiting for another operation to complete, and that operation is // waiting for a lease we're holding. try { - $operation->getImplementation() - ->setViewer($viewer); - $lease = $this->loadWorkingCopyLease($operation); $interface = $lease->getInterface( DrydockCommandInterface::INTERFACE_TYPE); // No matter what happens here, destroy the lease away once we're done. $lease->setReleaseOnDestruction(true); $operation->applyOperation($interface); } catch (PhabricatorWorkerYieldException $ex) { throw $ex; } catch (Exception $ex) { $operation ->setOperationState(DrydockRepositoryOperation::STATE_FAIL) ->save(); throw $ex; } $operation ->setOperationState(DrydockRepositoryOperation::STATE_DONE) ->save(); // TODO: Once we have sequencing, we could awaken the next operation // against this target after finishing or failing. } private function loadWorkingCopyLease( DrydockRepositoryOperation $operation) { $viewer = $this->getViewer(); // TODO: This is very similar to leasing in Harbormaster, maybe we can // share some of the logic? $working_copy = new DrydockWorkingCopyBlueprintImplementation(); $working_copy_type = $working_copy->getType(); $lease_phid = $operation->getProperty('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 { $repository = $operation->getRepository(); $allowed_phids = $repository->getAutomationBlueprintPHIDs(); $authorizing_phid = $repository->getPHID(); $lease = DrydockLease::initializeNewLease() ->setResourceType($working_copy_type) ->setOwnerPHID($operation->getPHID()) ->setAuthorizingPHID($authorizing_phid) ->setAllowedBlueprintPHIDs($allowed_phids); $map = $this->buildRepositoryMap($operation); $lease->setAttribute('repositories.map', $map); $task_id = $this->getCurrentWorkerTaskID(); if ($task_id) { $lease->setAwakenTaskIDs(array($task_id)); } $operation ->setWorkingCopyLeasePHID($lease->getPHID()) ->save(); $lease->queueForActivation(); } if ($lease->isActivating()) { throw new PhabricatorWorkerYieldException(15); } if (!$lease->isActive()) { $vcs_error = $working_copy->getCommandError($lease); if ($vcs_error) { $operation ->setCommandError($vcs_error) ->save(); } throw new PhabricatorWorkerPermanentFailureException( pht( 'Lease "%s" never activated.', $lease->getPHID())); } return $lease; } private function buildRepositoryMap(DrydockRepositoryOperation $operation) { $repository = $operation->getRepository(); $target = $operation->getRepositoryTarget(); list($type, $name) = explode(':', $target, 2); switch ($type) { case 'branch': $spec = array( 'branch' => $name, ); break; case 'none': $spec = array(); break; default: throw new Exception( pht( 'Unknown repository operation target type "%s" (in target "%s").', $type, $target)); } $spec['merges'] = $operation->getWorkingCopyMerges(); $map = array(); $map[$repository->getCloneName()] = array( 'phid' => $repository->getPHID(), 'default' => true, ) + $spec; return $map; } }