diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -797,6 +797,7 @@ 'DoorkeeperTagView' => 'applications/doorkeeper/view/DoorkeeperTagView.php', 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 'DrydockAllocatorWorker' => 'applications/drydock/worker/DrydockAllocatorWorker.php', + 'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', 'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php', 'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php', @@ -845,7 +846,6 @@ 'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php', 'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php', 'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php', - 'DrydockManagementCreateResourceWorkflow' => 'applications/drydock/management/DrydockManagementCreateResourceWorkflow.php', 'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php', 'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', @@ -4502,6 +4502,7 @@ 'DoorkeeperTagView' => 'AphrontView', 'DoorkeeperTagsController' => 'PhabricatorController', 'DrydockAllocatorWorker' => 'PhabricatorWorker', + 'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', 'DrydockBlueprint' => array( 'DrydockDAO', @@ -4564,7 +4565,6 @@ 'DrydockLogQuery' => 'DrydockQuery', 'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow', - 'DrydockManagementCreateResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php new file mode 100644 --- /dev/null +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -0,0 +1,234 @@ +loadServices($blueprint); + $bindings = $this->loadAllBindings($services); + + if (!$bindings) { + // If there are no devices bound to the services for this blueprint, + // we can not allocate resources. + return false; + } + + return true; + } + + public function canAllocateResourceForLease( + DrydockBlueprint $blueprint, + DrydockLease $lease) { + + // We will only allocate one resource per unique device bound to the + // services for this blueprint. Make sure we have a free device somewhere. + $free_bindings = $this->loadFreeBindings($blueprint); + if (!$free_bindings) { + return false; + } + + return true; + } + + public function allocateResource( + DrydockBlueprint $blueprint, + DrydockLease $lease) { + + $free_bindings = $this->loadFreeBindings($blueprint); + shuffle($free_bindings); + + $exceptions = array(); + foreach ($free_bindings as $binding) { + $device = $binding->getDevice(); + $device_name = $device->getName(); + + $resource = $this->newResourceTemplate($blueprint, $device_name) + ->setActivateWhenAllocated(true) + ->setAttribute('almanacServicePHID', $binding->getServicePHID()) + ->setAttribute('almanacBindingPHID', $binding->getPHID()); + + // TODO: This algorithm can race, and the "free" binding may not be + // free by the time we acquire it. Do slot-locking here if that works + // out, or some other kind of locking if it does not. + + try { + return $resource->allocateResource(DrydockResourceStatus::STATUS_OPEN); + } catch (Exception $ex) { + $exceptions[] = $ex; + } + } + + throw new PhutilAggregateException( + pht('Unable to allocate any binding as a resource.'), + $exceptions); + } + + public function canAcquireLeaseOnResource( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + + // TODO: We'll currently lease each resource an unlimited number of times, + // but should stop doing that. + + return true; + } + + public function acquireLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + + // TODO: Once we have limit rules, we should perform slot locking (or other + // kinds of locking) here. + + $lease + ->setActivateWhenAcquired(true) + ->acquireOnResource($resource); + } + + public function getType() { + return 'host'; + } + + public function getInterface( + DrydockResource $resource, + DrydockLease $lease, + $type) { + // TODO: Actually do stuff here, this needs work and currently makes this + // entire exercise pointless. + } + + public function getFieldSpecifications() { + return array( + 'almanacServicePHIDs' => array( + 'name' => pht('Almanac Services'), + 'type' => 'datasource', + 'datasource.class' => 'AlmanacServiceDatasource', + 'datasource.parameters' => array( + 'serviceClasses' => $this->getAlmanacServiceClasses(), + ), + 'required' => true, + ), + 'credentialPHID' => array( + 'name' => pht('Credentials'), + 'type' => 'credential', + 'credential.provides' => + PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE, + 'credential.type' => + PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE, + ), + ) + parent::getFieldSpecifications(); + } + + private function loadServices(DrydockBlueprint $blueprint) { + if (!$this->services) { + $service_phids = $blueprint->getFieldValue('almanacServicePHIDs'); + if (!$service_phids) { + throw new Exception( + pht( + 'This blueprint ("%s") does not define any Almanac Service PHIDs.', + $blueprint->getBlueprintName())); + } + + $viewer = PhabricatorUser::getOmnipotentUser(); + $services = id(new AlmanacServiceQuery()) + ->setViewer($viewer) + ->withPHIDs($service_phids) + ->withServiceClasses($this->getAlmanacServiceClasses()) + ->needBindings(true) + ->execute(); + $services = mpull($services, null, 'getPHID'); + + if (count($services) != count($service_phids)) { + $missing_phids = array_diff($service_phids, array_keys($services)); + throw new Exception( + pht( + 'Some of the Almanac Services defined by this blueprint '. + 'could not be loaded. They may be invalid, no longer exist, '. + 'or be of the wrong type: %s.', + implode(', ', $missing_phids))); + } + + $this->services = $services; + } + + return $this->services; + } + + private function loadAllBindings(array $services) { + assert_instances_of($services, 'AlmanacService'); + $bindings = array_mergev(mpull($services, 'getBindings')); + return mpull($bindings, null, 'getPHID'); + } + + private function loadFreeBindings(DrydockBlueprint $blueprint) { + if ($this->freeBindings === null) { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $pool = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(array($blueprint->getPHID())) + ->withStatuses( + array( + DrydockResourceStatus::STATUS_PENDING, + DrydockResourceStatus::STATUS_OPEN, + DrydockResourceStatus::STATUS_CLOSED, + )) + ->execute(); + + $allocated_phids = array(); + foreach ($pool as $resource) { + $allocated_phids[] = $resource->getAttribute('almanacDevicePHID'); + } + $allocated_phids = array_fuse($allocated_phids); + + $services = $this->loadServices($blueprint); + $bindings = $this->loadAllBindings($services); + + $free = array(); + foreach ($bindings as $binding) { + if (empty($allocated_phids[$binding->getPHID()])) { + $free[] = $binding; + } + } + + $this->freeBindings = $free; + } + + return $this->freeBindings; + } + + private function getAlmanacServiceClasses() { + return array( + 'AlmanacDrydockPoolServiceType', + ); + } + + +} diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -60,10 +60,6 @@ return array(); } - public function getDetail($key, $default = null) { - return $this->getInstance()->getDetail($key, $default); - } - /* -( Lease Acquisition )-------------------------------------------------- */ @@ -86,171 +82,29 @@ * @return bool True if the resource and lease are compatible. * @task lease */ - abstract public function canAllocateLeaseOnResource( + abstract public function canAcquireLeaseOnResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease); /** - * @task lease - */ - final public function allocateLease( - DrydockResource $resource, - DrydockLease $lease) { - - $scope = $this->pushActiveScope($resource, $lease); - - $this->log(pht('Trying to Allocate Lease')); - - $lease->setStatus(DrydockLeaseStatus::STATUS_ACQUIRING); - $lease->setResourceID($resource->getID()); - $lease->attachResource($resource); - - $ephemeral_lease = id(clone $lease)->makeEphemeral(); - - $allocated = false; - $allocation_exception = null; - - $resource->openTransaction(); - $resource->beginReadLocking(); - $resource->reload(); - - // TODO: Policy stuff. - $other_leases = id(new DrydockLease())->loadAllWhere( - 'status IN (%Ld) AND resourceID = %d', - array( - DrydockLeaseStatus::STATUS_ACQUIRING, - DrydockLeaseStatus::STATUS_ACTIVE, - ), - $resource->getID()); - - try { - $allocated = $this->shouldAllocateLease( - $resource, - $ephemeral_lease, - $other_leases); - } catch (Exception $ex) { - $allocation_exception = $ex; - } - - if ($allocated) { - $lease->save(); - } - $resource->endReadLocking(); - if ($allocated) { - $resource->saveTransaction(); - $this->log(pht('Allocated Lease')); - } else { - $resource->killTransaction(); - $this->log(pht('Failed to Allocate Lease')); - } - - if ($allocation_exception) { - $this->logException($allocation_exception); - } - - return $allocated; - } - - - /** - * Enforce lease limits on resources. Allows resources to reject leases if - * they would become over-allocated by accepting them. - * - * For example, if a resource represents disk space, this method might check - * how much space the lease is asking for (say, 200MB) and how much space is - * left unallocated on the resource. It could grant the lease (return true) - * if it has enough remaining space (more than 200MB), and reject the lease - * (return false) if it does not (less than 200MB). - * - * A resource might also allow only exclusive leases. In this case it could - * accept a new lease (return true) if there are no active leases, or reject - * the new lease (return false) if there any other leases. - * - * A lock is held on the resource while this method executes to prevent - * multiple processes from allocating leases on the resource simultaneously. - * However, this means you should implement the method as cheaply as possible. - * In particular, do not perform any actual acquisition or setup in this - * method. - * - * If allocation is permitted, the lease will be moved to `ACQUIRING` status - * and @{method:executeAcquireLease} will be called to actually perform - * acquisition. - * - * General compatibility checks unrelated to resource limits and capacity are - * better implemented in @{method:canAllocateLease}, which serves as a - * cheap filter before lock acquisition. - * - * @param DrydockResource Candidate resource to allocate the lease on. - * @param DrydockLease Pending lease that wants to allocate here. - * @param list Other allocated and acquired leases on the - * resource. The implementation can inspect them - * to verify it can safely add the new lease. - * @return bool True to allocate the lease on the resource; - * false to reject it. - * @task lease - */ - abstract protected function shouldAllocateLease( - DrydockResource $resource, - DrydockLease $lease, - array $other_leases); - - - /** - * @task lease - */ - final public function acquireLease( - DrydockResource $resource, - DrydockLease $lease) { - - $scope = $this->pushActiveScope($resource, $lease); - - $this->log(pht('Acquiring Lease')); - $lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE); - $lease->setResourceID($resource->getID()); - $lease->attachResource($resource); - - $ephemeral_lease = id(clone $lease)->makeEphemeral(); - - try { - $this->executeAcquireLease($resource, $ephemeral_lease); - } catch (Exception $ex) { - $this->logException($ex); - throw $ex; - } - - $lease->setAttributes($ephemeral_lease->getAttributes()); - $lease->save(); - $this->log(pht('Acquired Lease')); - } - - - /** - * Acquire and activate an allocated lease. Allows resources to peform setup - * as leases are brought online. - * - * Following a successful call to @{method:canAllocateLease}, a lease is moved - * to `ACQUIRING` status and this method is called after resource locks are - * released. Nothing is locked while this method executes; the implementation - * is free to perform expensive operations like writing files and directories, - * executing commands, etc. - * - * After this method executes, the lease status is moved to `ACTIVE` and the - * original leasee may access it. + * Acquire a lease. Allows resources to peform setup as leases are brought + * online. * * If acquisition fails, throw an exception. * - * @param DrydockResource Resource to acquire a lease on. - * @param DrydockLease Lease to acquire. - * @return void + * @param DrydockBlueprint Blueprint which built the resource. + * @param DrydockResource Resource to acquire a lease on. + * @param DrydockLease Requested lease. + * @return void + * @task lease */ - abstract protected function executeAcquireLease( + abstract public function acquireLease( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease); - - final public function releaseLease( DrydockResource $resource, DrydockLease $lease) { @@ -352,6 +206,7 @@ * @param DrydockLease Requested lease. * @return bool True if this blueprint appears likely to be able to allocate * a suitable resource. + * @task resource */ abstract public function canAllocateResourceForLease( DrydockBlueprint $blueprint, @@ -369,34 +224,12 @@ * @param DrydockBlueprint The blueprint which should allocate a resource. * @param DrydockLease Requested lease. * @return DrydockResource Allocated resource. + * @task resource */ - abstract protected function executeAllocateResource( + abstract public function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease); - final public function allocateResource( - DrydockBlueprint $blueprint, - DrydockLease $lease) { - - $scope = $this->pushActiveScope(null, $lease); - - $this->log( - pht( - "Blueprint '%s': Allocating Resource for '%s'", - $this->getBlueprintClass(), - $lease->getLeaseName())); - - try { - $resource = $this->executeAllocateResource($blueprint, $lease); - $this->validateAllocatedResource($resource); - } catch (Exception $ex) { - $this->logException($ex); - throw $ex; - } - - return $resource; - } - /* -( Logging )------------------------------------------------------------ */ @@ -454,14 +287,15 @@ return idx(self::getAllBlueprintImplementations(), $class); } - protected function newResourceTemplate($name) { + protected function newResourceTemplate( + DrydockBlueprint $blueprint, + $name) { + $resource = id(new DrydockResource()) - ->setBlueprintPHID($this->getInstance()->getPHID()) - ->setBlueprintClass($this->getBlueprintClass()) + ->setBlueprintPHID($blueprint->getPHID()) ->setType($this->getType()) ->setStatus(DrydockResourceStatus::STATUS_PENDING) - ->setName($name) - ->save(); + ->setName($name); $this->activeResource = $resource; @@ -473,39 +307,6 @@ return $resource; } - /** - * Sanity checks that the blueprint is implemented properly. - */ - private function validateAllocatedResource($resource) { - $blueprint = $this->getBlueprintClass(); - - if (!($resource instanceof DrydockResource)) { - throw new Exception( - pht( - "Blueprint '%s' is not properly implemented: %s must return an ". - "object of type %s or throw, but returned something else.", - $blueprint, - 'executeAllocateResource()', - 'DrydockResource')); - } - - $current_status = $resource->getStatus(); - $req_status = DrydockResourceStatus::STATUS_OPEN; - if ($current_status != $req_status) { - $current_name = DrydockResourceStatus::getNameForStatus($current_status); - $req_name = DrydockResourceStatus::getNameForStatus($req_status); - throw new Exception( - pht( - "Blueprint '%s' is not properly implemented: %s must return a %s ". - "with status '%s', but returned one with status '%s'.", - $blueprint, - 'executeAllocateResource()', - 'DrydockResource', - $req_name, - $current_name)); - } - } - private function pushActiveScope( DrydockResource $resource = null, DrydockLease $lease = null) { diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -35,7 +35,7 @@ return true; } - public function canAllocateLeaseOnResource( + public function canAcquireLeaseOnResource( DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { @@ -47,15 +47,7 @@ return ($resource_repo && $lease_repo && ($resource_repo == $lease_repo)); } - protected function shouldAllocateLease( - DrydockResource $resource, - DrydockLease $lease, - array $other_leases) { - // TODO: These checks are out of date. - return !$other_leases; - } - - protected function executeAllocateResource( + public function allocateResource( DrydockBlueprint $blueprint, DrydockLease $lease) { @@ -105,6 +97,7 @@ $this->log(pht('Complete.')); $resource = $this->newResourceTemplate( + $blueprint, pht( 'Working Copy (%s)', $repository->getCallsign())); @@ -117,7 +110,8 @@ return $resource; } - protected function executeAcquireLease( + public function acquireLease( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { return; diff --git a/src/applications/drydock/constants/DrydockLeaseStatus.php b/src/applications/drydock/constants/DrydockLeaseStatus.php --- a/src/applications/drydock/constants/DrydockLeaseStatus.php +++ b/src/applications/drydock/constants/DrydockLeaseStatus.php @@ -3,7 +3,7 @@ final class DrydockLeaseStatus extends DrydockConstants { const STATUS_PENDING = 0; - const STATUS_ACQUIRING = 5; + const STATUS_ACQUIRED = 5; const STATUS_ACTIVE = 1; const STATUS_RELEASED = 2; const STATUS_BROKEN = 3; @@ -12,7 +12,7 @@ public static function getNameForStatus($status) { $map = array( self::STATUS_PENDING => pht('Pending'), - self::STATUS_ACQUIRING => pht('Acquiring'), + self::STATUS_ACQUIRED => pht('Acquired'), self::STATUS_ACTIVE => pht('Active'), self::STATUS_RELEASED => pht('Released'), self::STATUS_BROKEN => pht('Broken'), @@ -25,7 +25,7 @@ public static function getAllStatuses() { return array( self::STATUS_PENDING, - self::STATUS_ACQUIRING, + self::STATUS_ACQUIRED, self::STATUS_ACTIVE, self::STATUS_RELEASED, self::STATUS_BROKEN, diff --git a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php --- a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php +++ b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php @@ -40,4 +40,8 @@ return; } + public function getBlueprintFieldValue() { + return $this->getProxy()->getFieldValue(); + } + } diff --git a/src/applications/drydock/customfield/DrydockBlueprintCustomField.php b/src/applications/drydock/customfield/DrydockBlueprintCustomField.php --- a/src/applications/drydock/customfield/DrydockBlueprintCustomField.php +++ b/src/applications/drydock/customfield/DrydockBlueprintCustomField.php @@ -1,4 +1,8 @@ setName('create-resource') - ->setSynopsis(pht('Create a resource manually.')) - ->setArguments( - array( - array( - 'name' => 'name', - 'param' => 'resource_name', - 'help' => pht('Resource name.'), - ), - array( - 'name' => 'blueprint', - 'param' => 'blueprint_id', - 'help' => pht('Blueprint ID.'), - ), - array( - 'name' => 'attributes', - 'param' => 'name=value,...', - 'help' => pht('Resource attributes.'), - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $resource_name = $args->getArg('name'); - if (!$resource_name) { - throw new PhutilArgumentUsageException( - pht( - 'Specify a resource name with `%s`.', - '--name')); - } - - $blueprint_id = $args->getArg('blueprint'); - if (!$blueprint_id) { - throw new PhutilArgumentUsageException( - pht( - 'Specify a blueprint ID with `%s`.', - '--blueprint')); - } - - $attributes = $args->getArg('attributes'); - if ($attributes) { - $options = new PhutilSimpleOptions(); - $options->setCaseSensitive(true); - $attributes = $options->parse($attributes); - } - - $viewer = $this->getViewer(); - - $blueprint = id(new DrydockBlueprintQuery()) - ->setViewer($viewer) - ->withIDs(array($blueprint_id)) - ->executeOne(); - if (!$blueprint) { - throw new PhutilArgumentUsageException( - pht('Specified blueprint does not exist.')); - } - - $resource = id(new DrydockResource()) - ->setBlueprintPHID($blueprint->getPHID()) - ->setType($blueprint->getImplementation()->getType()) - ->setName($resource_name) - ->setStatus(DrydockResourceStatus::STATUS_OPEN); - if ($attributes) { - $resource->setAttributes($attributes); - } - $resource->save(); - - $console->writeOut("%s\n", pht('Created Resource %s', $resource->getID())); - return 0; - } - -} diff --git a/src/applications/drydock/query/DrydockLeaseSearchEngine.php b/src/applications/drydock/query/DrydockLeaseSearchEngine.php --- a/src/applications/drydock/query/DrydockLeaseSearchEngine.php +++ b/src/applications/drydock/query/DrydockLeaseSearchEngine.php @@ -74,7 +74,7 @@ 'statuses', array( DrydockLeaseStatus::STATUS_PENDING, - DrydockLeaseStatus::STATUS_ACQUIRING, + DrydockLeaseStatus::STATUS_ACQUIRED, DrydockLeaseStatus::STATUS_ACTIVE, )); case 'all': diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -1,5 +1,9 @@ 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; + } + + +/* -( 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); } - public function canAllocateLeaseOnResource( + + /** + * @task resource + */ + public function allocateResource(DrydockLease $lease) { + return $this->getImplementation()->allocateResource( + $this, + $lease); + } + + +/* -( Acquiring Leases )--------------------------------------------------- */ + + + /** + * @task lease + */ + public function canAcquireLeaseOnResource( DrydockResource $resource, DrydockLease $lease) { - return $this->getImplementation()->canAllocateLeaseOnResource( + return $this->getImplementation()->canAcquireLeaseOnResource( $this, $resource, $lease); } + + /** + * @task lease + */ + public function acquireLease( + DrydockResource $resource, + DrydockLease $lease) { + return $this->getImplementation()->acquireLease( + $this, + $resource, + $lease); + } + + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -13,6 +13,8 @@ private $resource = self::ATTACHABLE; private $releaseOnDestruction; + private $isAcquired = false; + private $activateWhenAcquired = false; /** * Flag this lease to be released when its destructor is called. This is @@ -133,8 +135,8 @@ public function isActive() { switch ($this->status) { + case DrydockLeaseStatus::STATUS_ACQUIRED: case DrydockLeaseStatus::STATUS_ACTIVE: - case DrydockLeaseStatus::STATUS_ACQUIRING: return true; } return false; @@ -171,7 +173,7 @@ case DrydockLeaseStatus::STATUS_BROKEN: throw new Exception(pht('Lease has been broken!')); case DrydockLeaseStatus::STATUS_PENDING: - case DrydockLeaseStatus::STATUS_ACQUIRING: + case DrydockLeaseStatus::STATUS_ACQUIRED: break; default: throw new Exception(pht('Unknown status??')); @@ -199,6 +201,53 @@ return $this; } + public function setActivateWhenAcquired($activate) { + $this->activateWhenAcquired = true; + 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_PENDING; + } + + 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 + ->setResourceID($resource->getID()) + ->setStatus($new_status) + ->save(); + + $this->isAcquired = true; + + return $this; + } + + public function isAcquiredLease() { + return $this->isAcquired; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -15,6 +15,8 @@ protected $ownerPHID; private $blueprint = self::ATTACHABLE; + private $isAllocated = false; + private $activateWhenAllocated = false; protected function getConfiguration() { return array( @@ -73,10 +75,47 @@ return $this; } - public function canAllocateLease(DrydockLease $lease) { - return $this->getBlueprint()->canAllocateLeaseOnResource( - $this, - $lease); + public function setActivateWhenAllocated($activate) { + $this->activateWhenAllocated = $activate; + return $this; + } + + public function allocateResource($status) { + if ($this->getID()) { + throw new Exception( + pht( + 'Trying to allocate a resource which has already been persisted. '. + 'Only new resources may be allocated.')); + } + + $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_OPEN; + } else { + $new_status = DrydockResourceStatus::STATUS_PENDING; + } + + $this + ->setStatus($new_status) + ->save(); + + $this->didAllocate = true; + + return $this; + } + + public function isAllocatedResource() { + return $this->isAllocated; } public function closeResource() { diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php --- a/src/applications/drydock/worker/DrydockAllocatorWorker.php +++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php @@ -1,5 +1,10 @@ loadLease(); - $this->allocateLease($lease); + $this->allocateAndAcquireLease($lease); } - private function allocateLease(DrydockLease $lease) { + +/* -( 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 allocateAndAcquireLease(DrydockLease $lease) { $blueprints = $this->loadBlueprintsForAllocatingLease($lease); // If we get nothing back, that means no blueprint is defined which can @@ -72,7 +89,8 @@ $exceptions = array(); foreach ($usable_blueprints as $blueprint) { try { - $resources[] = $blueprint->allocateResource($lease); + $resources[] = $this->allocateResource($blueprint, $lease); + // Bail after allocating one resource, we don't need any more than // this. break; @@ -106,7 +124,7 @@ $allocated = false; foreach ($resources as $resource) { try { - $blueprint->allocateLease($resource, $lease); + $this->acquireLease($resource, $lease); $allocated = true; break; } catch (Exception $ex) { @@ -129,6 +147,86 @@ /** + * 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(); + } + + // TODO: When blueprints can be disabled, this query should ignore disabled + // blueprints. + + $blueprints = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withBlueprintClasses(array_keys($impls)) + ->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. * @@ -137,6 +235,7 @@ * @param DrydockLease Requested lease. * @return list Resources which may be able to allocate * the lease. + * @task allocator */ private function loadResourcesForAllocatingLease( array $blueprints, @@ -157,7 +256,9 @@ $keep = array(); foreach ($resources as $key => $resource) { - if (!$resource->canAllocateLease($lease)) { + $blueprint = $resource->getBlueprint(); + + if (!$blueprint->canAcquireLeaseOnResource($resource, $lease)) { continue; } @@ -169,12 +270,40 @@ /** + * 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'); @@ -193,6 +322,7 @@ * @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'); @@ -205,107 +335,142 @@ } +/* -( Managing Resources )------------------------------------------------- */ + + /** - * Get all the concrete @{class:DrydockBlueprint}s which can possibly - * build a resource to satisfy a lease. + * Perform an actual resource allocation with a particular blueprint. * + * @param DrydockBlueprint The blueprint to allocate a resource from. * @param DrydockLease Requested lease. - * @return list List of qualifying blueprints. + * @return DrydockResource Allocated resource. + * @task resource */ - private function loadBlueprintsForAllocatingLease( + private function allocateResource( + DrydockBlueprint $blueprint, DrydockLease $lease) { - $viewer = $this->getViewer(); + $resource = $blueprint->allocateResource($lease); + $this->validateAllocatedResource($resource); + return $resource; + } - $impls = $this->loadBlueprintImplementationsForAllocatingLease($lease); - if (!$impls) { - return array(); + + /** + * 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 resource + */ + private function validateAllocatedResource( + DrydockBlueprint $blueprint, + $resource, + DrydockLease $lease) { + $blueprint = $this->getBlueprintClass(); + + 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')); } - // TODO: When blueprints can be disabled, this query should ignore disabled - // blueprints. + 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()')); + } - $blueprints = id(new DrydockBlueprintQuery()) - ->setViewer($viewer) - ->withBlueprintClasses(array_keys($impls)) - ->execute(); + $resource_type = $resource->getType(); + $lease_type = $lease->getResourceType(); - $keep = array(); - foreach ($blueprints as $key => $blueprint) { - if (!$blueprint->canEverAllocateResourceForLease($lease)) { - continue; - } + if ($resource_type !== $lease_type) { + // TODO: Destroy the resource here? - $keep[$key] = $blueprint; + 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)); } - - return $keep; } +/* -( Managing Leases )---------------------------------------------------- */ + + /** - * Get all the @{class:DrydockBlueprintImplementation}s which can possibly - * build a resource to satisfy a lease. + * Perform an actual lease acquisition on a particular resource. * - * 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. + * @param DrydockResource Resource to acquire a lease on. + * @param DrydockLease Lease to acquire. + * @return void + * @task lease */ - private function loadBlueprintImplementationsForAllocatingLease( + private function acquireLease( + DrydockResource $resource, DrydockLease $lease) { - $impls = DrydockBlueprintImplementation::getAllBlueprintImplementations(); + $blueprint = $resource->getBlueprint(); + $blueprint->acquireLease($resource, $lease); - $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; + $this->validateAcquiredLease($blueprint, $resource, $lease); } /** - * Remove blueprints which are too heavily allocated to build a resource for - * a lease from a list of blueprints. + * Make sure that a lease was really acquired properly. * - * @param list List of blueprints. - * @param list List with fully allocated blueprints - * removed. + * @param DrydockBlueprint Blueprint which created the resource. + * @param DrydockResource Resource which was acquired. + * @param DrydockLease The lease which was supposedly acquired. + * @return void + * @task lease */ - private function removeOverallocatedBlueprints( - array $blueprints, + private function validateAcquiredLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, DrydockLease $lease) { - assert_instances_of($blueprints, 'DrydockBlueprint'); - - $keep = array(); - foreach ($blueprints as $key => $blueprint) { - if (!$blueprint->canAllocateResourceForLease($lease)) { - continue; - } - $keep[$key] = $blueprint; + 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()')); } - return $keep; + $lease_id = $lease->getResourceID(); + $resource_id = $resource->getID(); + + if ($lease_id !== $resource_id) { + // TODO: Destroy the lease? + 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()')); + } } + } diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -31,7 +31,7 @@ throw new Exception( pht( 'Credential has noncreateable type "%s"!', - $credential->getCredentialType())); + $type_const)); } $credential = PassphraseCredential::initializeNewCredential($viewer)