Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / drydock / worker / DrydockLeaseUpdateWorker.php
blob040227669602e752acdd51973a88e870a5b6ba85
1 <?php
3 /**
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)
22 ->lock(1);
24 try {
25 $lease = $this->loadLease($lease_phid);
26 $this->handleUpdate($lease);
27 } catch (Exception $ex) {
28 $lock->unlock();
29 $this->flushDrydockTaskQueue();
30 throw $ex;
33 $lock->unlock();
37 /* -( Updating Leases )---------------------------------------------------- */
40 /**
41 * @task update
43 private function handleUpdate(DrydockLease $lease) {
44 try {
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());
54 $lease
55 ->setStatus(DrydockLeaseStatus::STATUS_PENDING)
56 ->setResourcePHID(null)
57 ->save();
59 $lease->logEvent(
60 DrydockLeaseReacquireLogType::LOGCONST,
61 array(
62 'class' => get_class($ex),
63 'message' => $ex->getMessage(),
64 ));
66 $this->yieldLease($lease, $ex);
67 } catch (Exception $ex) {
68 if ($this->isTemporaryException($ex)) {
69 $this->yieldLease($lease, $ex);
70 } else {
71 $this->breakLease($lease, $ex);
77 /**
78 * @task update
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);
87 break;
88 case DrydockLeaseStatus::STATUS_ACQUIRED:
89 $this->activateLease($lease);
90 break;
91 case DrydockLeaseStatus::STATUS_ACTIVE:
92 // Nothing to do.
93 break;
94 case DrydockLeaseStatus::STATUS_RELEASED:
95 case DrydockLeaseStatus::STATUS_BROKEN:
96 $this->destroyLease($lease);
97 break;
98 case DrydockLeaseStatus::STATUS_DESTROYED:
99 break;
102 $this->yieldIfExpiringLease($lease);
107 * @task update
109 private function yieldLease(DrydockLease $lease, Exception $ex) {
110 $duration = $this->getYieldDurationFromException($ex);
112 $lease->logEvent(
113 DrydockLeaseActivationYieldLogType::LOGCONST,
114 array(
115 'duration' => $duration,
118 throw new PhabricatorWorkerYieldException($duration);
122 /* -( Processing Commands )------------------------------------------------ */
126 * @task command
128 private function processLeaseCommands(DrydockLease $lease) {
129 if (!$lease->canReceiveCommands()) {
130 return;
133 $this->checkLeaseExpiration($lease);
135 $commands = $this->loadCommands($lease->getPHID());
136 foreach ($commands as $command) {
137 if (!$lease->canReceiveCommands()) {
138 break;
141 $this->processLeaseCommand($lease, $command);
143 $command
144 ->setIsConsumed(true)
145 ->save();
151 * @task command
153 private function processLeaseCommand(
154 DrydockLease $lease,
155 DrydockCommand $command) {
156 switch ($command->getCommand()) {
157 case DrydockCommand::COMMAND_RELEASE:
158 $this->releaseLease($lease);
159 break;
164 /* -( Drydock Allocator )-------------------------------------------------- */
168 * Find or build a resource which can satisfy a given lease request, then
169 * acquire the lease.
171 * @param DrydockLease Requested lease.
172 * @return void
173 * @task allocator
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.
181 if (!$blueprints) {
182 throw new PhabricatorWorkerPermanentFailureException(
183 pht(
184 'No active Drydock blueprint exists which can ever allocate a '.
185 'resource for lease "%s".',
186 $lease->getPHID()));
189 // First, try to find a suitable open resource which we can acquire a new
190 // lease on.
191 $resources = $this->loadAcquirableResourcesForLease($blueprints, $lease);
193 list($free_resources, $used_resources) = $this->partitionResources(
194 $lease,
195 $resources);
197 $resource = $this->leaseAnyResource($lease, $free_resources);
198 if ($resource) {
199 return $resource;
202 // We're about to try creating a resource. If we're already creating
203 // something, just yield until that resolves.
205 $this->yieldForPendingResources($lease);
207 // We haven't been able to lease an existing resource yet, so now we try to
208 // create one. We may still have some less-desirable "used" resources that
209 // we'll sometimes try to lease later if we fail to allocate a new resource.
211 $resource = $this->newLeasedResource($lease, $blueprints);
212 if ($resource) {
213 return $resource;
216 // We haven't been able to lease a desirable "free" resource or create a
217 // new resource. Try to lease a "used" resource.
219 $resource = $this->leaseAnyResource($lease, $used_resources);
220 if ($resource) {
221 return $resource;
224 // If this lease has already triggered a reclaim, just yield and wait for
225 // it to resolve.
226 $this->yieldForReclaimingResources($lease);
228 // Try to reclaim a resource. This will yield if it reclaims something.
229 $this->reclaimAnyResource($lease, $blueprints);
231 // We weren't able to lease, create, or reclaim any resources. We just have
232 // to wait for resources to become available.
234 $lease->logEvent(
235 DrydockLeaseWaitingForResourcesLogType::LOGCONST,
236 array(
237 'blueprintPHIDs' => mpull($blueprints, 'getPHID'),
240 throw new PhabricatorWorkerYieldException(15);
243 private function reclaimAnyResource(DrydockLease $lease, array $blueprints) {
244 assert_instances_of($blueprints, 'DrydockBlueprint');
246 $blueprints = $this->rankBlueprints($blueprints, $lease);
248 // Try to actively reclaim unused resources. If we succeed, jump back
249 // into the queue in an effort to claim it.
251 foreach ($blueprints as $blueprint) {
252 $reclaimed = $this->reclaimResources($blueprint, $lease);
253 if ($reclaimed) {
255 $lease->logEvent(
256 DrydockLeaseReclaimLogType::LOGCONST,
257 array(
258 'resourcePHIDs' => array($reclaimed->getPHID()),
261 // Yield explicitly here: we'll be awakened when the resource is
262 // reclaimed.
264 throw new PhabricatorWorkerYieldException(15);
269 private function yieldForPendingResources(DrydockLease $lease) {
270 // See T13677. If this lease has already triggered the allocation of
271 // one or more resources and they are still pending, just yield and
272 // wait for them.
274 $viewer = $this->getViewer();
276 $phids = $lease->getAllocatedResourcePHIDs();
277 if (!$phids) {
278 return null;
281 $resources = id(new DrydockResourceQuery())
282 ->setViewer($viewer)
283 ->withPHIDs($phids)
284 ->withStatuses(
285 array(
286 DrydockResourceStatus::STATUS_PENDING,
288 ->setLimit(1)
289 ->execute();
290 if (!$resources) {
291 return;
294 $lease->logEvent(
295 DrydockLeaseWaitingForActivationLogType::LOGCONST,
296 array(
297 'resourcePHIDs' => mpull($resources, 'getPHID'),
300 throw new PhabricatorWorkerYieldException(15);
303 private function yieldForReclaimingResources(DrydockLease $lease) {
304 $viewer = $this->getViewer();
306 $phids = $lease->getReclaimedResourcePHIDs();
307 if (!$phids) {
308 return;
311 $resources = id(new DrydockResourceQuery())
312 ->setViewer($viewer)
313 ->withPHIDs($phids)
314 ->withStatuses(
315 array(
316 DrydockResourceStatus::STATUS_ACTIVE,
317 DrydockResourceStatus::STATUS_RELEASED,
319 ->setLimit(1)
320 ->execute();
321 if (!$resources) {
322 return;
325 $lease->logEvent(
326 DrydockLeaseWaitingForReclamationLogType::LOGCONST,
327 array(
328 'resourcePHIDs' => mpull($resources, 'getPHID'),
331 throw new PhabricatorWorkerYieldException(15);
334 private function newLeasedResource(
335 DrydockLease $lease,
336 array $blueprints) {
337 assert_instances_of($blueprints, 'DrydockBlueprint');
339 $usable_blueprints = $this->removeOverallocatedBlueprints(
340 $blueprints,
341 $lease);
343 // If we get nothing back here, some blueprint claims it can eventually
344 // satisfy the lease, just not right now. This is a temporary failure,
345 // and we expect allocation to succeed eventually.
347 // Return, try to lease a "used" resource, and continue from there.
349 if (!$usable_blueprints) {
350 return null;
353 $usable_blueprints = $this->rankBlueprints($usable_blueprints, $lease);
355 $new_resources = $this->newResources($lease, $usable_blueprints);
356 if (!$new_resources) {
357 // If we were unable to create any new resources, return and
358 // try to lease a "used" resource.
359 return null;
362 $new_resources = $this->removeUnacquirableResources(
363 $new_resources,
364 $lease);
365 if (!$new_resources) {
366 // If we make it here, we just built a resource but aren't allowed
367 // to acquire it. We expect this to happen if the resource prevents
368 // acquisition until it activates, which is common when a resource
369 // needs to perform setup steps.
371 // Explicitly yield and wait for activation, since we don't want to
372 // lease a "used" resource.
374 throw new PhabricatorWorkerYieldException(15);
377 $resource = $this->leaseAnyResource($lease, $new_resources);
378 if ($resource) {
379 return $resource;
382 // We may not be able to lease a resource even if we just built it:
383 // another process may snatch it up before we can lease it. This should
384 // be rare, but is not concerning. Just try to build another resource.
386 // We likely could try to build the next resource immediately, but err on
387 // the side of caution and yield for now, at least until this code is
388 // better vetted.
390 throw new PhabricatorWorkerYieldException(15);
393 private function partitionResources(
394 DrydockLease $lease,
395 array $resources) {
397 assert_instances_of($resources, 'DrydockResource');
398 $viewer = $this->getViewer();
400 $lease_statuses = array(
401 DrydockLeaseStatus::STATUS_PENDING,
402 DrydockLeaseStatus::STATUS_ACQUIRED,
403 DrydockLeaseStatus::STATUS_ACTIVE,
406 // Partition resources into "free" resources (which we can try to lease
407 // immediately) and "used" resources, which we can only to lease after we
408 // fail to allocate a new resource.
410 // "Free" resources are unleased and/or prefer reuse over allocation.
411 // "Used" resources are leased and prefer allocation over reuse.
413 $free_resources = array();
414 $used_resources = array();
416 foreach ($resources as $resource) {
417 $blueprint = $resource->getBlueprint();
419 if (!$blueprint->shouldAllocateSupplementalResource($resource, $lease)) {
420 $free_resources[] = $resource;
421 continue;
424 $leases = id(new DrydockLeaseQuery())
425 ->setViewer($viewer)
426 ->withResourcePHIDs(array($resource->getPHID()))
427 ->withStatuses($lease_statuses)
428 ->setLimit(1)
429 ->execute();
430 if (!$leases) {
431 $free_resources[] = $resource;
432 continue;
435 $used_resources[] = $resource;
438 return array($free_resources, $used_resources);
441 private function newResources(
442 DrydockLease $lease,
443 array $blueprints) {
444 assert_instances_of($blueprints, 'DrydockBlueprint');
446 $resources = array();
447 $exceptions = array();
448 foreach ($blueprints as $blueprint) {
449 $caught = null;
450 try {
451 $resources[] = $this->allocateResource($blueprint, $lease);
453 // Bail after allocating one resource, we don't need any more than
454 // this.
455 break;
456 } catch (Exception $ex) {
457 $caught = $ex;
458 } catch (Throwable $ex) {
459 $caught = $ex;
462 if ($caught) {
463 // This failure is not normally expected, so log it. It can be
464 // caused by something mundane and recoverable, however (see below
465 // for discussion).
467 // We log to the blueprint separately from the log to the lease:
468 // the lease is not attached to a blueprint yet so the lease log
469 // will not show up on the blueprint; more than one blueprint may
470 // fail; and the lease is not really impacted (and won't log) if at
471 // least one blueprint actually works.
473 $blueprint->logEvent(
474 DrydockResourceAllocationFailureLogType::LOGCONST,
475 array(
476 'class' => get_class($caught),
477 'message' => $caught->getMessage(),
480 $exceptions[] = $caught;
484 if (!$resources) {
485 // If one or more blueprints claimed that they would be able to allocate
486 // resources but none are actually able to allocate resources, log the
487 // failure and yield so we try again soon.
489 // This can happen if some unexpected issue occurs during allocation
490 // (for example, a call to build a VM fails for some reason) or if we
491 // raced another allocator and the blueprint is now full.
493 $ex = new PhutilAggregateException(
494 pht(
495 'All blueprints failed to allocate a suitable new resource when '.
496 'trying to allocate lease ("%s").',
497 $lease->getPHID()),
498 $exceptions);
500 $lease->logEvent(
501 DrydockLeaseAllocationFailureLogType::LOGCONST,
502 array(
503 'class' => get_class($ex),
504 'message' => $ex->getMessage(),
507 return null;
510 return $resources;
514 private function leaseAnyResource(
515 DrydockLease $lease,
516 array $resources) {
517 assert_instances_of($resources, 'DrydockResource');
519 if (!$resources) {
520 return null;
523 $resources = $this->rankResources($resources, $lease);
525 $exceptions = array();
526 $yields = array();
528 $allocated = null;
529 foreach ($resources as $resource) {
530 try {
531 $this->acquireLease($resource, $lease);
532 $allocated = $resource;
533 break;
534 } catch (DrydockResourceLockException $ex) {
535 // We need to lock the resource to actually acquire it. If we aren't
536 // able to acquire the lock quickly enough, we can yield and try again
537 // later.
538 $yields[] = $ex;
539 } catch (DrydockSlotLockException $ex) {
540 // This also just indicates we ran into some kind of contention,
541 // probably from another lease. Just yield.
542 $yields[] = $ex;
543 } catch (DrydockAcquiredBrokenResourceException $ex) {
544 // If a resource was reclaimed or destroyed by the time we actually
545 // got around to acquiring it, we just got unlucky.
546 $yields[] = $ex;
547 } catch (PhabricatorWorkerYieldException $ex) {
548 // We can be told to yield, particularly by the supplemental allocator
549 // trying to give us a supplemental resource.
550 $yields[] = $ex;
551 } catch (Exception $ex) {
552 $exceptions[] = $ex;
556 if ($allocated) {
557 return $allocated;
560 if ($yields) {
561 throw new PhabricatorWorkerYieldException(15);
564 throw new PhutilAggregateException(
565 pht(
566 'Unable to acquire lease "%s" on any resource.',
567 $lease->getPHID()),
568 $exceptions);
573 * Get all the concrete @{class:DrydockBlueprint}s which can possibly
574 * build a resource to satisfy a lease.
576 * @param DrydockLease Requested lease.
577 * @return list<DrydockBlueprint> List of qualifying blueprints.
578 * @task allocator
580 private function loadBlueprintsForAllocatingLease(
581 DrydockLease $lease) {
582 $viewer = $this->getViewer();
584 $impls = DrydockBlueprintImplementation::getAllForAllocatingLease($lease);
585 if (!$impls) {
586 return array();
589 $blueprint_phids = $lease->getAllowedBlueprintPHIDs();
590 if (!$blueprint_phids) {
591 $lease->logEvent(DrydockLeaseNoBlueprintsLogType::LOGCONST);
592 return array();
595 $query = id(new DrydockBlueprintQuery())
596 ->setViewer($viewer)
597 ->withPHIDs($blueprint_phids)
598 ->withBlueprintClasses(array_keys($impls))
599 ->withDisabled(false);
601 // The Drydock application itself is allowed to authorize anything. This
602 // is primarily used for leases generated by CLI administrative tools.
603 $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID();
605 $authorizing_phid = $lease->getAuthorizingPHID();
606 if ($authorizing_phid != $drydock_phid) {
607 $blueprints = id(clone $query)
608 ->withAuthorizedPHIDs(array($authorizing_phid))
609 ->execute();
610 if (!$blueprints) {
611 // If we didn't hit any blueprints, check if this is an authorization
612 // problem: re-execute the query without the authorization constraint.
613 // If the second query hits blueprints, the overall configuration is
614 // fine but this is an authorization problem. If the second query also
615 // comes up blank, this is some other kind of configuration issue so
616 // we fall through to the default pathway.
617 $all_blueprints = $query->execute();
618 if ($all_blueprints) {
619 $lease->logEvent(
620 DrydockLeaseNoAuthorizationsLogType::LOGCONST,
621 array(
622 'authorizingPHID' => $authorizing_phid,
624 return array();
627 } else {
628 $blueprints = $query->execute();
631 $keep = array();
632 foreach ($blueprints as $key => $blueprint) {
633 if (!$blueprint->canEverAllocateResourceForLease($lease)) {
634 continue;
637 $keep[$key] = $blueprint;
640 return $keep;
645 * Load a list of all resources which a given lease can possibly be
646 * allocated against.
648 * @param list<DrydockBlueprint> Blueprints which may produce suitable
649 * resources.
650 * @param DrydockLease Requested lease.
651 * @return list<DrydockResource> Resources which may be able to allocate
652 * the lease.
653 * @task allocator
655 private function loadAcquirableResourcesForLease(
656 array $blueprints,
657 DrydockLease $lease) {
658 assert_instances_of($blueprints, 'DrydockBlueprint');
659 $viewer = $this->getViewer();
661 $resources = id(new DrydockResourceQuery())
662 ->setViewer($viewer)
663 ->withBlueprintPHIDs(mpull($blueprints, 'getPHID'))
664 ->withTypes(array($lease->getResourceType()))
665 ->withStatuses(
666 array(
667 DrydockResourceStatus::STATUS_ACTIVE,
669 ->execute();
671 return $this->removeUnacquirableResources($resources, $lease);
676 * Remove resources which can not be acquired by a given lease from a list.
678 * @param list<DrydockResource> Candidate resources.
679 * @param DrydockLease Acquiring lease.
680 * @return list<DrydockResource> Resources which the lease may be able to
681 * acquire.
682 * @task allocator
684 private function removeUnacquirableResources(
685 array $resources,
686 DrydockLease $lease) {
687 $keep = array();
688 foreach ($resources as $key => $resource) {
689 $blueprint = $resource->getBlueprint();
691 if (!$blueprint->canAcquireLeaseOnResource($resource, $lease)) {
692 continue;
695 $keep[$key] = $resource;
698 return $keep;
703 * Remove blueprints which are too heavily allocated to build a resource for
704 * a lease from a list of blueprints.
706 * @param list<DrydockBlueprint> List of blueprints.
707 * @return list<DrydockBlueprint> List with blueprints that can not allocate
708 * a resource for the lease right now removed.
709 * @task allocator
711 private function removeOverallocatedBlueprints(
712 array $blueprints,
713 DrydockLease $lease) {
714 assert_instances_of($blueprints, 'DrydockBlueprint');
716 $keep = array();
718 foreach ($blueprints as $key => $blueprint) {
719 if (!$blueprint->canAllocateResourceForLease($lease)) {
720 continue;
723 $keep[$key] = $blueprint;
726 return $keep;
731 * Rank blueprints by suitability for building a new resource for a
732 * particular lease.
734 * @param list<DrydockBlueprint> List of blueprints.
735 * @param DrydockLease Requested lease.
736 * @return list<DrydockBlueprint> Ranked list of blueprints.
737 * @task allocator
739 private function rankBlueprints(array $blueprints, DrydockLease $lease) {
740 assert_instances_of($blueprints, 'DrydockBlueprint');
742 // TODO: Implement improvements to this ranking algorithm if they become
743 // available.
744 shuffle($blueprints);
746 return $blueprints;
751 * Rank resources by suitability for allocating a particular lease.
753 * @param list<DrydockResource> List of resources.
754 * @param DrydockLease Requested lease.
755 * @return list<DrydockResource> Ranked list of resources.
756 * @task allocator
758 private function rankResources(array $resources, DrydockLease $lease) {
759 assert_instances_of($resources, 'DrydockResource');
761 // TODO: Implement improvements to this ranking algorithm if they become
762 // available.
763 shuffle($resources);
765 return $resources;
770 * Perform an actual resource allocation with a particular blueprint.
772 * @param DrydockBlueprint The blueprint to allocate a resource from.
773 * @param DrydockLease Requested lease.
774 * @return DrydockResource Allocated resource.
775 * @task allocator
777 private function allocateResource(
778 DrydockBlueprint $blueprint,
779 DrydockLease $lease) {
780 $resource = $blueprint->allocateResource($lease);
781 $this->validateAllocatedResource($blueprint, $resource, $lease);
783 // If this resource was allocated as a pending resource, queue a task to
784 // activate it.
785 if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) {
787 $lease->addAllocatedResourcePHIDs(
788 array(
789 $resource->getPHID(),
791 $lease->save();
793 PhabricatorWorker::scheduleTask(
794 'DrydockResourceUpdateWorker',
795 array(
796 'resourcePHID' => $resource->getPHID(),
798 // This task will generally yield while the resource activates, so
799 // wake it back up once the resource comes online. Most of the time,
800 // we'll be able to lease the newly activated resource.
801 'awakenOnActivation' => array(
802 $this->getCurrentWorkerTaskID(),
805 array(
806 'objectPHID' => $resource->getPHID(),
810 return $resource;
815 * Check that the resource a blueprint allocated is roughly the sort of
816 * object we expect.
818 * @param DrydockBlueprint Blueprint which built the resource.
819 * @param wild Thing which the blueprint claims is a valid resource.
820 * @param DrydockLease Lease the resource was allocated for.
821 * @return void
822 * @task allocator
824 private function validateAllocatedResource(
825 DrydockBlueprint $blueprint,
826 $resource,
827 DrydockLease $lease) {
829 if (!($resource instanceof DrydockResource)) {
830 throw new Exception(
831 pht(
832 'Blueprint "%s" (of type "%s") is not properly implemented: %s must '.
833 'return an object of type %s or throw, but returned something else.',
834 $blueprint->getBlueprintName(),
835 $blueprint->getClassName(),
836 'allocateResource()',
837 'DrydockResource'));
840 if (!$resource->isAllocatedResource()) {
841 throw new Exception(
842 pht(
843 'Blueprint "%s" (of type "%s") is not properly implemented: %s '.
844 'must actually allocate the resource it returns.',
845 $blueprint->getBlueprintName(),
846 $blueprint->getClassName(),
847 'allocateResource()'));
850 $resource_type = $resource->getType();
851 $lease_type = $lease->getResourceType();
853 if ($resource_type !== $lease_type) {
854 throw new Exception(
855 pht(
856 'Blueprint "%s" (of type "%s") is not properly implemented: it '.
857 'built a resource of type "%s" to satisfy a lease requesting a '.
858 'resource of type "%s".',
859 $blueprint->getBlueprintName(),
860 $blueprint->getClassName(),
861 $resource_type,
862 $lease_type));
866 private function reclaimResources(
867 DrydockBlueprint $blueprint,
868 DrydockLease $lease) {
869 $viewer = $this->getViewer();
871 $resources = id(new DrydockResourceQuery())
872 ->setViewer($viewer)
873 ->withBlueprintPHIDs(array($blueprint->getPHID()))
874 ->withStatuses(
875 array(
876 DrydockResourceStatus::STATUS_ACTIVE,
878 ->execute();
880 // TODO: We could be much smarter about this and try to release long-unused
881 // resources, resources with many similar copies, old resources, resources
882 // that are cheap to rebuild, etc.
883 shuffle($resources);
885 foreach ($resources as $resource) {
886 if ($this->canReclaimResource($resource)) {
887 $this->reclaimResource($resource, $lease);
888 return $resource;
892 return null;
896 /* -( Acquiring Leases )--------------------------------------------------- */
900 * Perform an actual lease acquisition on a particular resource.
902 * @param DrydockResource Resource to acquire a lease on.
903 * @param DrydockLease Lease to acquire.
904 * @return void
905 * @task acquire
907 private function acquireLease(
908 DrydockResource $resource,
909 DrydockLease $lease) {
911 $blueprint = $resource->getBlueprint();
912 $blueprint->acquireLease($resource, $lease);
914 $this->validateAcquiredLease($blueprint, $resource, $lease);
916 // If this lease has been acquired but not activated, queue a task to
917 // activate it.
918 if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACQUIRED) {
919 $this->queueTask(
920 __CLASS__,
921 array(
922 'leasePHID' => $lease->getPHID(),
924 array(
925 'objectPHID' => $lease->getPHID(),
932 * Make sure that a lease was really acquired properly.
934 * @param DrydockBlueprint Blueprint which created the resource.
935 * @param DrydockResource Resource which was acquired.
936 * @param DrydockLease The lease which was supposedly acquired.
937 * @return void
938 * @task acquire
940 private function validateAcquiredLease(
941 DrydockBlueprint $blueprint,
942 DrydockResource $resource,
943 DrydockLease $lease) {
945 if (!$lease->isAcquiredLease()) {
946 throw new Exception(
947 pht(
948 'Blueprint "%s" (of type "%s") is not properly implemented: it '.
949 'returned from "%s" without acquiring a lease.',
950 $blueprint->getBlueprintName(),
951 $blueprint->getClassName(),
952 'acquireLease()'));
955 $lease_phid = $lease->getResourcePHID();
956 $resource_phid = $resource->getPHID();
958 if ($lease_phid !== $resource_phid) {
959 throw new Exception(
960 pht(
961 'Blueprint "%s" (of type "%s") is not properly implemented: it '.
962 'returned from "%s" with a lease acquired on the wrong resource.',
963 $blueprint->getBlueprintName(),
964 $blueprint->getClassName(),
965 'acquireLease()'));
970 /* -( Activating Leases )-------------------------------------------------- */
974 * @task activate
976 private function activateLease(DrydockLease $lease) {
977 $resource = $lease->getResource();
978 if (!$resource) {
979 throw new Exception(
980 pht('Trying to activate lease with no resource.'));
983 $resource_status = $resource->getStatus();
985 if ($resource_status == DrydockResourceStatus::STATUS_PENDING) {
986 throw new PhabricatorWorkerYieldException(15);
989 if ($resource_status != DrydockResourceStatus::STATUS_ACTIVE) {
990 throw new DrydockAcquiredBrokenResourceException(
991 pht(
992 'Trying to activate lease ("%s") on a resource ("%s") in '.
993 'the wrong status ("%s").',
994 $lease->getPHID(),
995 $resource->getPHID(),
996 $resource_status));
999 // NOTE: We can race resource destruction here. Between the time we
1000 // performed the read above and now, the resource might have closed, so
1001 // we may activate leases on dead resources. At least for now, this seems
1002 // fine: a resource dying right before we activate a lease on it should not
1003 // be distinguishable from a resource dying right after we activate a lease
1004 // on it. We end up with an active lease on a dead resource either way, and
1005 // can not prevent resources dying from lightning strikes.
1007 $blueprint = $resource->getBlueprint();
1008 $blueprint->activateLease($resource, $lease);
1009 $this->validateActivatedLease($blueprint, $resource, $lease);
1013 * @task activate
1015 private function validateActivatedLease(
1016 DrydockBlueprint $blueprint,
1017 DrydockResource $resource,
1018 DrydockLease $lease) {
1020 if (!$lease->isActivatedLease()) {
1021 throw new Exception(
1022 pht(
1023 'Blueprint "%s" (of type "%s") is not properly implemented: it '.
1024 'returned from "%s" without activating a lease.',
1025 $blueprint->getBlueprintName(),
1026 $blueprint->getClassName(),
1027 'acquireLease()'));
1033 /* -( Releasing Leases )--------------------------------------------------- */
1037 * @task release
1039 private function releaseLease(DrydockLease $lease) {
1040 $lease
1041 ->setStatus(DrydockLeaseStatus::STATUS_RELEASED)
1042 ->save();
1044 $lease->logEvent(DrydockLeaseReleasedLogType::LOGCONST);
1046 $resource = $lease->getResource();
1047 if ($resource) {
1048 $blueprint = $resource->getBlueprint();
1049 $blueprint->didReleaseLease($resource, $lease);
1052 $this->destroyLease($lease);
1056 /* -( Breaking Leases )---------------------------------------------------- */
1060 * @task break
1062 protected function breakLease(DrydockLease $lease, Exception $ex) {
1063 switch ($lease->getStatus()) {
1064 case DrydockLeaseStatus::STATUS_BROKEN:
1065 case DrydockLeaseStatus::STATUS_RELEASED:
1066 case DrydockLeaseStatus::STATUS_DESTROYED:
1067 throw new PhutilProxyException(
1068 pht(
1069 'Unexpected failure while destroying lease ("%s").',
1070 $lease->getPHID()),
1071 $ex);
1074 $lease
1075 ->setStatus(DrydockLeaseStatus::STATUS_BROKEN)
1076 ->save();
1078 $lease->logEvent(
1079 DrydockLeaseActivationFailureLogType::LOGCONST,
1080 array(
1081 'class' => get_class($ex),
1082 'message' => $ex->getMessage(),
1085 $lease->awakenTasks();
1087 $this->queueTask(
1088 __CLASS__,
1089 array(
1090 'leasePHID' => $lease->getPHID(),
1092 array(
1093 'objectPHID' => $lease->getPHID(),
1096 throw new PhabricatorWorkerPermanentFailureException(
1097 pht(
1098 'Permanent failure while activating lease ("%s"): %s',
1099 $lease->getPHID(),
1100 $ex->getMessage()));
1104 /* -( Destroying Leases )-------------------------------------------------- */
1108 * @task destroy
1110 private function destroyLease(DrydockLease $lease) {
1111 $resource = $lease->getResource();
1113 if ($resource) {
1114 $blueprint = $resource->getBlueprint();
1115 $blueprint->destroyLease($resource, $lease);
1118 DrydockSlotLock::releaseLocks($lease->getPHID());
1120 $lease
1121 ->setStatus(DrydockLeaseStatus::STATUS_DESTROYED)
1122 ->save();
1124 $lease->logEvent(DrydockLeaseDestroyedLogType::LOGCONST);
1126 $lease->awakenTasks();