4 * @task update Updating Leases
5 * @task command Processing Commands
6 * @task allocator Drydock Allocator
7 * @task acquire Acquiring Leases
8 * @task activate Activating Leases
9 * @task release Releasing Leases
10 * @task break Breaking Leases
11 * @task destroy Destroying Leases
13 final class DrydockLeaseUpdateWorker
extends DrydockWorker
{
15 protected function doWork() {
16 $lease_phid = $this->getTaskDataValue('leasePHID');
18 $hash = PhabricatorHash
::digestForIndex($lease_phid);
19 $lock_key = 'drydock.lease:'.$hash;
21 $lock = PhabricatorGlobalLock
::newLock($lock_key)
25 $lease = $this->loadLease($lease_phid);
26 $this->handleUpdate($lease);
27 } catch (Exception
$ex) {
29 $this->flushDrydockTaskQueue();
37 /* -( Updating Leases )---------------------------------------------------- */
43 private function handleUpdate(DrydockLease
$lease) {
45 $this->updateLease($lease);
46 } catch (DrydockAcquiredBrokenResourceException
$ex) {
47 // If this lease acquired a resource but failed to activate, we don't
48 // need to break the lease. We can throw it back in the pool and let
49 // it take another shot at acquiring a new resource.
51 // Before we throw it back, release any locks the lease is holding.
52 DrydockSlotLock
::releaseLocks($lease->getPHID());
55 ->setStatus(DrydockLeaseStatus
::STATUS_PENDING
)
56 ->setResourcePHID(null)
60 DrydockLeaseReacquireLogType
::LOGCONST
,
62 'class' => get_class($ex),
63 'message' => $ex->getMessage(),
66 $this->yieldLease($lease, $ex);
67 } catch (Exception
$ex) {
68 if ($this->isTemporaryException($ex)) {
69 $this->yieldLease($lease, $ex);
71 $this->breakLease($lease, $ex);
80 private function updateLease(DrydockLease
$lease) {
81 $this->processLeaseCommands($lease);
83 $lease_status = $lease->getStatus();
84 switch ($lease_status) {
85 case DrydockLeaseStatus
::STATUS_PENDING
:
86 $this->executeAllocator($lease);
88 case DrydockLeaseStatus
::STATUS_ACQUIRED
:
89 $this->activateLease($lease);
91 case DrydockLeaseStatus
::STATUS_ACTIVE
:
94 case DrydockLeaseStatus
::STATUS_RELEASED
:
95 case DrydockLeaseStatus
::STATUS_BROKEN
:
96 $this->destroyLease($lease);
98 case DrydockLeaseStatus
::STATUS_DESTROYED
:
102 $this->yieldIfExpiringLease($lease);
109 private function yieldLease(DrydockLease
$lease, Exception
$ex) {
110 $duration = $this->getYieldDurationFromException($ex);
113 DrydockLeaseActivationYieldLogType
::LOGCONST
,
115 'duration' => $duration,
118 throw new PhabricatorWorkerYieldException($duration);
122 /* -( Processing Commands )------------------------------------------------ */
128 private function processLeaseCommands(DrydockLease
$lease) {
129 if (!$lease->canReceiveCommands()) {
133 $this->checkLeaseExpiration($lease);
135 $commands = $this->loadCommands($lease->getPHID());
136 foreach ($commands as $command) {
137 if (!$lease->canReceiveCommands()) {
141 $this->processLeaseCommand($lease, $command);
144 ->setIsConsumed(true)
153 private function processLeaseCommand(
155 DrydockCommand
$command) {
156 switch ($command->getCommand()) {
157 case DrydockCommand
::COMMAND_RELEASE
:
158 $this->releaseLease($lease);
164 /* -( Drydock Allocator )-------------------------------------------------- */
168 * Find or build a resource which can satisfy a given lease request, then
171 * @param DrydockLease Requested lease.
175 private function executeAllocator(DrydockLease
$lease) {
176 $blueprints = $this->loadBlueprintsForAllocatingLease($lease);
178 // If we get nothing back, that means no blueprint is defined which can
179 // ever build the requested resource. This is a permanent failure, since
180 // we don't expect to succeed no matter how many times we try.
182 throw new PhabricatorWorkerPermanentFailureException(
184 'No active Drydock blueprint exists which can ever allocate a '.
185 'resource for lease "%s".',
189 // First, try to find a suitable open resource which we can acquire a new
191 $resources = $this->loadResourcesForAllocatingLease($blueprints, $lease);
193 // If no resources exist yet, see if we can build one.
195 $usable_blueprints = $this->removeOverallocatedBlueprints(
199 // If we get nothing back here, some blueprint claims it can eventually
200 // satisfy the lease, just not right now. This is a temporary failure,
201 // and we expect allocation to succeed eventually.
202 if (!$usable_blueprints) {
203 $blueprints = $this->rankBlueprints($blueprints, $lease);
205 // Try to actively reclaim unused resources. If we succeed, jump back
206 // into the queue in an effort to claim it.
207 foreach ($blueprints as $blueprint) {
208 $reclaimed = $this->reclaimResources($blueprint, $lease);
211 DrydockLeaseReclaimLogType
::LOGCONST
,
213 'resourcePHIDs' => array($reclaimed->getPHID()),
216 throw new PhabricatorWorkerYieldException(15);
221 DrydockLeaseWaitingForResourcesLogType
::LOGCONST
,
223 'blueprintPHIDs' => mpull($blueprints, 'getPHID'),
226 throw new PhabricatorWorkerYieldException(15);
229 $usable_blueprints = $this->rankBlueprints($usable_blueprints, $lease);
231 $exceptions = array();
232 foreach ($usable_blueprints as $blueprint) {
234 $resources[] = $this->allocateResource($blueprint, $lease);
236 // Bail after allocating one resource, we don't need any more than
239 } catch (Exception
$ex) {
240 // This failure is not normally expected, so log it. It can be
241 // caused by something mundane and recoverable, however (see below
244 // We log to the blueprint separately from the log to the lease:
245 // the lease is not attached to a blueprint yet so the lease log
246 // will not show up on the blueprint; more than one blueprint may
247 // fail; and the lease is not really impacted (and won't log) if at
248 // least one blueprint actually works.
250 $blueprint->logEvent(
251 DrydockResourceAllocationFailureLogType
::LOGCONST
,
253 'class' => get_class($ex),
254 'message' => $ex->getMessage(),
262 // If one or more blueprints claimed that they would be able to
263 // allocate resources but none are actually able to allocate resources,
264 // log the failure and yield so we try again soon.
266 // This can happen if some unexpected issue occurs during allocation
267 // (for example, a call to build a VM fails for some reason) or if we
268 // raced another allocator and the blueprint is now full.
270 $ex = new PhutilAggregateException(
272 'All blueprints failed to allocate a suitable new resource when '.
273 'trying to allocate lease ("%s").',
278 DrydockLeaseAllocationFailureLogType
::LOGCONST
,
280 'class' => get_class($ex),
281 'message' => $ex->getMessage(),
284 throw new PhabricatorWorkerYieldException(15);
287 $resources = $this->removeUnacquirableResources($resources, $lease);
289 // If we make it here, we just built a resource but aren't allowed
290 // to acquire it. We expect this during routine operation if the
291 // resource prevents acquisition until it activates. Yield and wait
293 throw new PhabricatorWorkerYieldException(15);
296 // NOTE: We have not acquired the lease yet, so it is possible that the
297 // resource we just built will be snatched up by some other lease before
298 // we can acquire it. This is not problematic: we'll retry a little later
299 // and should succeed eventually.
302 $resources = $this->rankResources($resources, $lease);
304 $exceptions = array();
307 foreach ($resources as $resource) {
309 $resource = $this->newResourceForAcquisition($resource, $lease);
310 $this->acquireLease($resource, $lease);
313 } catch (DrydockResourceLockException
$ex) {
314 // We need to lock the resource to actually acquire it. If we aren't
315 // able to acquire the lock quickly enough, we can yield and try again
318 } catch (DrydockAcquiredBrokenResourceException
$ex) {
319 // If a resource was reclaimed or destroyed by the time we actually
320 // got around to acquiring it, we just got unlucky. We can yield and
323 } catch (PhabricatorWorkerYieldException
$ex) {
324 // We can be told to yield, particularly by the supplemental allocator
325 // trying to give us a supplemental resource.
327 } catch (Exception
$ex) {
334 throw new PhabricatorWorkerYieldException(15);
336 throw new PhutilAggregateException(
338 'Unable to acquire lease "%s" on any resource.',
347 * Get all the @{class:DrydockBlueprintImplementation}s which can possibly
348 * build a resource to satisfy a lease.
350 * This method returns blueprints which might, at some time, be able to
351 * build a resource which can satisfy the lease. They may not be able to
352 * build that resource right now.
354 * @param DrydockLease Requested lease.
355 * @return list<DrydockBlueprintImplementation> List of qualifying blueprint
359 private function loadBlueprintImplementationsForAllocatingLease(
360 DrydockLease
$lease) {
362 $impls = DrydockBlueprintImplementation
::getAllBlueprintImplementations();
365 foreach ($impls as $key => $impl) {
366 // Don't use disabled blueprint types.
367 if (!$impl->isEnabled()) {
371 // Don't use blueprint types which can't allocate the correct kind of
373 if ($impl->getType() != $lease->getResourceType()) {
377 if (!$impl->canAnyBlueprintEverAllocateResourceForLease($lease)) {
389 * Get all the concrete @{class:DrydockBlueprint}s which can possibly
390 * build a resource to satisfy a lease.
392 * @param DrydockLease Requested lease.
393 * @return list<DrydockBlueprint> List of qualifying blueprints.
396 private function loadBlueprintsForAllocatingLease(
397 DrydockLease
$lease) {
398 $viewer = $this->getViewer();
400 $impls = $this->loadBlueprintImplementationsForAllocatingLease($lease);
405 $blueprint_phids = $lease->getAllowedBlueprintPHIDs();
406 if (!$blueprint_phids) {
407 $lease->logEvent(DrydockLeaseNoBlueprintsLogType
::LOGCONST
);
411 $query = id(new DrydockBlueprintQuery())
413 ->withPHIDs($blueprint_phids)
414 ->withBlueprintClasses(array_keys($impls))
415 ->withDisabled(false);
417 // The Drydock application itself is allowed to authorize anything. This
418 // is primarily used for leases generated by CLI administrative tools.
419 $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
421 $authorizing_phid = $lease->getAuthorizingPHID();
422 if ($authorizing_phid != $drydock_phid) {
423 $blueprints = id(clone $query)
424 ->withAuthorizedPHIDs(array($authorizing_phid))
427 // If we didn't hit any blueprints, check if this is an authorization
428 // problem: re-execute the query without the authorization constraint.
429 // If the second query hits blueprints, the overall configuration is
430 // fine but this is an authorization problem. If the second query also
431 // comes up blank, this is some other kind of configuration issue so
432 // we fall through to the default pathway.
433 $all_blueprints = $query->execute();
434 if ($all_blueprints) {
436 DrydockLeaseNoAuthorizationsLogType
::LOGCONST
,
438 'authorizingPHID' => $authorizing_phid,
444 $blueprints = $query->execute();
448 foreach ($blueprints as $key => $blueprint) {
449 if (!$blueprint->canEverAllocateResourceForLease($lease)) {
453 $keep[$key] = $blueprint;
461 * Load a list of all resources which a given lease can possibly be
464 * @param list<DrydockBlueprint> Blueprints which may produce suitable
466 * @param DrydockLease Requested lease.
467 * @return list<DrydockResource> Resources which may be able to allocate
471 private function loadResourcesForAllocatingLease(
473 DrydockLease
$lease) {
474 assert_instances_of($blueprints, 'DrydockBlueprint');
475 $viewer = $this->getViewer();
477 $resources = id(new DrydockResourceQuery())
479 ->withBlueprintPHIDs(mpull($blueprints, 'getPHID'))
480 ->withTypes(array($lease->getResourceType()))
483 DrydockResourceStatus
::STATUS_PENDING
,
484 DrydockResourceStatus
::STATUS_ACTIVE
,
488 return $this->removeUnacquirableResources($resources, $lease);
493 * Remove resources which can not be acquired by a given lease from a list.
495 * @param list<DrydockResource> Candidate resources.
496 * @param DrydockLease Acquiring lease.
497 * @return list<DrydockResource> Resources which the lease may be able to
501 private function removeUnacquirableResources(
503 DrydockLease
$lease) {
505 foreach ($resources as $key => $resource) {
506 $blueprint = $resource->getBlueprint();
508 if (!$blueprint->canAcquireLeaseOnResource($resource, $lease)) {
512 $keep[$key] = $resource;
520 * Remove blueprints which are too heavily allocated to build a resource for
521 * a lease from a list of blueprints.
523 * @param list<DrydockBlueprint> List of blueprints.
524 * @return list<DrydockBlueprint> List with blueprints that can not allocate
525 * a resource for the lease right now removed.
528 private function removeOverallocatedBlueprints(
530 DrydockLease
$lease) {
531 assert_instances_of($blueprints, 'DrydockBlueprint');
535 foreach ($blueprints as $key => $blueprint) {
536 if (!$blueprint->canAllocateResourceForLease($lease)) {
540 $keep[$key] = $blueprint;
548 * Rank blueprints by suitability for building a new resource for a
551 * @param list<DrydockBlueprint> List of blueprints.
552 * @param DrydockLease Requested lease.
553 * @return list<DrydockBlueprint> Ranked list of blueprints.
556 private function rankBlueprints(array $blueprints, DrydockLease
$lease) {
557 assert_instances_of($blueprints, 'DrydockBlueprint');
559 // TODO: Implement improvements to this ranking algorithm if they become
561 shuffle($blueprints);
568 * Rank resources by suitability for allocating a particular lease.
570 * @param list<DrydockResource> List of resources.
571 * @param DrydockLease Requested lease.
572 * @return list<DrydockResource> Ranked list of resources.
575 private function rankResources(array $resources, DrydockLease
$lease) {
576 assert_instances_of($resources, 'DrydockResource');
578 // TODO: Implement improvements to this ranking algorithm if they become
587 * Perform an actual resource allocation with a particular blueprint.
589 * @param DrydockBlueprint The blueprint to allocate a resource from.
590 * @param DrydockLease Requested lease.
591 * @return DrydockResource Allocated resource.
594 private function allocateResource(
595 DrydockBlueprint
$blueprint,
596 DrydockLease
$lease) {
597 $resource = $blueprint->allocateResource($lease);
598 $this->validateAllocatedResource($blueprint, $resource, $lease);
600 // If this resource was allocated as a pending resource, queue a task to
602 if ($resource->getStatus() == DrydockResourceStatus
::STATUS_PENDING
) {
603 PhabricatorWorker
::scheduleTask(
604 'DrydockResourceUpdateWorker',
606 'resourcePHID' => $resource->getPHID(),
608 // This task will generally yield while the resource activates, so
609 // wake it back up once the resource comes online. Most of the time,
610 // we'll be able to lease the newly activated resource.
611 'awakenOnActivation' => array(
612 $this->getCurrentWorkerTaskID(),
616 'objectPHID' => $resource->getPHID(),
625 * Check that the resource a blueprint allocated is roughly the sort of
628 * @param DrydockBlueprint Blueprint which built the resource.
629 * @param wild Thing which the blueprint claims is a valid resource.
630 * @param DrydockLease Lease the resource was allocated for.
634 private function validateAllocatedResource(
635 DrydockBlueprint
$blueprint,
637 DrydockLease
$lease) {
639 if (!($resource instanceof DrydockResource
)) {
642 'Blueprint "%s" (of type "%s") is not properly implemented: %s must '.
643 'return an object of type %s or throw, but returned something else.',
644 $blueprint->getBlueprintName(),
645 $blueprint->getClassName(),
646 'allocateResource()',
650 if (!$resource->isAllocatedResource()) {
653 'Blueprint "%s" (of type "%s") is not properly implemented: %s '.
654 'must actually allocate the resource it returns.',
655 $blueprint->getBlueprintName(),
656 $blueprint->getClassName(),
657 'allocateResource()'));
660 $resource_type = $resource->getType();
661 $lease_type = $lease->getResourceType();
663 if ($resource_type !== $lease_type) {
666 'Blueprint "%s" (of type "%s") is not properly implemented: it '.
667 'built a resource of type "%s" to satisfy a lease requesting a '.
668 'resource of type "%s".',
669 $blueprint->getBlueprintName(),
670 $blueprint->getClassName(),
676 private function reclaimResources(
677 DrydockBlueprint
$blueprint,
678 DrydockLease
$lease) {
679 $viewer = $this->getViewer();
681 // If this lease is marked as already in the process of reclaiming a
682 // resource, don't let it reclaim another one until the first reclaim
683 // completes. This stops one lease from reclaiming a large number of
684 // resources if the reclaims take a while to complete.
685 $reclaiming_phid = $lease->getAttribute('drydock.reclaimingPHID');
686 if ($reclaiming_phid) {
687 $reclaiming_resource = id(new DrydockResourceQuery())
689 ->withPHIDs(array($reclaiming_phid))
692 DrydockResourceStatus
::STATUS_ACTIVE
,
693 DrydockResourceStatus
::STATUS_RELEASED
,
696 if ($reclaiming_resource) {
701 $resources = id(new DrydockResourceQuery())
703 ->withBlueprintPHIDs(array($blueprint->getPHID()))
706 DrydockResourceStatus
::STATUS_ACTIVE
,
710 // TODO: We could be much smarter about this and try to release long-unused
711 // resources, resources with many similar copies, old resources, resources
712 // that are cheap to rebuild, etc.
715 foreach ($resources as $resource) {
716 if ($this->canReclaimResource($resource)) {
717 $this->reclaimResource($resource, $lease);
726 /* -( Acquiring Leases )--------------------------------------------------- */
730 * Perform an actual lease acquisition on a particular resource.
732 * @param DrydockResource Resource to acquire a lease on.
733 * @param DrydockLease Lease to acquire.
737 private function acquireLease(
738 DrydockResource
$resource,
739 DrydockLease
$lease) {
741 $blueprint = $resource->getBlueprint();
742 $blueprint->acquireLease($resource, $lease);
744 $this->validateAcquiredLease($blueprint, $resource, $lease);
746 // If this lease has been acquired but not activated, queue a task to
748 if ($lease->getStatus() == DrydockLeaseStatus
::STATUS_ACQUIRED
) {
752 'leasePHID' => $lease->getPHID(),
755 'objectPHID' => $lease->getPHID(),
762 * Make sure that a lease was really acquired properly.
764 * @param DrydockBlueprint Blueprint which created the resource.
765 * @param DrydockResource Resource which was acquired.
766 * @param DrydockLease The lease which was supposedly acquired.
770 private function validateAcquiredLease(
771 DrydockBlueprint
$blueprint,
772 DrydockResource
$resource,
773 DrydockLease
$lease) {
775 if (!$lease->isAcquiredLease()) {
778 'Blueprint "%s" (of type "%s") is not properly implemented: it '.
779 'returned from "%s" without acquiring a lease.',
780 $blueprint->getBlueprintName(),
781 $blueprint->getClassName(),
785 $lease_phid = $lease->getResourcePHID();
786 $resource_phid = $resource->getPHID();
788 if ($lease_phid !== $resource_phid) {
791 'Blueprint "%s" (of type "%s") is not properly implemented: it '.
792 'returned from "%s" with a lease acquired on the wrong resource.',
793 $blueprint->getBlueprintName(),
794 $blueprint->getClassName(),
799 private function newResourceForAcquisition(
800 DrydockResource
$resource,
801 DrydockLease
$lease) {
803 // If the resource has no leases against it, never build a new one. This is
804 // likely already a new resource that just activated.
805 $viewer = $this->getViewer();
808 DrydockLeaseStatus
::STATUS_PENDING
,
809 DrydockLeaseStatus
::STATUS_ACQUIRED
,
810 DrydockLeaseStatus
::STATUS_ACTIVE
,
813 $leases = id(new DrydockLeaseQuery())
815 ->withResourcePHIDs(array($resource->getPHID()))
816 ->withStatuses($statuses)
823 // If we're about to get a lease on a resource, check if the blueprint
824 // wants to allocate a supplemental resource. If it does, try to perform a
825 // new allocation instead.
826 $blueprint = $resource->getBlueprint();
827 if (!$blueprint->shouldAllocateSupplementalResource($resource, $lease)) {
831 // If the blueprint is already overallocated, we can't allocate a new
832 // resource. Just return the existing resource.
833 $remaining = $this->removeOverallocatedBlueprints(
840 // Try to build a new resource.
842 $new_resource = $this->allocateResource($blueprint, $lease);
843 } catch (Exception
$ex) {
844 $blueprint->logEvent(
845 DrydockResourceAllocationFailureLogType
::LOGCONST
,
847 'class' => get_class($ex),
848 'message' => $ex->getMessage(),
854 // If we can't actually acquire the new resource yet, just yield.
855 // (We could try to move forward with the original resource instead.)
856 $acquirable = $this->removeUnacquirableResources(
857 array($new_resource),
860 throw new PhabricatorWorkerYieldException(15);
863 return $new_resource;
867 /* -( Activating Leases )-------------------------------------------------- */
873 private function activateLease(DrydockLease
$lease) {
874 $resource = $lease->getResource();
877 pht('Trying to activate lease with no resource.'));
880 $resource_status = $resource->getStatus();
882 if ($resource_status == DrydockResourceStatus
::STATUS_PENDING
) {
883 throw new PhabricatorWorkerYieldException(15);
886 if ($resource_status != DrydockResourceStatus
::STATUS_ACTIVE
) {
887 throw new DrydockAcquiredBrokenResourceException(
889 'Trying to activate lease ("%s") on a resource ("%s") in '.
890 'the wrong status ("%s").',
892 $resource->getPHID(),
896 // NOTE: We can race resource destruction here. Between the time we
897 // performed the read above and now, the resource might have closed, so
898 // we may activate leases on dead resources. At least for now, this seems
899 // fine: a resource dying right before we activate a lease on it should not
900 // be distinguishable from a resource dying right after we activate a lease
901 // on it. We end up with an active lease on a dead resource either way, and
902 // can not prevent resources dying from lightning strikes.
904 $blueprint = $resource->getBlueprint();
905 $blueprint->activateLease($resource, $lease);
906 $this->validateActivatedLease($blueprint, $resource, $lease);
912 private function validateActivatedLease(
913 DrydockBlueprint
$blueprint,
914 DrydockResource
$resource,
915 DrydockLease
$lease) {
917 if (!$lease->isActivatedLease()) {
920 'Blueprint "%s" (of type "%s") is not properly implemented: it '.
921 'returned from "%s" without activating a lease.',
922 $blueprint->getBlueprintName(),
923 $blueprint->getClassName(),
930 /* -( Releasing Leases )--------------------------------------------------- */
936 private function releaseLease(DrydockLease
$lease) {
938 ->setStatus(DrydockLeaseStatus
::STATUS_RELEASED
)
941 $lease->logEvent(DrydockLeaseReleasedLogType
::LOGCONST
);
943 $resource = $lease->getResource();
945 $blueprint = $resource->getBlueprint();
946 $blueprint->didReleaseLease($resource, $lease);
949 $this->destroyLease($lease);
953 /* -( Breaking Leases )---------------------------------------------------- */
959 protected function breakLease(DrydockLease
$lease, Exception
$ex) {
960 switch ($lease->getStatus()) {
961 case DrydockLeaseStatus
::STATUS_BROKEN
:
962 case DrydockLeaseStatus
::STATUS_RELEASED
:
963 case DrydockLeaseStatus
::STATUS_DESTROYED
:
964 throw new PhutilProxyException(
966 'Unexpected failure while destroying lease ("%s").',
972 ->setStatus(DrydockLeaseStatus
::STATUS_BROKEN
)
976 DrydockLeaseActivationFailureLogType
::LOGCONST
,
978 'class' => get_class($ex),
979 'message' => $ex->getMessage(),
982 $lease->awakenTasks();
987 'leasePHID' => $lease->getPHID(),
990 'objectPHID' => $lease->getPHID(),
993 throw new PhabricatorWorkerPermanentFailureException(
995 'Permanent failure while activating lease ("%s"): %s',
1001 /* -( Destroying Leases )-------------------------------------------------- */
1007 private function destroyLease(DrydockLease
$lease) {
1008 $resource = $lease->getResource();
1011 $blueprint = $resource->getBlueprint();
1012 $blueprint->destroyLease($resource, $lease);
1015 DrydockSlotLock
::releaseLocks($lease->getPHID());
1018 ->setStatus(DrydockLeaseStatus
::STATUS_DESTROYED
)
1021 $lease->logEvent(DrydockLeaseDestroyedLogType
::LOGCONST
);
1023 $lease->awakenTasks();