1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-18 21:02:41 +01:00

Implement optimistic "slot locks" in Drydock

Summary:
See discussion in D10304. There's a lot of context there, but the general idea is:

  - Blueprints should manage locks in a granular way during the actual allocation/acquisition phase.
  - Optimistic "slot locks" might a pretty good primitive to make that easy to implement and reason about in most cases.

The way these locks work is that you just pick some name for the lock (like the PHID of a resource) and say that it needs to be acquired for the allocation/acquisition to work:

```
...
->needSlotLock("mylock(PHID-XYZQ-...)")
...
```

When you fire off the acquisition or allocation, it fails unless it could acquire the slot with that name. This is really simple (no explicit lock management) and a pretty good fit for most of the locking that blueprints and leases need to do.

If you need to do limit-based locks (e.g., maximum of 3 locks) you could acquire a lock like this:

```
mylock(whatever).slot(2)
```

Blueprints generally only contend with themselves, so it's normally OK for them to pick whatever strategy works best for them in naming locks.

This may not work as well if you have a huge number of slots (e.g., 100TB you want to give out in 1MB chunks), or other complex needs for locks (like you have to synchronize access to some external resource), but slot locks don't need to be the only mechanism that blueprints use. If they run into a problem that slot locks aren't a good fit for, they can use something else instead. For now, slot locks seem like a good fit for the problems we currently face and most of the problems I anticipate facing.

(The release workflows have other race issues which I'm not addressing here. They work fine if nothing races, but aren't race-safe.)

Test Plan:
To create a race where the same binding is allocated as a resource twice:

  - Add `sleep(10)` near the beginning of `allocateResource()`, after the free bindings are loaded but before resources are allocated.
  - (Comment out slot lock acquisition if you have this patch.)
  - Run `bin/drydock lease ...` in two windows, within 10 seconds of one another.

This will reliably double-allocate the binding because both blueprints see a view of the world where the binding is free.

To verify the lock works, un-comment it (or apply this patch) and run the same test again. Now, the lock fails in one process and only one resource is allocated.

Reviewers: hach-que, chad

Reviewed By: hach-que, chad

Differential Revision: https://secure.phabricator.com/D14118
This commit is contained in:
epriestley 2015-09-21 04:45:25 -07:00
parent 6e03419593
commit 3ac99006bf
9 changed files with 185 additions and 19 deletions

View file

@ -0,0 +1,8 @@
CREATE TABLE {$NAMESPACE}_drydock.drydock_slotlock (
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
ownerPHID VARBINARY(64) NOT NULL,
lockIndex BINARY(12) NOT NULL,
lockKey LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
UNIQUE KEY `key_lock` (lockIndex),
KEY `key_owner` (ownerPHID)
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};

View file

@ -863,6 +863,7 @@ phutil_register_library_map(array(
'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php', 'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php',
'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php', 'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php',
'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php', 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php',
'DrydockSlotLock' => 'applications/drydock/storage/DrydockSlotLock.php',
'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php',
'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php',
'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php', 'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php',
@ -4585,6 +4586,7 @@ phutil_register_library_map(array(
'DrydockResourceViewController' => 'DrydockResourceController', 'DrydockResourceViewController' => 'DrydockResourceController',
'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface',
'DrydockSSHCommandInterface' => 'DrydockCommandInterface', 'DrydockSSHCommandInterface' => 'DrydockCommandInterface',
'DrydockSlotLock' => 'DrydockDAO',
'DrydockWebrootInterface' => 'DrydockInterface', 'DrydockWebrootInterface' => 'DrydockInterface',
'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation',
'FeedConduitAPIMethod' => 'ConduitAPIMethod', 'FeedConduitAPIMethod' => 'ConduitAPIMethod',

View file

@ -67,14 +67,13 @@ final class DrydockAlmanacServiceHostBlueprintImplementation
$device = $binding->getDevice(); $device = $binding->getDevice();
$device_name = $device->getName(); $device_name = $device->getName();
$binding_phid = $binding->getPHID();
$resource = $this->newResourceTemplate($blueprint, $device_name) $resource = $this->newResourceTemplate($blueprint, $device_name)
->setActivateWhenAllocated(true) ->setActivateWhenAllocated(true)
->setAttribute('almanacServicePHID', $binding->getServicePHID()) ->setAttribute('almanacServicePHID', $binding->getServicePHID())
->setAttribute('almanacBindingPHID', $binding->getPHID()); ->setAttribute('almanacBindingPHID', $binding_phid)
->needSlotLock("almanac.host.binding({$binding_phid})");
// TODO: This algorithm can race, and the "free" binding may not be
// free by the time we acquire it. Do slot-locking here if that works
// out, or some other kind of locking if it does not.
try { try {
return $resource->allocateResource(DrydockResourceStatus::STATUS_OPEN); return $resource->allocateResource(DrydockResourceStatus::STATUS_OPEN);
@ -93,8 +92,11 @@ final class DrydockAlmanacServiceHostBlueprintImplementation
DrydockResource $resource, DrydockResource $resource,
DrydockLease $lease) { DrydockLease $lease) {
// TODO: We'll currently lease each resource an unlimited number of times, // TODO: The current rule is one lease per resource, and there's no way to
// but should stop doing that. // make that cheaper here than by just trying to acquire the lease below,
// so don't do any special checks for now. When we eventually permit
// multiple leases per host, we'll need to load leases anyway, so we can
// reject fully leased hosts cheaply here.
return true; return true;
} }
@ -104,11 +106,11 @@ final class DrydockAlmanacServiceHostBlueprintImplementation
DrydockResource $resource, DrydockResource $resource,
DrydockLease $lease) { DrydockLease $lease) {
// TODO: Once we have limit rules, we should perform slot locking (or other $resource_phid = $resource->getPHID();
// kinds of locking) here.
$lease $lease
->setActivateWhenAcquired(true) ->setActivateWhenAcquired(true)
->needSlotLock("almanac.host.lease({$resource_phid})")
->acquireOnResource($resource); ->acquireOnResource($resource);
} }

View file

@ -106,8 +106,12 @@ abstract class DrydockBlueprintImplementation extends Phobject {
DrydockLease $lease); DrydockLease $lease);
final public function releaseLease( final public function releaseLease(
DrydockBlueprint $blueprint,
DrydockResource $resource, DrydockResource $resource,
DrydockLease $lease) { DrydockLease $lease) {
// TODO: This is all broken nonsense.
$scope = $this->pushActiveScope(null, $lease); $scope = $this->pushActiveScope(null, $lease);
$released = false; $released = false;
@ -117,6 +121,7 @@ abstract class DrydockBlueprintImplementation extends Phobject {
$lease->reload(); $lease->reload();
if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) { if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) {
$lease->release();
$lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED); $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
$lease->save(); $lease->save();
$released = true; $released = true;
@ -293,6 +298,7 @@ abstract class DrydockBlueprintImplementation extends Phobject {
$resource = id(new DrydockResource()) $resource = id(new DrydockResource())
->setBlueprintPHID($blueprint->getPHID()) ->setBlueprintPHID($blueprint->getPHID())
->attachBlueprint($blueprint)
->setType($this->getType()) ->setType($this->getType())
->setStatus(DrydockResourceStatus::STATUS_PENDING) ->setStatus(DrydockResourceStatus::STATUS_PENDING)
->setName($name); ->setName($name);

View file

@ -163,6 +163,17 @@ final class DrydockBlueprint extends DrydockDAO
} }
/**
* @task lease
*/
public function releaseLease(
DrydockResource $resource,
DrydockLease $lease) {
$this->getImplementation()->releaseLease($this, $resource, $lease);
return $this;
}
/* -( PhabricatorApplicationTransactionInterface )------------------------- */ /* -( PhabricatorApplicationTransactionInterface )------------------------- */

View file

@ -15,6 +15,7 @@ final class DrydockLease extends DrydockDAO
private $releaseOnDestruction; private $releaseOnDestruction;
private $isAcquired = false; private $isAcquired = false;
private $activateWhenAcquired = false; private $activateWhenAcquired = false;
private $slotLocks = array();
/** /**
* Flag this lease to be released when its destructor is called. This is * Flag this lease to be released when its destructor is called. This is
@ -128,6 +129,8 @@ final class DrydockLease extends DrydockDAO
$this->setStatus(DrydockLeaseStatus::STATUS_RELEASED); $this->setStatus(DrydockLeaseStatus::STATUS_RELEASED);
$this->save(); $this->save();
DrydockSlotLock::releaseLocks($this->getPHID());
$this->resource = null; $this->resource = null;
return $this; return $this;
@ -206,6 +209,11 @@ final class DrydockLease extends DrydockDAO
return $this; return $this;
} }
public function needSlotLock($key) {
$this->slotLocks[] = $key;
return $this;
}
public function acquireOnResource(DrydockResource $resource) { public function acquireOnResource(DrydockResource $resource) {
$expect_status = DrydockLeaseStatus::STATUS_PENDING; $expect_status = DrydockLeaseStatus::STATUS_PENDING;
$actual_status = $this->getStatus(); $actual_status = $this->getStatus();
@ -234,11 +242,18 @@ final class DrydockLease extends DrydockDAO
} }
} }
$this->openTransaction();
$this $this
->setResourceID($resource->getID()) ->setResourceID($resource->getID())
->setStatus($new_status) ->setStatus($new_status)
->save(); ->save();
DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks);
$this->slotLocks = array();
$this->saveTransaction();
$this->isAcquired = true; $this->isAcquired = true;
return $this; return $this;

View file

@ -17,6 +17,7 @@ final class DrydockResource extends DrydockDAO
private $blueprint = self::ATTACHABLE; private $blueprint = self::ATTACHABLE;
private $isAllocated = false; private $isAllocated = false;
private $activateWhenAllocated = false; private $activateWhenAllocated = false;
private $slotLocks = array();
protected function getConfiguration() { protected function getConfiguration() {
return array( return array(
@ -80,6 +81,11 @@ final class DrydockResource extends DrydockDAO
return $this; return $this;
} }
public function needSlotLock($key) {
$this->slotLocks[] = $key;
return $this;
}
public function allocateResource($status) { public function allocateResource($status) {
if ($this->getID()) { if ($this->getID()) {
throw new Exception( throw new Exception(
@ -105,11 +111,18 @@ final class DrydockResource extends DrydockDAO
$new_status = DrydockResourceStatus::STATUS_PENDING; $new_status = DrydockResourceStatus::STATUS_PENDING;
} }
$this->openTransaction();
$this $this
->setStatus($new_status) ->setStatus($new_status)
->save(); ->save();
$this->didAllocate = true; DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks);
$this->slotLocks = array();
$this->saveTransaction();
$this->isAllocated = true;
return $this; return $this;
} }
@ -151,6 +164,9 @@ final class DrydockResource extends DrydockDAO
$this->setStatus(DrydockResourceStatus::STATUS_CLOSED); $this->setStatus(DrydockResourceStatus::STATUS_CLOSED);
$this->save(); $this->save();
DrydockSlotLock::releaseLocks($this->getPHID());
$this->saveTransaction(); $this->saveTransaction();
} }

View file

@ -0,0 +1,107 @@
<?php
/**
* Simple optimistic locks for Drydock resources and leases.
*
* Most blueprints only need very simple locks: for example, a host blueprint
* might not want to create multiple resources representing the same physical
* machine. These optimistic "slot locks" provide a flexible way to do this
* sort of simple locking.
*
* @task lock Acquiring and Releasing Locks
*/
final class DrydockSlotLock extends DrydockDAO {
protected $ownerPHID;
protected $lockIndex;
protected $lockKey;
protected function getConfiguration() {
return array(
self::CONFIG_TIMESTAMPS => false,
self::CONFIG_COLUMN_SCHEMA => array(
'lockIndex' => 'bytes12',
'lockKey' => 'text',
),
self::CONFIG_KEY_SCHEMA => array(
'key_lock' => array(
'columns' => array('lockIndex'),
'unique' => true,
),
'key_owner' => array(
'columns' => array('ownerPHID'),
),
),
) + parent::getConfiguration();
}
public static function loadLocks($owner_phid) {
return id(new DrydockSlotLock())->loadAllWhere(
'ownerPHID = %s',
$owner_phid);
}
/* -( Acquiring and Releasing Locks )-------------------------------------- */
/**
* Acquire a set of slot locks.
*
* This method either acquires all the locks or throws an exception (usually
* because one or more locks are held).
*
* @param phid Lock owner PHID.
* @param list<string> List of locks to acquire.
* @return void
* @task locks
*/
public static function acquireLocks($owner_phid, array $locks) {
if (!$locks) {
return;
}
$table = new DrydockSlotLock();
$conn_w = $table->establishConnection('w');
$sql = array();
foreach ($locks as $lock) {
$sql[] = qsprintf(
$conn_w,
'(%s, %s, %s)',
$owner_phid,
PhabricatorHash::digestForIndex($lock),
$lock);
}
// TODO: These exceptions are pretty tricky to read. It would be good to
// figure out which locks could not be acquired and try to improve the
// exception to make debugging easier.
queryfx(
$conn_w,
'INSERT INTO %T (ownerPHID, lockIndex, lockKey) VALUES %Q',
$table->getTableName(),
implode(', ', $sql));
}
/**
* Release all locks held by an owner.
*
* @param phid Lock owner PHID.
* @return void
* @task locks
*/
public static function releaseLocks($owner_phid) {
$table = new DrydockSlotLock();
$conn_w = $table->establishConnection('w');
queryfx(
$conn_w,
'DELETE FROM %T WHERE ownerPHID = %s',
$table->getTableName(),
$owner_phid);
}
}

View file

@ -350,7 +350,7 @@ final class DrydockAllocatorWorker extends PhabricatorWorker {
DrydockBlueprint $blueprint, DrydockBlueprint $blueprint,
DrydockLease $lease) { DrydockLease $lease) {
$resource = $blueprint->allocateResource($lease); $resource = $blueprint->allocateResource($lease);
$this->validateAllocatedResource($resource); $this->validateAllocatedResource($blueprint, $resource, $lease);
return $resource; return $resource;
} }
@ -369,7 +369,6 @@ final class DrydockAllocatorWorker extends PhabricatorWorker {
DrydockBlueprint $blueprint, DrydockBlueprint $blueprint,
$resource, $resource,
DrydockLease $lease) { DrydockLease $lease) {
$blueprint = $this->getBlueprintClass();
if (!($resource instanceof DrydockResource)) { if (!($resource instanceof DrydockResource)) {
throw new Exception( throw new Exception(