Path: blob/master/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php
12256 views
<?php12/**3* @task lease Lease Acquisition4* @task resource Resource Allocation5* @task interface Resource Interfaces6* @task log Logging7*/8abstract class DrydockBlueprintImplementation extends Phobject {910abstract public function getType();1112abstract public function isEnabled();1314abstract public function getBlueprintName();15abstract public function getDescription();1617public function getBlueprintIcon() {18return 'fa-map-o';19}2021public function getFieldSpecifications() {22$fields = array();2324$fields += $this->getCustomFieldSpecifications();2526if ($this->shouldUseConcurrentResourceLimit()) {27$fields += array(28'allocator.limit' => array(29'name' => pht('Limit'),30'caption' => pht(31'Maximum number of resources this blueprint can have active '.32'concurrently.'),33'type' => 'int',34),35);36}3738return $fields;39}4041protected function getCustomFieldSpecifications() {42return array();43}4445public function getViewer() {46return PhabricatorUser::getOmnipotentUser();47}484950/* -( Lease Acquisition )-------------------------------------------------- */515253/**54* Enforce basic checks on lease/resource compatibility. Allows resources to55* reject leases if they are incompatible, even if the resource types match.56*57* For example, if a resource represents a 32-bit host, this method might58* reject leases that need a 64-bit host. The blueprint might also reject59* a resource if the lease needs 8GB of RAM and the resource only has 6GB60* free.61*62* This method should not acquire locks or expect anything to be locked. This63* is a coarse compatibility check between a lease and a resource.64*65* @param DrydockBlueprint Concrete blueprint to allocate for.66* @param DrydockResource Candidate resource to allocate the lease on.67* @param DrydockLease Pending lease that wants to allocate here.68* @return bool True if the resource and lease are compatible.69* @task lease70*/71abstract public function canAcquireLeaseOnResource(72DrydockBlueprint $blueprint,73DrydockResource $resource,74DrydockLease $lease);757677/**78* Acquire a lease. Allows resources to perform setup as leases are brought79* online.80*81* If acquisition fails, throw an exception.82*83* @param DrydockBlueprint Blueprint which built the resource.84* @param DrydockResource Resource to acquire a lease on.85* @param DrydockLease Requested lease.86* @return void87* @task lease88*/89abstract public function acquireLease(90DrydockBlueprint $blueprint,91DrydockResource $resource,92DrydockLease $lease);939495/**96* @return void97* @task lease98*/99public function activateLease(100DrydockBlueprint $blueprint,101DrydockResource $resource,102DrydockLease $lease) {103throw new PhutilMethodNotImplementedException();104}105106107/**108* React to a lease being released.109*110* This callback is primarily useful for automatically releasing resources111* once all leases are released.112*113* @param DrydockBlueprint Blueprint which built the resource.114* @param DrydockResource Resource a lease was released on.115* @param DrydockLease Recently released lease.116* @return void117* @task lease118*/119abstract public function didReleaseLease(120DrydockBlueprint $blueprint,121DrydockResource $resource,122DrydockLease $lease);123124125/**126* Destroy any temporary data associated with a lease.127*128* If a lease creates temporary state while held, destroy it here.129*130* @param DrydockBlueprint Blueprint which built the resource.131* @param DrydockResource Resource the lease is acquired on.132* @param DrydockLease The lease being destroyed.133* @return void134* @task lease135*/136abstract public function destroyLease(137DrydockBlueprint $blueprint,138DrydockResource $resource,139DrydockLease $lease);140141/**142* Return true to try to allocate a new resource and expand the resource143* pool instead of permitting an otherwise valid acquisition on an existing144* resource.145*146* This allows the blueprint to provide a soft hint about when the resource147* pool should grow.148*149* Returning "true" in all cases generally makes sense when a blueprint150* controls a fixed pool of resources, like a particular number of physical151* hosts: you want to put all the hosts in service, so whenever it is152* possible to allocate a new host you want to do this.153*154* Returning "false" in all cases generally make sense when a blueprint155* has a flexible pool of expensive resources and you want to pack leases156* onto them as tightly as possible.157*158* @param DrydockBlueprint The blueprint for an existing resource being159* acquired.160* @param DrydockResource The resource being acquired, which we may want to161* build a supplemental resource for.162* @param DrydockLease The current lease performing acquisition.163* @return bool True to prefer allocating a supplemental resource.164*165* @task lease166*/167public function shouldAllocateSupplementalResource(168DrydockBlueprint $blueprint,169DrydockResource $resource,170DrydockLease $lease) {171return false;172}173174/* -( Resource Allocation )------------------------------------------------ */175176177/**178* Enforce fundamental implementation/lease checks. Allows implementations to179* reject a lease which no concrete blueprint can ever satisfy.180*181* For example, if a lease only builds ARM hosts and the lease needs a182* PowerPC host, it may be rejected here.183*184* This is the earliest rejection phase, and followed by185* @{method:canEverAllocateResourceForLease}.186*187* This method should not actually check if a resource can be allocated188* right now, or even if a blueprint which can allocate a suitable resource189* really exists, only if some blueprint may conceivably exist which could190* plausibly be able to build a suitable resource.191*192* @param DrydockLease Requested lease.193* @return bool True if some concrete blueprint of this implementation's194* type might ever be able to build a resource for the lease.195* @task resource196*/197abstract public function canAnyBlueprintEverAllocateResourceForLease(198DrydockLease $lease);199200201/**202* Enforce basic blueprint/lease checks. Allows blueprints to reject a lease203* which they can not build a resource for.204*205* This is the second rejection phase. It follows206* @{method:canAnyBlueprintEverAllocateResourceForLease} and is followed by207* @{method:canAllocateResourceForLease}.208*209* This method should not check if a resource can be built right now, only210* if the blueprint as configured may, at some time, be able to build a211* suitable resource.212*213* @param DrydockBlueprint Blueprint which may be asked to allocate a214* resource.215* @param DrydockLease Requested lease.216* @return bool True if this blueprint can eventually build a suitable217* resource for the lease, as currently configured.218* @task resource219*/220abstract public function canEverAllocateResourceForLease(221DrydockBlueprint $blueprint,222DrydockLease $lease);223224225/**226* Enforce basic availability limits. Allows blueprints to reject resource227* allocation if they are currently overallocated.228*229* This method should perform basic capacity/limit checks. For example, if230* it has a limit of 6 resources and currently has 6 resources allocated,231* it might reject new leases.232*233* This method should not acquire locks or expect locks to be acquired. This234* is a coarse check to determine if the operation is likely to succeed235* right now without needing to acquire locks.236*237* It is expected that this method will sometimes return `true` (indicating238* that a resource can be allocated) but find that another allocator has239* eaten up free capacity by the time it actually tries to build a resource.240* This is normal and the allocator will recover from it.241*242* @param DrydockBlueprint The blueprint which may be asked to allocate a243* resource.244* @param DrydockLease Requested lease.245* @return bool True if this blueprint appears likely to be able to allocate246* a suitable resource.247* @task resource248*/249abstract public function canAllocateResourceForLease(250DrydockBlueprint $blueprint,251DrydockLease $lease);252253254/**255* Allocate a suitable resource for a lease.256*257* This method MUST acquire, hold, and manage locks to prevent multiple258* allocations from racing. World state is not locked before this method is259* called. Blueprints are entirely responsible for any lock handling they260* need to perform.261*262* @param DrydockBlueprint The blueprint which should allocate a resource.263* @param DrydockLease Requested lease.264* @return DrydockResource Allocated resource.265* @task resource266*/267abstract public function allocateResource(268DrydockBlueprint $blueprint,269DrydockLease $lease);270271272/**273* @task resource274*/275public function activateResource(276DrydockBlueprint $blueprint,277DrydockResource $resource) {278throw new PhutilMethodNotImplementedException();279}280281282/**283* Destroy any temporary data associated with a resource.284*285* If a resource creates temporary state when allocated, destroy that state286* here. For example, you might shut down a virtual host or destroy a working287* copy on disk.288*289* @param DrydockBlueprint Blueprint which built the resource.290* @param DrydockResource Resource being destroyed.291* @return void292* @task resource293*/294abstract public function destroyResource(295DrydockBlueprint $blueprint,296DrydockResource $resource);297298299/**300* Get a human readable name for a resource.301*302* @param DrydockBlueprint Blueprint which built the resource.303* @param DrydockResource Resource to get the name of.304* @return string Human-readable resource name.305* @task resource306*/307abstract public function getResourceName(308DrydockBlueprint $blueprint,309DrydockResource $resource);310311312/* -( Resource Interfaces )------------------------------------------------ */313314315abstract public function getInterface(316DrydockBlueprint $blueprint,317DrydockResource $resource,318DrydockLease $lease,319$type);320321322/* -( Logging )------------------------------------------------------------ */323324325public static function getAllBlueprintImplementations() {326return id(new PhutilClassMapQuery())327->setAncestorClass(__CLASS__)328->execute();329}330331332/**333* Get all the @{class:DrydockBlueprintImplementation}s which can possibly334* build a resource to satisfy a lease.335*336* This method returns blueprints which might, at some time, be able to337* build a resource which can satisfy the lease. They may not be able to338* build that resource right now.339*340* @param DrydockLease Requested lease.341* @return list<DrydockBlueprintImplementation> List of qualifying blueprint342* implementations.343*/344public static function getAllForAllocatingLease(345DrydockLease $lease) {346347$impls = self::getAllBlueprintImplementations();348349$keep = array();350foreach ($impls as $key => $impl) {351// Don't use disabled blueprint types.352if (!$impl->isEnabled()) {353continue;354}355356// Don't use blueprint types which can't allocate the correct kind of357// resource.358if ($impl->getType() != $lease->getResourceType()) {359continue;360}361362if (!$impl->canAnyBlueprintEverAllocateResourceForLease($lease)) {363continue;364}365366$keep[$key] = $impl;367}368369return $keep;370}371372public static function getNamedImplementation($class) {373return idx(self::getAllBlueprintImplementations(), $class);374}375376protected function newResourceTemplate(DrydockBlueprint $blueprint) {377378$resource = id(new DrydockResource())379->setBlueprintPHID($blueprint->getPHID())380->attachBlueprint($blueprint)381->setType($this->getType())382->setStatus(DrydockResourceStatus::STATUS_PENDING);383384// Pre-allocate the resource PHID.385$resource->setPHID($resource->generatePHID());386387return $resource;388}389390protected function newLease(DrydockBlueprint $blueprint) {391return DrydockLease::initializeNewLease()392->setAuthorizingPHID($blueprint->getPHID());393}394395protected function requireActiveLease(DrydockLease $lease) {396$lease_status = $lease->getStatus();397398switch ($lease_status) {399case DrydockLeaseStatus::STATUS_PENDING:400case DrydockLeaseStatus::STATUS_ACQUIRED:401throw new PhabricatorWorkerYieldException(15);402case DrydockLeaseStatus::STATUS_ACTIVE:403return;404default:405throw new Exception(406pht(407'Lease ("%s") is in bad state ("%s"), expected "%s".',408$lease->getPHID(),409$lease_status,410DrydockLeaseStatus::STATUS_ACTIVE));411}412}413414415/**416* Does this implementation use concurrent resource limits?417*418* Implementations can override this method to opt into standard limit419* behavior, which provides a simple concurrent resource limit.420*421* @return bool True to use limits.422*/423protected function shouldUseConcurrentResourceLimit() {424return false;425}426427428/**429* Get the effective concurrent resource limit for this blueprint.430*431* @param DrydockBlueprint Blueprint to get the limit for.432* @return int|null Limit, or `null` for no limit.433*/434protected function getConcurrentResourceLimit(DrydockBlueprint $blueprint) {435if ($this->shouldUseConcurrentResourceLimit()) {436$limit = $blueprint->getFieldValue('allocator.limit');437$limit = (int)$limit;438if ($limit > 0) {439return $limit;440} else {441return null;442}443}444445return null;446}447448449protected function getConcurrentResourceLimitSlotLock(450DrydockBlueprint $blueprint) {451452$limit = $this->getConcurrentResourceLimit($blueprint);453if ($limit === null) {454return;455}456457$blueprint_phid = $blueprint->getPHID();458459// TODO: This logic shouldn't do anything awful, but is a little silly. It460// would be nice to unify the "huge limit" and "small limit" cases461// eventually but it's a little tricky.462463// If the limit is huge, just pick a random slot. This is just stopping464// us from exploding if someone types a billion zillion into the box.465if ($limit > 1024) {466$slot = mt_rand(0, $limit - 1);467return "allocator({$blueprint_phid}).limit({$slot})";468}469470// For reasonable limits, actually check for an available slot.471$slots = range(0, $limit - 1);472shuffle($slots);473474$lock_names = array();475foreach ($slots as $slot) {476$lock_names[] = "allocator({$blueprint_phid}).limit({$slot})";477}478479$locks = DrydockSlotLock::loadHeldLocks($lock_names);480$locks = mpull($locks, null, 'getLockKey');481482foreach ($lock_names as $lock_name) {483if (empty($locks[$lock_name])) {484return $lock_name;485}486}487488// If we found no free slot, just return whatever we checked last (which489// is just a random slot). There's a small chance we'll get lucky and the490// lock will be free by the time we try to take it, but usually we'll just491// fail to grab the lock, throw an appropriate lock exception, and get back492// on the right path to retry later.493494return $lock_name;495}496497498499/**500* Apply standard limits on resource allocation rate.501*502* @param DrydockBlueprint The blueprint requesting an allocation.503* @return bool True if further allocations should be limited.504*/505protected function shouldLimitAllocatingPoolSize(506DrydockBlueprint $blueprint) {507508// Limit on total number of active resources.509$total_limit = $this->getConcurrentResourceLimit($blueprint);510if ($total_limit === null) {511return false;512}513514$resource = new DrydockResource();515$conn = $resource->establishConnection('r');516517$counts = queryfx_all(518$conn,519'SELECT status, COUNT(*) N FROM %R520WHERE blueprintPHID = %s AND status != %s521GROUP BY status',522$resource,523$blueprint->getPHID(),524DrydockResourceStatus::STATUS_DESTROYED);525$counts = ipull($counts, 'N', 'status');526527$n_alloc = idx($counts, DrydockResourceStatus::STATUS_PENDING, 0);528$n_active = idx($counts, DrydockResourceStatus::STATUS_ACTIVE, 0);529$n_broken = idx($counts, DrydockResourceStatus::STATUS_BROKEN, 0);530$n_released = idx($counts, DrydockResourceStatus::STATUS_RELEASED, 0);531532// If we're at the limit on total active resources, limit additional533// allocations.534$n_total = ($n_alloc + $n_active + $n_broken + $n_released);535if ($n_total >= $total_limit) {536return true;537}538539return false;540}541542}543544545