setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($lease_id)) ->execute(); $lease = idx($query, $lease_id); if (!$lease) { throw new Exception("No such lease '{$lease_id}'!"); } return $lease; } protected function getInstance() { if (!$this->instance) { throw new Exception( 'Attach the blueprint instance to the implementation.'); } return $this->instance; } public function attachInstance(DrydockBlueprint $instance) { $this->instance = $instance; return $this; } public function getFieldSpecifications() { return array(); } public function getDetail($key, $default = null) { return $this->getInstance()->getDetail($key, $default); } /* -( Lease Acquisition )-------------------------------------------------- */ /** * @task lease */ final public function filterResource( DrydockResource $resource, DrydockLease $lease) { $scope = $this->pushActiveScope($resource, $lease); return $this->canAllocateLease($resource, $lease); } /** * Enforce basic checks on lease/resource compatibility. Allows resources to * reject leases if they are incompatible, even if the resource types match. * * For example, if a resource represents a 32-bit host, this method might * reject leases that need a 64-bit host. If a resource represents a working * copy of repository "X", this method might reject leases which need a * working copy of repository "Y". Generally, although the main types of * a lease and resource may match (e.g., both "host"), it may not actually be * possible to satisfy the lease with a specific resource. * * This method generally should not enforce limits or perform capacity * checks. Perform those in @{method:shouldAllocateLease} instead. It also * should not perform actual acquisition of the lease; perform that in * @{method:executeAcquireLease} instead. * * @param DrydockResource Candidiate resource to allocate the lease on. * @param DrydockLease Pending lease that wants to allocate here. * @return bool True if the resource and lease are compatible. * @task lease */ abstract protected function canAllocateLease( DrydockResource $resource, DrydockLease $lease); /** * @task lease */ final public function allocateLease( DrydockResource $resource, DrydockLease $lease) { $scope = $this->pushActiveScope($resource, $lease); $this->log('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('Allocated Lease'); } else { $resource->killTransaction(); $this->log('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('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('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. * * If acquisition fails, throw an exception. * * @param DrydockResource Resource to acquire a lease on. * @param DrydockLease Lease to acquire. * @return void */ abstract protected function executeAcquireLease( DrydockResource $resource, DrydockLease $lease); final public function releaseLease( DrydockResource $resource, DrydockLease $lease) { $scope = $this->pushActiveScope(null, $lease); $released = false; $lease->openTransaction(); $lease->beginReadLocking(); $lease->reload(); if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) { $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED); $lease->save(); $released = true; } $lease->endReadLocking(); $lease->saveTransaction(); if (!$released) { throw new Exception('Unable to release lease: lease not active!'); } } /* -( Resource Allocation )------------------------------------------------ */ public function canAllocateMoreResources(array $pool) { return true; } abstract protected function executeAllocateResource(DrydockLease $lease); final public function allocateResource(DrydockLease $lease) { $scope = $this->pushActiveScope(null, $lease); $this->log( pht( "Blueprint '%s': Allocating Resource for '%s'", $this->getBlueprintClass(), $lease->getLeaseName())); try { $resource = $this->executeAllocateResource($lease); $this->validateAllocatedResource($resource); } catch (Exception $ex) { $this->logException($ex); throw $ex; } return $resource; } /* -( Logging )------------------------------------------------------------ */ /** * @task log */ protected function logException(Exception $ex) { $this->log($ex->getMessage()); } /** * @task log */ protected function log($message) { self::writeLog( $this->activeResource, $this->activeLease, $message); } /** * @task log */ public static function writeLog( DrydockResource $resource = null, DrydockLease $lease = null, $message) { $log = id(new DrydockLog()) ->setEpoch(time()) ->setMessage($message); if ($resource) { $log->setResourceID($resource->getID()); } if ($lease) { $log->setLeaseID($lease->getID()); } $log->save(); } public static function getAllBlueprintImplementations() { static $list = null; if ($list === null) { $blueprints = id(new PhutilSymbolLoader()) ->setType('class') ->setAncestorClass('DrydockBlueprintImplementation') ->setConcreteOnly(true) ->selectAndLoadSymbols(); $list = ipull($blueprints, 'name', 'name'); foreach ($list as $class_name => $ignored) { $list[$class_name] = newv($class_name, array()); } } return $list; } public static function getAllBlueprintImplementationsForResource($type) { static $groups = null; if ($groups === null) { $groups = mgroup(self::getAllBlueprintImplementations(), 'getType'); } return idx($groups, $type, array()); } public static function getNamedImplementation($class) { return idx(self::getAllBlueprintImplementations(), $class); } protected function newResourceTemplate($name) { $resource = id(new DrydockResource()) ->setBlueprintPHID($this->getInstance()->getPHID()) ->setBlueprintClass($this->getBlueprintClass()) ->setType($this->getType()) ->setStatus(DrydockResourceStatus::STATUS_PENDING) ->setName($name) ->save(); $this->activeResource = $resource; $this->log( pht( "Blueprint '%s': Created New Template", $this->getBlueprintClass())); return $resource; } /** * Sanity checks that the blueprint is implemented properly. */ private function validateAllocatedResource($resource) { $blueprint = $this->getBlueprintClass(); if (!($resource instanceof DrydockResource)) { throw new Exception( "Blueprint '{$blueprint}' is not properly implemented: ". "executeAllocateResource() must return an object of type ". "DrydockResource or throw, but returned something else."); } $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( "Blueprint '{$blueprint}' is not properly implemented: ". "executeAllocateResource() must return a DrydockResource with ". "status '{$req_name}', but returned one with status ". "'{$current_name}'."); } } private function pushActiveScope( DrydockResource $resource = null, DrydockLease $lease = null) { if (($this->activeResource !== null) || ($this->activeLease !== null)) { throw new Exception('There is already an active resource or lease!'); } $this->activeResource = $resource; $this->activeLease = $lease; return new DrydockBlueprintScopeGuard($this); } public function popActiveScope() { $this->activeResource = null; $this->activeLease = null; } }