@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @task lease Lease Acquisition
5 * @task resource Resource Allocation
6 * @task interface Resource Interfaces
7 * @task log Logging
8 */
9abstract class DrydockBlueprintImplementation extends Phobject {
10
11 abstract public function getType();
12
13 abstract public function isEnabled();
14
15 abstract public function getBlueprintName();
16 abstract public function getDescription();
17
18 public function getBlueprintIcon() {
19 return 'fa-map-o';
20 }
21
22 public function getFieldSpecifications() {
23 $fields = array();
24
25 $fields += $this->getCustomFieldSpecifications();
26
27 if ($this->shouldUseConcurrentResourceLimit()) {
28 $fields += array(
29 'allocator.limit' => array(
30 'name' => pht('Limit'),
31 'caption' => pht(
32 'Maximum number of resources this blueprint can have active '.
33 'concurrently.'),
34 'type' => 'int',
35 ),
36 );
37 }
38
39 return $fields;
40 }
41
42 protected function getCustomFieldSpecifications() {
43 return array();
44 }
45
46 public function getViewer() {
47 return PhabricatorUser::getOmnipotentUser();
48 }
49
50
51/* -( Lease Acquisition )-------------------------------------------------- */
52
53
54 /**
55 * Enforce basic checks on lease/resource compatibility. Allows resources to
56 * reject leases if they are incompatible, even if the resource types match.
57 *
58 * For example, if a resource represents a 32-bit host, this method might
59 * reject leases that need a 64-bit host. The blueprint might also reject
60 * a resource if the lease needs 8GB of RAM and the resource only has 6GB
61 * free.
62 *
63 * This method should not acquire locks or expect anything to be locked. This
64 * is a coarse compatibility check between a lease and a resource.
65 *
66 * @param DrydockBlueprint $blueprint Concrete blueprint to allocate for.
67 * @param DrydockResource $resource Candidate resource to allocate the lease
68 * on.
69 * @param DrydockLease $lease Pending lease that wants to allocate here.
70 * @return bool True if the resource and lease are compatible.
71 * @task lease
72 */
73 abstract public function canAcquireLeaseOnResource(
74 DrydockBlueprint $blueprint,
75 DrydockResource $resource,
76 DrydockLease $lease);
77
78
79 /**
80 * Acquire a lease. Allows resources to perform setup as leases are brought
81 * online.
82 *
83 * If acquisition fails, throw an exception.
84 *
85 * @param DrydockBlueprint $blueprint Blueprint which built the resource.
86 * @param DrydockResource $resource Resource to acquire a lease on.
87 * @param DrydockLease $lease Requested lease.
88 * @return void
89 * @task lease
90 */
91 abstract public function acquireLease(
92 DrydockBlueprint $blueprint,
93 DrydockResource $resource,
94 DrydockLease $lease);
95
96
97 /**
98 * @return void
99 * @task lease
100 */
101 public function activateLease(
102 DrydockBlueprint $blueprint,
103 DrydockResource $resource,
104 DrydockLease $lease) {
105 throw new PhutilMethodNotImplementedException();
106 }
107
108
109 /**
110 * React to a lease being released.
111 *
112 * This callback is primarily useful for automatically releasing resources
113 * once all leases are released.
114 *
115 * @param DrydockBlueprint $blueprint Blueprint which built the resource.
116 * @param DrydockResource $resource Resource a lease was released on.
117 * @param DrydockLease $lease Recently released lease.
118 * @return void
119 * @task lease
120 */
121 abstract public function didReleaseLease(
122 DrydockBlueprint $blueprint,
123 DrydockResource $resource,
124 DrydockLease $lease);
125
126
127 /**
128 * Destroy any temporary data associated with a lease.
129 *
130 * If a lease creates temporary state while held, destroy it here.
131 *
132 * @param DrydockBlueprint $blueprint Blueprint which built the resource.
133 * @param DrydockResource $resource Resource the lease is acquired on.
134 * @param DrydockLease $lease The lease being destroyed.
135 * @return void
136 * @task lease
137 */
138 abstract public function destroyLease(
139 DrydockBlueprint $blueprint,
140 DrydockResource $resource,
141 DrydockLease $lease);
142
143 /**
144 * Return true to try to allocate a new resource and expand the resource
145 * pool instead of permitting an otherwise valid acquisition on an existing
146 * resource.
147 *
148 * This allows the blueprint to provide a soft hint about when the resource
149 * pool should grow.
150 *
151 * Returning "true" in all cases generally makes sense when a blueprint
152 * controls a fixed pool of resources, like a particular number of physical
153 * hosts: you want to put all the hosts in service, so whenever it is
154 * possible to allocate a new host you want to do this.
155 *
156 * Returning "false" in all cases generally make sense when a blueprint
157 * has a flexible pool of expensive resources and you want to pack leases
158 * onto them as tightly as possible.
159 *
160 * @param DrydockBlueprint $blueprint The blueprint for an existing resource
161 * being acquired.
162 * @param DrydockResource $resource The resource being acquired, which we may
163 * want to build a supplemental resource for.
164 * @param DrydockLease $lease The current lease performing acquisition.
165 * @return bool True to prefer allocating a supplemental resource.
166 *
167 * @task lease
168 */
169 public function shouldAllocateSupplementalResource(
170 DrydockBlueprint $blueprint,
171 DrydockResource $resource,
172 DrydockLease $lease) {
173 return false;
174 }
175
176/* -( Resource Allocation )------------------------------------------------ */
177
178
179 /**
180 * Enforce fundamental implementation/lease checks. Allows implementations to
181 * reject a lease which no concrete blueprint can ever satisfy.
182 *
183 * For example, if a lease only builds ARM hosts and the lease needs a
184 * PowerPC host, it may be rejected here.
185 *
186 * This is the earliest rejection phase, and followed by
187 * @{method:canEverAllocateResourceForLease}.
188 *
189 * This method should not actually check if a resource can be allocated
190 * right now, or even if a blueprint which can allocate a suitable resource
191 * really exists, only if some blueprint may conceivably exist which could
192 * plausibly be able to build a suitable resource.
193 *
194 * @param DrydockLease $lease Requested lease.
195 * @return bool True if some concrete blueprint of this implementation's
196 * type might ever be able to build a resource for the lease.
197 * @task resource
198 */
199 abstract public function canAnyBlueprintEverAllocateResourceForLease(
200 DrydockLease $lease);
201
202
203 /**
204 * Enforce basic blueprint/lease checks. Allows blueprints to reject a lease
205 * which they can not build a resource for.
206 *
207 * This is the second rejection phase. It follows
208 * @{method:canAnyBlueprintEverAllocateResourceForLease} and is followed by
209 * @{method:canAllocateResourceForLease}.
210 *
211 * This method should not check if a resource can be built right now, only
212 * if the blueprint as configured may, at some time, be able to build a
213 * suitable resource.
214 *
215 * @param DrydockBlueprint $blueprint Blueprint which may be asked to
216 * allocate a resource.
217 * @param DrydockLease $lease Requested lease.
218 * @return bool True if this blueprint can eventually build a suitable
219 * resource for the lease, as currently configured.
220 * @task resource
221 */
222 abstract public function canEverAllocateResourceForLease(
223 DrydockBlueprint $blueprint,
224 DrydockLease $lease);
225
226
227 /**
228 * Enforce basic availability limits. Allows blueprints to reject resource
229 * allocation if they are currently overallocated.
230 *
231 * This method should perform basic capacity/limit checks. For example, if
232 * it has a limit of 6 resources and currently has 6 resources allocated,
233 * it might reject new leases.
234 *
235 * This method should not acquire locks or expect locks to be acquired. This
236 * is a coarse check to determine if the operation is likely to succeed
237 * right now without needing to acquire locks.
238 *
239 * It is expected that this method will sometimes return `true` (indicating
240 * that a resource can be allocated) but find that another allocator has
241 * eaten up free capacity by the time it actually tries to build a resource.
242 * This is normal and the allocator will recover from it.
243 *
244 * @param DrydockBlueprint $blueprint The blueprint which may be asked to
245 * allocate a resource.
246 * @param DrydockLease $lease Requested lease.
247 * @return bool True if this blueprint appears likely to be able to allocate
248 * a suitable resource.
249 * @task resource
250 */
251 abstract public function canAllocateResourceForLease(
252 DrydockBlueprint $blueprint,
253 DrydockLease $lease);
254
255
256 /**
257 * Allocate a suitable resource for a lease.
258 *
259 * This method MUST acquire, hold, and manage locks to prevent multiple
260 * allocations from racing. World state is not locked before this method is
261 * called. Blueprints are entirely responsible for any lock handling they
262 * need to perform.
263 *
264 * @param DrydockBlueprint $blueprint The blueprint which should allocate a
265 * resource.
266 * @param DrydockLease $lease Requested lease.
267 * @return DrydockResource Allocated resource.
268 * @task resource
269 */
270 abstract public function allocateResource(
271 DrydockBlueprint $blueprint,
272 DrydockLease $lease);
273
274
275 /**
276 * @task resource
277 */
278 public function activateResource(
279 DrydockBlueprint $blueprint,
280 DrydockResource $resource) {
281 throw new PhutilMethodNotImplementedException();
282 }
283
284
285 /**
286 * Destroy any temporary data associated with a resource.
287 *
288 * If a resource creates temporary state when allocated, destroy that state
289 * here. For example, you might shut down a virtual host or destroy a working
290 * copy on disk.
291 *
292 * @param DrydockBlueprint $blueprint Blueprint which built the resource.
293 * @param DrydockResource $resource Resource being destroyed.
294 * @return void
295 * @task resource
296 */
297 abstract public function destroyResource(
298 DrydockBlueprint $blueprint,
299 DrydockResource $resource);
300
301
302 /**
303 * Get a human readable name for a resource.
304 *
305 * @param DrydockBlueprint $blueprint Blueprint which built the resource.
306 * @param DrydockResource $resource Resource to get the name of.
307 * @return string Human-readable resource name.
308 * @task resource
309 */
310 abstract public function getResourceName(
311 DrydockBlueprint $blueprint,
312 DrydockResource $resource);
313
314
315/* -( Resource Interfaces )------------------------------------------------ */
316
317
318 abstract public function getInterface(
319 DrydockBlueprint $blueprint,
320 DrydockResource $resource,
321 DrydockLease $lease,
322 $type);
323
324
325/* -( Logging )------------------------------------------------------------ */
326
327
328 public static function getAllBlueprintImplementations() {
329 return id(new PhutilClassMapQuery())
330 ->setAncestorClass(self::class)
331 ->execute();
332 }
333
334
335 /**
336 * Get all the @{class:DrydockBlueprintImplementation}s which can possibly
337 * build a resource to satisfy a lease.
338 *
339 * This method returns blueprints which might, at some time, be able to
340 * build a resource which can satisfy the lease. They may not be able to
341 * build that resource right now.
342 *
343 * @param DrydockLease $lease Requested lease.
344 * @return list<DrydockBlueprintImplementation> List of qualifying blueprint
345 * implementations.
346 */
347 public static function getAllForAllocatingLease(
348 DrydockLease $lease) {
349
350 $impls = self::getAllBlueprintImplementations();
351
352 $keep = array();
353 foreach ($impls as $key => $impl) {
354 // Don't use disabled blueprint types.
355 if (!$impl->isEnabled()) {
356 continue;
357 }
358
359 // Don't use blueprint types which can't allocate the correct kind of
360 // resource.
361 if ($impl->getType() != $lease->getResourceType()) {
362 continue;
363 }
364
365 if (!$impl->canAnyBlueprintEverAllocateResourceForLease($lease)) {
366 continue;
367 }
368
369 $keep[$key] = $impl;
370 }
371
372 return $keep;
373 }
374
375 public static function getNamedImplementation($class) {
376 return idx(self::getAllBlueprintImplementations(), $class);
377 }
378
379 protected function newResourceTemplate(DrydockBlueprint $blueprint) {
380
381 $resource = id(new DrydockResource())
382 ->setBlueprintPHID($blueprint->getPHID())
383 ->attachBlueprint($blueprint)
384 ->setType($this->getType())
385 ->setStatus(DrydockResourceStatus::STATUS_PENDING);
386
387 // Pre-allocate the resource PHID.
388 $resource->setPHID($resource->generatePHID());
389
390 return $resource;
391 }
392
393 protected function newLease(DrydockBlueprint $blueprint) {
394 return DrydockLease::initializeNewLease()
395 ->setAuthorizingPHID($blueprint->getPHID());
396 }
397
398 protected function requireActiveLease(DrydockLease $lease) {
399 $lease_status = $lease->getStatus();
400
401 switch ($lease_status) {
402 case DrydockLeaseStatus::STATUS_PENDING:
403 case DrydockLeaseStatus::STATUS_ACQUIRED:
404 throw new PhabricatorWorkerYieldException(15);
405 case DrydockLeaseStatus::STATUS_ACTIVE:
406 return;
407 default:
408 throw new Exception(
409 pht(
410 'Lease ("%s") is in bad state ("%s"), expected "%s".',
411 $lease->getPHID(),
412 $lease_status,
413 DrydockLeaseStatus::STATUS_ACTIVE));
414 }
415 }
416
417
418 /**
419 * Does this implementation use concurrent resource limits?
420 *
421 * Implementations can override this method to opt into standard limit
422 * behavior, which provides a simple concurrent resource limit.
423 *
424 * @return bool True to use limits.
425 */
426 protected function shouldUseConcurrentResourceLimit() {
427 return false;
428 }
429
430
431 /**
432 * Get the effective concurrent resource limit for this blueprint.
433 *
434 * @param DrydockBlueprint $blueprint Blueprint to get the limit for.
435 * @return int|null Limit, or `null` for no limit.
436 */
437 protected function getConcurrentResourceLimit(DrydockBlueprint $blueprint) {
438 if ($this->shouldUseConcurrentResourceLimit()) {
439 $limit = $blueprint->getFieldValue('allocator.limit');
440 $limit = (int)$limit;
441 if ($limit > 0) {
442 return $limit;
443 } else {
444 return null;
445 }
446 }
447
448 return null;
449 }
450
451
452 protected function getConcurrentResourceLimitSlotLock(
453 DrydockBlueprint $blueprint) {
454
455 $limit = $this->getConcurrentResourceLimit($blueprint);
456 if ($limit === null) {
457 return;
458 }
459
460 $blueprint_phid = $blueprint->getPHID();
461
462 // TODO: This logic shouldn't do anything awful, but is a little silly. It
463 // would be nice to unify the "huge limit" and "small limit" cases
464 // eventually but it's a little tricky.
465
466 // If the limit is huge, just pick a random slot. This is just stopping
467 // us from exploding if someone types a billion zillion into the box.
468 if ($limit > 1024) {
469 $slot = mt_rand(0, $limit - 1);
470 return "allocator({$blueprint_phid}).limit({$slot})";
471 }
472
473 // For reasonable limits, actually check for an available slot.
474 $slots = range(0, $limit - 1);
475 shuffle($slots);
476
477 $lock_names = array();
478 foreach ($slots as $slot) {
479 $lock_names[] = "allocator({$blueprint_phid}).limit({$slot})";
480 }
481
482 $locks = DrydockSlotLock::loadHeldLocks($lock_names);
483 $locks = mpull($locks, null, 'getLockKey');
484
485 foreach ($lock_names as $lock_name) {
486 if (empty($locks[$lock_name])) {
487 return $lock_name;
488 }
489 }
490
491 // If we found no free slot, just return whatever we checked last (which
492 // is just a random slot). There's a small chance we'll get lucky and the
493 // lock will be free by the time we try to take it, but usually we'll just
494 // fail to grab the lock, throw an appropriate lock exception, and get back
495 // on the right path to retry later.
496
497 return $lock_name;
498 }
499
500
501
502 /**
503 * Apply standard limits on resource allocation rate.
504 *
505 * @param DrydockBlueprint $blueprint The blueprint requesting an allocation.
506 * @return bool True if further allocations should be limited.
507 */
508 protected function shouldLimitAllocatingPoolSize(
509 DrydockBlueprint $blueprint) {
510
511 // Limit on total number of active resources.
512 $total_limit = $this->getConcurrentResourceLimit($blueprint);
513 if ($total_limit === null) {
514 return false;
515 }
516
517 $resource = new DrydockResource();
518 $conn = $resource->establishConnection('r');
519
520 $counts = queryfx_all(
521 $conn,
522 'SELECT status, COUNT(*) N FROM %R
523 WHERE blueprintPHID = %s AND status != %s
524 GROUP BY status',
525 $resource,
526 $blueprint->getPHID(),
527 DrydockResourceStatus::STATUS_DESTROYED);
528 $counts = ipull($counts, 'N', 'status');
529
530 $n_alloc = idx($counts, DrydockResourceStatus::STATUS_PENDING, 0);
531 $n_active = idx($counts, DrydockResourceStatus::STATUS_ACTIVE, 0);
532 $n_broken = idx($counts, DrydockResourceStatus::STATUS_BROKEN, 0);
533 $n_released = idx($counts, DrydockResourceStatus::STATUS_RELEASED, 0);
534
535 // If we're at the limit on total active resources, limit additional
536 // allocations.
537 $n_total = ($n_alloc + $n_active + $n_broken + $n_released);
538 if ($n_total >= $total_limit) {
539 return true;
540 }
541
542 return false;
543 }
544
545}