diff --git a/scripts/drydock/drydock_control.php b/scripts/drydock/drydock_control.php index 21ff9f8ee8..f22032b286 100755 --- a/scripts/drydock/drydock_control.php +++ b/scripts/drydock/drydock_control.php @@ -1,21 +1,21 @@ #!/usr/bin/env php setTagline(pht('manage drydock software resources')); $args->setSynopsis(<<parseStandardArguments(); $workflows = id(new PhutilClassMapQuery()) ->setAncestorClass('DrydockManagementWorkflow') ->execute(); $workflows[] = new PhutilHelpArgumentWorkflow(); $args->parseWorkflows($workflows); diff --git a/scripts/init/init-script-with-signals.php b/scripts/init/init-script-with-signals.php new file mode 100644 index 0000000000..a479c4b758 --- /dev/null +++ b/scripts/init/init-script-with-signals.php @@ -0,0 +1,11 @@ +getViewer(); if ($this->shouldLimitAllocatingPoolSize($blueprint)) { return false; } // TODO: If we have a pending resource which is compatible with the // configuration for this lease, prevent a new allocation? Otherwise the // queue can fill up with copies of requests from the same lease. But // maybe we can deal with this with "pre-leasing"? return true; } public function canAcquireLeaseOnResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { // Don't hand out leases on working copies which have not activated, since // it may take an arbitrarily long time for them to acquire a host. if (!$resource->isActive()) { return false; } $need_map = $lease->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(); $blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs'); $host_lease = $this->newLease($blueprint) ->setResourceType('host') ->setOwnerPHID($resource_phid) ->setAttribute('workingcopy.resourcePHID', $resource_phid) ->setAllowedBlueprintPHIDs($blueprint_phids); $resource->setAttribute('host.leasePHID', $host_lease->getPHID()); $map = $lease->getAttribute('repositories.map'); foreach ($map as $key => $value) { $map[$key] = array_select_keys( $value, array( 'phid', )); } $resource->setAttribute('repositories.map', $map); $slot_lock = $this->getConcurrentResourceLimitSlotLock($blueprint); if ($slot_lock !== null) { $resource->needSlotLock($slot_lock); } $resource->allocateResource(); $host_lease->queueForActivation(); return $resource; } 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) { try { $lease = $this->loadHostLease($resource); } catch (Exception $ex) { // If we can't load the lease, assume we don't need to take any actions // to destroy it. return; } // Destroy the lease on the host. - $lease->releaseOnDestruction(); + $lease->setReleaseOnDestruction(true); 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) { $interface->pushWorkingDirectory("{$root}/repo/{$directory}/"); $cmd = array(); $arg = array(); $cmd[] = 'git clean -d --force'; $cmd[] = 'git fetch'; $commit = idx($spec, 'commit'); $branch = idx($spec, 'branch'); $ref = idx($spec, 'ref'); // Reset things first, in case previous builds left anything staged or // dirty. $cmd[] = 'git reset --hard HEAD'; if ($commit !== null) { $cmd[] = 'git checkout %s --'; $arg[] = $commit; } else if ($branch !== null) { $cmd[] = 'git checkout %s --'; $arg[] = $branch; $cmd[] = 'git reset --hard origin/%s'; $arg[] = $branch; } $this->execxv($interface, $cmd, $arg); if (idx($spec, 'default')) { $default = $directory; } // If we're fetching a ref from a remote, do that separately so we can // raise a more tailored error. if ($ref) { $cmd = array(); $arg = array(); $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; try { $this->execxv($interface, $cmd, $arg); } catch (CommandException $ex) { $display_command = csprintf( 'git fetch %R %R', $ref_uri, $ref_ref); $error = DrydockCommandError::newFromCommandException($ex) ->setPhase(self::PHASE_REMOTEFETCH) ->setDisplayCommand($display_command); $lease->setAttribute( 'workingcopy.vcs.error', $error->toDictionary()); throw $ex; } } $merges = idx($spec, 'merges'); if ($merges) { foreach ($merges as $merge) { $this->applyMerge($lease, $interface, $merge); } } $interface->popWorkingDirectory(); } 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->pushWorkingDirectory($path); return $command_interface; } } private function loadRepositories(array $phids) { $viewer = $this->getViewer(); $repositories = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->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 = $this->getViewer(); $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; } protected function getCustomFieldSpecifications() { return array( 'blueprintPHIDs' => array( 'name' => pht('Use Blueprints'), 'type' => 'blueprints', 'required' => true, ), ); } protected function shouldUseConcurrentResourceLimit() { return true; } private function applyMerge( DrydockLease $lease, DrydockCommandInterface $interface, array $merge) { $src_uri = $merge['src.uri']; $src_ref = $merge['src.ref']; try { $interface->execx( 'git fetch --no-tags -- %s +%s:%s', $src_uri, $src_ref, $src_ref); } catch (CommandException $ex) { $display_command = csprintf( 'git fetch %R +%R:%R', $src_uri, $src_ref, $src_ref); $error = DrydockCommandError::newFromCommandException($ex) ->setPhase(self::PHASE_MERGEFETCH) ->setDisplayCommand($display_command); $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary()); throw $ex; } // NOTE: This can never actually generate a commit because we pass // "--squash", but git sometimes runs code to check that a username and // email are configured anyway. $real_command = csprintf( 'git -c user.name=%s -c user.email=%s merge --no-stat --squash -- %R', 'drydock', 'drydock@phabricator', $src_ref); try { $interface->execx('%C', $real_command); } catch (CommandException $ex) { $display_command = csprintf( 'git merge --squash %R', $src_ref); $error = DrydockCommandError::newFromCommandException($ex) ->setPhase(self::PHASE_SQUASHMERGE) ->setDisplayCommand($display_command); $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary()); throw $ex; } } public function getCommandError(DrydockLease $lease) { return $lease->getAttribute('workingcopy.vcs.error'); } private function execxv( DrydockCommandInterface $interface, array $commands, array $arguments) { $commands = implode(' && ', $commands); $argv = array_merge(array($commands), $arguments); return call_user_func_array(array($interface, 'execx'), $argv); } } diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php index 069f82339b..3a27fe9045 100644 --- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php +++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php @@ -1,178 +1,208 @@ setName('lease') ->setSynopsis(pht('Lease a resource.')) ->setArguments( array( array( 'name' => 'type', 'param' => 'resource_type', 'help' => pht('Resource type.'), ), array( 'name' => 'until', 'param' => 'time', 'help' => pht('Set lease expiration time.'), ), array( 'name' => 'attributes', 'param' => 'name=value,...', 'help' => pht('Resource specification.'), ), )); } public function execute(PhutilArgumentParser $args) { $viewer = $this->getViewer(); $resource_type = $args->getArg('type'); if (!$resource_type) { throw new PhutilArgumentUsageException( pht( 'Specify a resource type with `%s`.', '--type')); } $until = $args->getArg('until'); if (strlen($until)) { $until = strtotime($until); if ($until <= 0) { throw new PhutilArgumentUsageException( pht( 'Unable to parse argument to "%s".', '--until')); } } $attributes = $args->getArg('attributes'); if ($attributes) { $options = new PhutilSimpleOptions(); $options->setCaseSensitive(true); $attributes = $options->parse($attributes); } $lease = id(new DrydockLease()) ->setResourceType($resource_type); $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); $lease->setAuthorizingPHID($drydock_phid); // TODO: This is not hugely scalable, although this is a debugging workflow // so maybe it's fine. Do we even need `bin/drydock lease` in the long run? $all_blueprints = id(new DrydockBlueprintQuery()) ->setViewer($viewer) ->execute(); $allowed_phids = mpull($all_blueprints, 'getPHID'); if (!$allowed_phids) { throw new Exception( pht( 'No blueprints exist which can plausibly allocate resources to '. 'satisfy the requested lease.')); } $lease->setAllowedBlueprintPHIDs($allowed_phids); if ($attributes) { $lease->setAttributes($attributes); } if ($until) { $lease->setUntil($until); } + // If something fatals or the user interrupts the process (for example, + // with "^C"), release the lease. We'll cancel this below, if the lease + // actually activates. + $lease->setReleaseOnDestruction(true); + + // TODO: This would probably be better handled with PhutilSignalRouter, + // but it currently doesn't route SIGINT. We're initializing it to setup + // SIGTERM handling and make eventual migration easier. + $router = PhutilSignalRouter::getRouter(); + pcntl_signal(SIGINT, array($this, 'didReceiveInterrupt')); + + $t_start = microtime(true); $lease->queueForActivation(); echo tsprintf( - "%s\n", + "%s\n\n __%s__\n\n%s\n", + pht('Queued lease for activation:'), + PhabricatorEnv::getProductionURI($lease->getURI()), pht('Waiting for daemons to activate lease...')); $this->waitUntilActive($lease); + // Now that we've survived activation and the lease is good, make it + // durable. + $lease->setReleaseOnDestruction(false); + $t_end = microtime(true); + echo tsprintf( - "%s\n", - pht('Activated lease "%s".', $lease->getID())); + "%s\n\n %s\n\n%s\n", + pht( + 'Activation complete. This lease is permanent until manually '. + 'released with:'), + pht('$ ./bin/drydock release-lease --id %d', $lease->getID()), + pht( + 'Lease activated in %sms.', + new PhutilNumber((int)(($t_end - $t_start) * 1000)))); return 0; } + public function didReceiveInterrupt($signo) { + // Doing this makes us run destructors, particularly the "release on + // destruction" trigger on the lease. + exit(128 + $signo); + } private function waitUntilActive(DrydockLease $lease) { $viewer = $this->getViewer(); $log_cursor = 0; $log_types = DrydockLogType::getAllLogTypes(); $is_active = false; while (!$is_active) { $lease->reload(); // While we're waiting, show the user any logs which the daemons have // generated to give them some clue about what's going on. $logs = id(new DrydockLogQuery()) ->setViewer($viewer) ->withLeasePHIDs(array($lease->getPHID())) ->setBeforeID($log_cursor) ->execute(); if ($logs) { $logs = mpull($logs, null, 'getID'); ksort($logs); $log_cursor = last_key($logs); } foreach ($logs as $log) { $type_key = $log->getType(); if (isset($log_types[$type_key])) { $type_object = id(clone $log_types[$type_key]) ->setLog($log) ->setViewer($viewer); $log_data = $log->getData(); $type = $type_object->getLogTypeName(); $data = $type_object->renderLog($log_data); } else { $type = pht('Unknown ("%s")', $type_key); $data = null; } echo tsprintf( "<%s> %B\n", $type, $data); } $status = $lease->getStatus(); switch ($status) { case DrydockLeaseStatus::STATUS_ACTIVE: $is_active = true; break; 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)); } if ($is_active) { break; } else { sleep(1); } } } } diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index e8cdc5b802..e217223aa8 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -1,471 +1,476 @@ setPHID($lease->generatePHID()); return $lease; } /** * Flag this lease to be released when its destructor is called. This is * mostly useful if you have a script which acquires, uses, and then releases * a lease, as you don't need to explicitly handle exceptions to properly * release the lease. */ - public function releaseOnDestruction() { - $this->releaseOnDestruction = true; + public function setReleaseOnDestruction($release) { + $this->releaseOnDestruction = $release; 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'), ), 'key_status' => array( 'columns' => array('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!')); } if (!$this->getAuthorizingPHID()) { throw new Exception( pht( 'Trying to queue a lease for activation without an authorizing '. 'object. Use "%s" to specify the PHID of the authorizing object. '. 'The authorizing object must be approved to use the allowed '. 'blueprints.', 'setAuthorizingPHID()')); } if (!$this->getAllowedBlueprintPHIDs()) { throw new Exception( pht( 'Trying to queue a lease for activation without any allowed '. 'Blueprints. Use "%s" to specify allowed blueprints. The '. 'authorizing object must be approved to use the allowed blueprints.', 'setAllowedBlueprintPHIDs()')); } $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 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 { DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); $this->slotLocks = array(); } catch (DrydockSlotLockException $ex) { $this->killTransaction(); $this->logEvent( DrydockSlotLockFailureLogType::LOGCONST, array( 'locks' => $ex->getLockMap(), )); throw $ex; } $this ->setResourcePHID($resource->getPHID()) ->attachResource($resource) ->setStatus($new_status) ->save(); $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(); try { DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); $this->slotLocks = array(); } catch (DrydockSlotLockException $ex) { $this->killTransaction(); $this->logEvent( DrydockSlotLockFailureLogType::LOGCONST, array( 'locks' => $ex->getLockMap(), )); throw $ex; } $this ->setStatus(DrydockLeaseStatus::STATUS_ACTIVE) ->save(); $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; } public function setAllowedBlueprintPHIDs(array $phids) { $this->setAttribute('internal.blueprintPHIDs', $phids); return $this; } public function getAllowedBlueprintPHIDs() { return $this->getAttribute('internal.blueprintPHIDs', array()); } 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); } $this->awakenTasks(); } 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(); } /** * Awaken yielded tasks after a state change. * * @return this */ public function awakenTasks() { $awaken_ids = $this->getAttribute('internal.awakenTaskIDs'); if (is_array($awaken_ids) && $awaken_ids) { PhabricatorWorker::awakenTaskIDs($awaken_ids); } return $this; } + public function getURI() { + $id = $this->getID(); + return "/drydock/lease/{$id}/"; + } + /* -( 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/worker/DrydockRepositoryOperationUpdateWorker.php b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php index 9038b3b6d7..dfd603fd12 100644 --- a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php +++ b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php @@ -1,189 +1,189 @@ 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->releaseOnDestruction(true); + $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; } }