From 789df89c84b5a453770f51f296f3b0e6df677233 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 23 Sep 2015 07:42:08 -0700 Subject: [PATCH] Add a command queue to Drydock to manage lease/resource release Summary: Ref T9252. Broadly, Drydock currently races on releasing objects from the "active" state. To reproduce this: - Scatter some sleep()s pretty much anywhere in the release code. - Release several times from web UI or CLI in quick succession. Resources or leases will execute some release code twice or otherwise do inconsistent things. (I didn't chase down a detailed reproduction scenario for this since inspection of the code makes it clear that there are no meaningful locks or mechanisms preventing this.) Instead, add a Harbormaster-style command queue to resources and leases. When something wants to do a release, it adds a command to the queue and schedules a worker. The workers acquire a lock, then try to consume commands from the queue. This guarantees that only one process is responsible for writes to active resource/leases. This is the last major step to giving resources and leases a single writer during all states: - Resource, Unsaved: AllocatorWorker - Resource, Pending: ResourceWorker (Possible rename to "Allocated?") - Resource, Open: This diff, ResourceUpdateWorker. (Likely rename to "Active"). - Resource, Closed/Broken: Future destruction worker. (Likely rename to "Released" / "Broken"; maybe remove "Broken"). - Resource, Destroyed: No writes. - Lease, Unsaved: Whatever wants the lease. - Lease, Pending: AllocatorWorker - Lease, Acquired: LeaseWorker - Lease, Active: This diff, LeaseUpdateWorker. - Lease, Released/Broken: Future destruction worker (Maybe remove "Broken"?) - Lease, Expired: No writes. (Likely rename to "Destroyed"). In most phases, we can already guarantee that there is a single writer without doing any extra work. This is more complicated in the "Active" case because the release buttons on the web UI, the release tools on the CLI, the lease requestor itself, the garbage collector, and any other release process cleaning up related objects may try to effect a release. All of these could race one another (and, in many cases, race other processes from other phases because all of these get to act immediately) as this code is currently written. Using a queue here lets us make sure there's only a single writer in this phase. One thing which is notable is that whatever acquires a lease **can not write to it**! It is never the writer once it queues the lease for activation. It can not write to any resources, either. And, likewise, Blueprints can not write to resources while acquiring or releasing leases. We may need to provide a mechinism so that blueprints and/or resource/lease holders get to attach some storage to resources/leases for bookkeeping. For example, a blueprint might need to keep some kind of cache on a resource to help it manage state. But I think we can cross that bridge when we come to it, and nothing else would need to write to this storage so it's technically straightforward to introduce such a mechanism if we need one. Test Plan: - Viewed buttons in web UI, checked enabled/disabled states. - Clicked the buttons. - Saw commands show up in the command queue. - Saw some daemon stuff get scheduled. - Ran CLI tools, saw commands get consumed and resources/leases release. Reviewers: hach-que, chad Reviewed By: chad Maniphest Tasks: T9252 Differential Revision: https://secure.phabricator.com/D14143 --- .../20150922.drydock.commands.1.sql | 10 ++ src/__phutil_library_map__.php | 27 ++++-- .../PhabricatorDrydockApplication.php | 6 +- .../drydock/controller/DrydockController.php | 49 ++++++++++ .../DrydockLeaseReleaseController.php | 59 ++++++------ .../controller/DrydockLeaseViewController.php | 16 +++- .../DrydockResourceCloseController.php | 49 ---------- .../DrydockResourceReleaseController.php | 56 +++++++++++ .../DrydockResourceViewController.php | 21 +++-- .../DrydockManagementCloseWorkflow.php | 49 ---------- .../DrydockManagementReleaseLeaseWorkflow.php | 70 ++++++++++++++ ...ydockManagementReleaseResourceWorkflow.php | 71 ++++++++++++++ .../DrydockManagementReleaseWorkflow.php | 52 ----------- .../DrydockManagementUpdateLeaseWorkflow.php | 57 ++++++++++++ ...rydockManagementUpdateResourceWorkflow.php | 58 ++++++++++++ .../drydock/query/DrydockCommandQuery.php | 82 +++++++++++++++++ .../drydock/query/DrydockLeaseQuery.php | 1 + .../drydock/storage/DrydockCommand.php | 69 ++++++++++++++ .../drydock/storage/DrydockLease.php | 85 +++++++++++++---- .../drydock/storage/DrydockResource.php | 68 +++++++------- .../worker/DrydockLeaseUpdateWorker.php | 60 ++++++++++++ .../worker/DrydockResourceUpdateWorker.php | 92 +++++++++++++++++++ .../drydock/worker/DrydockWorker.php | 14 +++ .../artifact/HarbormasterHostArtifact.php | 15 +-- 24 files changed, 883 insertions(+), 253 deletions(-) create mode 100644 resources/sql/autopatches/20150922.drydock.commands.1.sql delete mode 100644 src/applications/drydock/controller/DrydockResourceCloseController.php create mode 100644 src/applications/drydock/controller/DrydockResourceReleaseController.php delete mode 100644 src/applications/drydock/management/DrydockManagementCloseWorkflow.php create mode 100644 src/applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php create mode 100644 src/applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php delete mode 100644 src/applications/drydock/management/DrydockManagementReleaseWorkflow.php create mode 100644 src/applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php create mode 100644 src/applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php create mode 100644 src/applications/drydock/query/DrydockCommandQuery.php create mode 100644 src/applications/drydock/storage/DrydockCommand.php create mode 100644 src/applications/drydock/worker/DrydockLeaseUpdateWorker.php create mode 100644 src/applications/drydock/worker/DrydockResourceUpdateWorker.php diff --git a/resources/sql/autopatches/20150922.drydock.commands.1.sql b/resources/sql/autopatches/20150922.drydock.commands.1.sql new file mode 100644 index 0000000000..173fe861ac --- /dev/null +++ b/resources/sql/autopatches/20150922.drydock.commands.1.sql @@ -0,0 +1,10 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_command ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + authorPHID VARBINARY(64) NOT NULL, + targetPHID VARBINARY(64) NOT NULL, + command VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + isConsumed BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_target` (targetPHID, isConsumed) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 747e085023..229f074c58 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -817,7 +817,9 @@ phutil_register_library_map(array( 'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php', 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', + 'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', + 'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php', 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', 'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php', 'DrydockController' => 'applications/drydock/controller/DrydockController.php', @@ -837,6 +839,7 @@ phutil_register_library_map(array( 'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php', 'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php', 'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php', + 'DrydockLeaseUpdateWorker' => 'applications/drydock/worker/DrydockLeaseUpdateWorker.php', 'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php', 'DrydockLeaseWorker' => 'applications/drydock/worker/DrydockLeaseWorker.php', 'DrydockLog' => 'applications/drydock/storage/DrydockLog.php', @@ -845,22 +848,25 @@ phutil_register_library_map(array( 'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php', 'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php', 'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php', - 'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php', 'DrydockManagementCommandWorkflow' => 'applications/drydock/management/DrydockManagementCommandWorkflow.php', 'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php', - 'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php', + 'DrydockManagementReleaseLeaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php', + 'DrydockManagementReleaseResourceWorkflow' => 'applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php', + 'DrydockManagementUpdateLeaseWorkflow' => 'applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php', + 'DrydockManagementUpdateResourceWorkflow' => 'applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', - 'DrydockResourceCloseController' => 'applications/drydock/controller/DrydockResourceCloseController.php', 'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php', 'DrydockResourceDatasource' => 'applications/drydock/typeahead/DrydockResourceDatasource.php', 'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php', 'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php', 'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php', 'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php', + 'DrydockResourceReleaseController' => 'applications/drydock/controller/DrydockResourceReleaseController.php', 'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', + 'DrydockResourceUpdateWorker' => 'applications/drydock/worker/DrydockResourceUpdateWorker.php', 'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php', 'DrydockResourceWorker' => 'applications/drydock/worker/DrydockResourceWorker.php', 'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php', @@ -4535,7 +4541,12 @@ phutil_register_library_map(array( 'DrydockBlueprintTransaction' => 'PhabricatorApplicationTransaction', 'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DrydockBlueprintViewController' => 'DrydockBlueprintController', + 'DrydockCommand' => array( + 'DrydockDAO', + 'PhabricatorPolicyInterface', + ), 'DrydockCommandInterface' => 'DrydockInterface', + 'DrydockCommandQuery' => 'DrydockQuery', 'DrydockConsoleController' => 'DrydockController', 'DrydockConstants' => 'Phobject', 'DrydockController' => 'PhabricatorController', @@ -4558,6 +4569,7 @@ phutil_register_library_map(array( 'DrydockLeaseReleaseController' => 'DrydockLeaseController', 'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockLeaseStatus' => 'DrydockConstants', + 'DrydockLeaseUpdateWorker' => 'DrydockWorker', 'DrydockLeaseViewController' => 'DrydockLeaseController', 'DrydockLeaseWorker' => 'DrydockWorker', 'DrydockLog' => array( @@ -4569,25 +4581,28 @@ phutil_register_library_map(array( 'DrydockLogListView' => 'AphrontView', 'DrydockLogQuery' => 'DrydockQuery', 'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', - 'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementReleaseLeaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementReleaseResourceWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), - 'DrydockResourceCloseController' => 'DrydockResourceController', 'DrydockResourceController' => 'DrydockController', 'DrydockResourceDatasource' => 'PhabricatorTypeaheadDatasource', 'DrydockResourceListController' => 'DrydockResourceController', 'DrydockResourceListView' => 'AphrontView', 'DrydockResourcePHIDType' => 'PhabricatorPHIDType', 'DrydockResourceQuery' => 'DrydockQuery', + 'DrydockResourceReleaseController' => 'DrydockResourceController', 'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockResourceStatus' => 'DrydockConstants', + 'DrydockResourceUpdateWorker' => 'DrydockWorker', 'DrydockResourceViewController' => 'DrydockResourceController', 'DrydockResourceWorker' => 'DrydockWorker', 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', diff --git a/src/applications/drydock/application/PhabricatorDrydockApplication.php b/src/applications/drydock/application/PhabricatorDrydockApplication.php index 7919f1c9cf..d3a543a4f0 100644 --- a/src/applications/drydock/application/PhabricatorDrydockApplication.php +++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php @@ -55,8 +55,10 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication { ), 'resource/' => array( '(?:query/(?P[^/]+)/)?' => 'DrydockResourceListController', - '(?P[1-9]\d*)/' => 'DrydockResourceViewController', - '(?P[1-9]\d*)/close/' => 'DrydockResourceCloseController', + '(?P[1-9]\d*)/' => array( + '' => 'DrydockResourceViewController', + 'release/' => 'DrydockResourceReleaseController', + ), ), 'lease/' => array( '(?:query/(?P[^/]+)/)?' => 'DrydockLeaseListController', diff --git a/src/applications/drydock/controller/DrydockController.php b/src/applications/drydock/controller/DrydockController.php index 3e3d83cc1d..e0130bdf56 100644 --- a/src/applications/drydock/controller/DrydockController.php +++ b/src/applications/drydock/controller/DrydockController.php @@ -36,4 +36,53 @@ abstract class DrydockController extends PhabricatorController { ->addRawContent($table); } + protected function buildCommandsTab($target_phid) { + $viewer = $this->getViewer(); + + $commands = id(new DrydockCommandQuery()) + ->setViewer($viewer) + ->withTargetPHIDs(array($target_phid)) + ->execute(); + + $consumed_yes = id(new PHUIIconView()) + ->setIconFont('fa-check green'); + $consumed_no = id(new PHUIIconView()) + ->setIconFont('fa-clock-o grey'); + + $rows = array(); + foreach ($commands as $command) { + $rows[] = array( + $command->getID(), + $viewer->renderHandle($command->getAuthorPHID()), + $command->getCommand(), + ($command->getIsConsumed() + ? $consumed_yes + : $consumed_no), + phabricator_datetime($command->getDateCreated(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('No commands issued.')) + ->setHeaders( + array( + pht('ID'), + pht('From'), + pht('Command'), + null, + pht('Date'), + )) + ->setColumnClasses( + array( + null, + null, + 'wide', + null, + null, + )); + + return id(new PHUIPropertyListView()) + ->addRawContent($table); + } + } diff --git a/src/applications/drydock/controller/DrydockLeaseReleaseController.php b/src/applications/drydock/controller/DrydockLeaseReleaseController.php index 4bac0330ba..52be31c97a 100644 --- a/src/applications/drydock/controller/DrydockLeaseReleaseController.php +++ b/src/applications/drydock/controller/DrydockLeaseReleaseController.php @@ -9,6 +9,11 @@ final class DrydockLeaseReleaseController extends DrydockLeaseController { $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->executeOne(); if (!$lease) { return new Aphront404Response(); @@ -17,43 +22,35 @@ final class DrydockLeaseReleaseController extends DrydockLeaseController { $lease_uri = '/lease/'.$lease->getID().'/'; $lease_uri = $this->getApplicationURI($lease_uri); - if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Lease Not Active')) - ->appendChild( - phutil_tag( - 'p', - array(), - pht('You can only release "active" leases.'))) + if (!$lease->canRelease()) { + return $this->newDialog() + ->setTitle(pht('Lease Not Releasable')) + ->appendParagraph( + pht( + 'Leases can not be released after they are destroyed.')) ->addCancelButton($lease_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); } - if (!$request->isDialogFormPost()) { - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Really release lease?')) - ->appendChild( - phutil_tag( - 'p', - array(), - pht( - 'Releasing a lease may cause trouble for the lease holder and '. - 'trigger cleanup of the underlying resource. It can not be '. - 'undone. Continue?'))) - ->addSubmitButton(pht('Release Lease')) - ->addCancelButton($lease_uri); + if ($request->isFormPost()) { + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); - return id(new AphrontDialogResponse())->setDialog($dialog); + $lease->scheduleUpdate(); + + return id(new AphrontRedirectResponse())->setURI($lease_uri); } - $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - $blueprint->releaseLease($resource, $lease); - - return id(new AphrontReloadResponse())->setURI($lease_uri); + return $this->newDialog() + ->setTitle(pht('Release Lease?')) + ->appendParagraph( + pht( + 'Forcefully releasing a lease may interfere with the operation '. + 'of the lease holder and trigger destruction of the underlying '. + 'resource. It can not be undone.')) + ->addSubmitButton(pht('Release Lease')) + ->addCancelButton($lease_uri); } } diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php index aed1f6416f..eaa0683c87 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -43,11 +43,13 @@ final class DrydockLeaseViewController extends DrydockLeaseController { $crumbs->addTextCrumb($title, $lease_uri); $locks = $this->buildLocksTab($lease->getPHID()); + $commands = $this->buildCommandsTab($lease->getPHID()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties, pht('Properties')) - ->addPropertyList($locks, pht('Slot Locks')); + ->addPropertyList($locks, pht('Slot Locks')) + ->addPropertyList($commands, pht('Commands')); $log_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Lease Logs')) @@ -66,14 +68,20 @@ final class DrydockLeaseViewController extends DrydockLeaseController { } private function buildActionListView(DrydockLease $lease) { + $viewer = $this->getViewer(); + $view = id(new PhabricatorActionListView()) - ->setUser($this->getRequest()->getUser()) + ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($lease); $id = $lease->getID(); - $can_release = ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE); + $can_release = $lease->canRelease(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $lease, + PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) @@ -81,7 +89,7 @@ final class DrydockLeaseViewController extends DrydockLeaseController { ->setIcon('fa-times') ->setHref($this->getApplicationURI("/lease/{$id}/release/")) ->setWorkflow(true) - ->setDisabled(!$can_release)); + ->setDisabled(!$can_release || !$can_edit)); return $view; } diff --git a/src/applications/drydock/controller/DrydockResourceCloseController.php b/src/applications/drydock/controller/DrydockResourceCloseController.php deleted file mode 100644 index 915bf89452..0000000000 --- a/src/applications/drydock/controller/DrydockResourceCloseController.php +++ /dev/null @@ -1,49 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - $resource = id(new DrydockResourceQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); - if (!$resource) { - return new Aphront404Response(); - } - - $resource_uri = '/resource/'.$resource->getID().'/'; - $resource_uri = $this->getApplicationURI($resource_uri); - - if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Resource Not Open')) - ->appendChild(phutil_tag('p', array(), pht( - 'You can only close "open" resources.'))) - ->addCancelButton($resource_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); - } - - if ($request->isFormPost()) { - $resource->closeResource(); - return id(new AphrontReloadResponse())->setURI($resource_uri); - } - - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Really close resource?')) - ->appendChild( - pht( - 'Closing a resource releases all leases and destroys the '. - 'resource. It can not be undone. Continue?')) - ->addSubmitButton(pht('Close Resource')) - ->addCancelButton($resource_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); - } - -} diff --git a/src/applications/drydock/controller/DrydockResourceReleaseController.php b/src/applications/drydock/controller/DrydockResourceReleaseController.php new file mode 100644 index 0000000000..4e508597ef --- /dev/null +++ b/src/applications/drydock/controller/DrydockResourceReleaseController.php @@ -0,0 +1,56 @@ +getViewer(); + $id = $request->getURIData('id'); + + $resource = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$resource) { + return new Aphront404Response(); + } + + $resource_uri = '/resource/'.$resource->getID().'/'; + $resource_uri = $this->getApplicationURI($resource_uri); + + if (!$resource->canRelease()) { + return $this->newDialog() + ->setTitle(pht('Resource Not Releasable')) + ->appendParagraph( + pht( + 'Resources can not be released after they are destroyed.')) + ->addCancelButton($resource_uri); + } + + if ($request->isFormPost()) { + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($resource->getPHID()) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $resource->scheduleUpdate(); + + return id(new AphrontRedirectResponse())->setURI($resource_uri); + } + + + return $this->newDialog() + ->setTitle(pht('Really release resource?')) + ->appendChild( + pht( + 'Releasing a resource releases all leases and destroys the '. + 'resource. It can not be undone.')) + ->addSubmitButton(pht('Release Resource')) + ->addCancelButton($resource_uri); + } + +} diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index d4a37af33c..40009e34ce 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -55,11 +55,13 @@ final class DrydockResourceViewController extends DrydockResourceController { $crumbs->addTextCrumb(pht('Resource %d', $resource->getID())); $locks = $this->buildLocksTab($resource->getPHID()); + $commands = $this->buildCommandsTab($resource->getPHID()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties, pht('Properties')) - ->addPropertyList($locks, pht('Slot Locks')); + ->addPropertyList($locks, pht('Slot Locks')) + ->addPropertyList($commands, pht('Commands')); $lease_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Leases')) @@ -83,22 +85,29 @@ final class DrydockResourceViewController extends DrydockResourceController { } private function buildActionListView(DrydockResource $resource) { + $viewer = $this->getViewer(); + $view = id(new PhabricatorActionListView()) - ->setUser($this->getRequest()->getUser()) + ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($resource); - $can_close = ($resource->getStatus() == DrydockResourceStatus::STATUS_OPEN); - $uri = '/resource/'.$resource->getID().'/close/'; + $can_release = $resource->canRelease(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $resource, + PhabricatorPolicyCapability::CAN_EDIT); + + $uri = '/resource/'.$resource->getID().'/release/'; $uri = $this->getApplicationURI($uri); $view->addAction( id(new PhabricatorActionView()) ->setHref($uri) - ->setName(pht('Close Resource')) + ->setName(pht('Release Resource')) ->setIcon('fa-times') ->setWorkflow(true) - ->setDisabled(!$can_close)); + ->setDisabled(!$can_release || !$can_edit)); return $view; } diff --git a/src/applications/drydock/management/DrydockManagementCloseWorkflow.php b/src/applications/drydock/management/DrydockManagementCloseWorkflow.php deleted file mode 100644 index f20b0e692b..0000000000 --- a/src/applications/drydock/management/DrydockManagementCloseWorkflow.php +++ /dev/null @@ -1,49 +0,0 @@ -setName('close') - ->setSynopsis(pht('Close a resource.')) - ->setArguments( - array( - array( - 'name' => 'ids', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $ids = $args->getArg('ids'); - if (!$ids) { - throw new PhutilArgumentUsageException( - pht('Specify one or more resource IDs to close.')); - } - - $viewer = $this->getViewer(); - - $resources = id(new DrydockResourceQuery()) - ->setViewer($viewer) - ->withIDs($ids) - ->execute(); - - foreach ($ids as $id) { - $resource = idx($resources, $id); - if (!$resource) { - $console->writeErr("%s\n", pht('Resource %d does not exist!', $id)); - } else if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { - $console->writeErr("%s\n", pht("Resource %d is not 'open'!", $id)); - } else { - $resource->closeResource(); - $console->writeErr("%s\n", pht('Closed resource %d.', $id)); - } - } - - } - -} diff --git a/src/applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php new file mode 100644 index 0000000000..20af18ec21 --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php @@ -0,0 +1,70 @@ +setName('release-lease') + ->setSynopsis(pht('Release a lease.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'repeat' => true, + 'help' => pht('Lease ID to release.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $ids = $args->getArg('id'); + if (!$ids) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more lease IDs to release with "%s".', + '--id')); + } + + $viewer = $this->getViewer(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + PhabricatorWorker::setRunAllTasksInProcess(true); + foreach ($ids as $id) { + $lease = idx($leases, $id); + if (!$lease) { + echo tsprintf( + "%s\n", + pht('Lease "%s" does not exist.', $id)); + continue; + } + + if (!$lease->canRelease()) { + echo tsprintf( + "%s\n", + pht('Lease "%s" is not releasable.', $id)); + continue; + } + + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); + + echo tsprintf( + "%s\n", + pht('Scheduled release of lease "%s".', $id)); + } + + } + +} diff --git a/src/applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php b/src/applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php new file mode 100644 index 0000000000..01060a5325 --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php @@ -0,0 +1,71 @@ +setName('release-resource') + ->setSynopsis(pht('Release a resource.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'repeat' => true, + 'help' => pht('Resource ID to release.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $ids = $args->getArg('id'); + if (!$ids) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more resource IDs to release with "%s".', + '--id')); + } + + $viewer = $this->getViewer(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $resources = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + PhabricatorWorker::setRunAllTasksInProcess(true); + foreach ($ids as $id) { + $resource = idx($resources, $id); + + if (!$resource) { + echo tsprintf( + "%s\n", + pht('Resource "%s" does not exist.', $id)); + continue; + } + + if (!$resource->canRelease()) { + echo tsprintf( + "%s\n", + pht('Resource "%s" is not releasable.', $id)); + continue; + } + + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($resource->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $resource->scheduleUpdate(); + + echo tsprintf( + "%s\n", + pht('Scheduled release of resource "%s".', $id)); + } + + } + +} diff --git a/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php b/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php deleted file mode 100644 index 616a5deb7b..0000000000 --- a/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php +++ /dev/null @@ -1,52 +0,0 @@ -setName('release') - ->setSynopsis(pht('Release a lease.')) - ->setArguments( - array( - array( - 'name' => 'ids', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $ids = $args->getArg('ids'); - if (!$ids) { - throw new PhutilArgumentUsageException( - pht('Specify one or more lease IDs to release.')); - } - - $viewer = $this->getViewer(); - - $leases = id(new DrydockLeaseQuery()) - ->setViewer($viewer) - ->withIDs($ids) - ->execute(); - - foreach ($ids as $id) { - $lease = idx($leases, $id); - if (!$lease) { - $console->writeErr("%s\n", pht('Lease %d does not exist!', $id)); - } else if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { - $console->writeErr("%s\n", pht("Lease %d is not 'active'!", $id)); - } else { - $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - $blueprint->releaseLease($resource, $lease); - - $console->writeErr("%s\n", pht('Released lease %d.', $id)); - } - } - - } - -} diff --git a/src/applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php new file mode 100644 index 0000000000..aa74bb0748 --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php @@ -0,0 +1,57 @@ +setName('update-lease') + ->setSynopsis(pht('Update a lease.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'repeat' => true, + 'help' => pht('Lease ID to update.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $ids = $args->getArg('id'); + if (!$ids) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more lease IDs to update with "%s".', + '--id')); + } + + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + PhabricatorWorker::setRunAllTasksInProcess(true); + + foreach ($ids as $id) { + $lease = idx($leases, $id); + + if (!$lease) { + echo tsprintf( + "%s\n", + pht('Lease "%s" does not exist.', $id)); + continue; + } + + echo tsprintf( + "%s\n", + pht('Updating lease "%s".', $id)); + + $lease->scheduleUpdate(); + } + } + +} diff --git a/src/applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php b/src/applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php new file mode 100644 index 0000000000..79928fda8d --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php @@ -0,0 +1,58 @@ +setName('update-resource') + ->setSynopsis(pht('Update a resource.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'repeat' => true, + 'help' => pht('Resource ID to update.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $ids = $args->getArg('id'); + if (!$ids) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more resource IDs to update with "%s".', + '--id')); + } + + $resources = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + PhabricatorWorker::setRunAllTasksInProcess(true); + + foreach ($ids as $id) { + $resource = idx($resources, $id); + + if (!$resource) { + echo tsprintf( + "%s\n", + pht('Resource "%s" does not exist.', $id)); + continue; + } + + echo tsprintf( + "%s\n", + pht('Updating resource "%s".', $id)); + + $resource->scheduleUpdate(); + } + + } + +} diff --git a/src/applications/drydock/query/DrydockCommandQuery.php b/src/applications/drydock/query/DrydockCommandQuery.php new file mode 100644 index 0000000000..0d71288a85 --- /dev/null +++ b/src/applications/drydock/query/DrydockCommandQuery.php @@ -0,0 +1,82 @@ +ids = $ids; + return $this; + } + + public function withTargetPHIDs(array $phids) { + $this->targetPHIDs = $phids; + return $this; + } + + public function withConsumed($consumed) { + $this->consumed = $consumed; + return $this; + } + + public function newResultObject() { + return new DrydockCommand(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $commands) { + $target_phids = mpull($commands, 'getTargetPHID'); + + $targets = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($target_phids) + ->execute(); + $targets = mpull($targets, null, 'getPHID'); + + foreach ($commands as $key => $command) { + $target = idx($targets, $command->getTargetPHID()); + if (!$target) { + $this->didRejectResult($command); + unset($commands[$key]); + continue; + } + $command->attachCommandTarget($target); + } + + return $commands; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->targetPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'targetPHID IN (%Ls)', + $this->targetPHIDs); + } + + if ($this->consumed !== null) { + $where[] = qsprintf( + $conn, + 'isConsumed = %d', + (int)$this->consumed); + } + + return $where; + } + +} diff --git a/src/applications/drydock/query/DrydockLeaseQuery.php b/src/applications/drydock/query/DrydockLeaseQuery.php index f7adc07cce..a212a27ca8 100644 --- a/src/applications/drydock/query/DrydockLeaseQuery.php +++ b/src/applications/drydock/query/DrydockLeaseQuery.php @@ -7,6 +7,7 @@ final class DrydockLeaseQuery extends DrydockQuery { private $resourceIDs; private $statuses; private $datasourceQuery; + private $needCommands; public function withIDs(array $ids) { $this->ids = $ids; diff --git a/src/applications/drydock/storage/DrydockCommand.php b/src/applications/drydock/storage/DrydockCommand.php new file mode 100644 index 0000000000..60cb363ecb --- /dev/null +++ b/src/applications/drydock/storage/DrydockCommand.php @@ -0,0 +1,69 @@ +setAuthorPHID($author->getPHID()) + ->setIsConsumed(0); + } + + protected function getConfiguration() { + return array( + self::CONFIG_COLUMN_SCHEMA => array( + 'command' => 'text32', + 'isConsumed' => 'bool', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_target' => array( + 'columns' => array('targetPHID', 'isConsumed'), + ), + ), + ) + parent::getConfiguration(); + } + + public function attachCommandTarget($target) { + $this->commandTarget = $target; + return $this; + } + + public function getCommandTarget() { + return $this->assertAttached($this->commandTarget); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getCommandTarget()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getCommandTarget()->hasAutomaticCapability( + $capability, + $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht('Drydock commands have the same policies as their targets.'); + } + +} diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index 475d209ea0..bcdd008f68 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -30,11 +30,24 @@ final class DrydockLease extends DrydockDAO } public function __destruct() { - if ($this->releaseOnDestruction) { - if ($this->isActive()) { - $this->release(); - } + if (!$this->releaseOnDestruction) { + return; } + + if (!$this->canRelease()) { + return; + } + + $actor = PhabricatorUser::getOmnipotentUser(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $command = DrydockCommand::initializeNewCommand($actor) + ->setTargetPHID($this->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $this->scheduleUpdate(); } public function getLeaseName() { @@ -130,18 +143,6 @@ final class DrydockLease extends DrydockDAO return $this; } - public function release() { - $this->assertActive(); - $this->setStatus(DrydockLeaseStatus::STATUS_RELEASED); - $this->save(); - - DrydockSlotLock::releaseLocks($this->getPHID()); - - $this->resource = null; - - return $this; - } - public function isActive() { switch ($this->status) { case DrydockLeaseStatus::STATUS_ACQUIRED: @@ -262,6 +263,10 @@ final class DrydockLease extends DrydockDAO $this->isAcquired = true; + if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) { + $this->didActivate(); + } + return $this; } @@ -301,6 +306,8 @@ final class DrydockLease extends DrydockDAO $this->isActivated = true; + $this->didActivate(); + return $this; } @@ -308,6 +315,48 @@ final class DrydockLease extends DrydockDAO return $this->isActivated; } + public function canRelease() { + if (!$this->getID()) { + return false; + } + + switch ($this->getStatus()) { + case DrydockLeaseStatus::STATUS_RELEASED: + return false; + default: + return true; + } + } + + public function scheduleUpdate() { + PhabricatorWorker::scheduleTask( + 'DrydockLeaseUpdateWorker', + array( + 'leasePHID' => $this->getPHID(), + ), + array( + 'objectPHID' => $this->getPHID(), + )); + } + + private function didActivate() { + $viewer = PhabricatorUser::getOmnipotentUser(); + $need_update = false; + + $commands = id(new DrydockCommandQuery()) + ->setViewer($viewer) + ->withTargetPHIDs(array($this->getPHID())) + ->withConsumed(false) + ->execute(); + if ($commands) { + $need_update = true; + } + + if ($need_update) { + $this->scheduleUpdate(); + } + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -315,6 +364,7 @@ final class DrydockLease extends DrydockDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } @@ -322,6 +372,9 @@ final class DrydockLease extends DrydockDAO if ($this->getResource()) { return $this->getResource()->getPolicy($capability); } + + // TODO: Implement reasonable policies. + return PhabricatorPolicies::getMostOpenPolicy(); } diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php index 6affb7863c..fefb6e7516 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -170,43 +170,44 @@ final class DrydockResource extends DrydockDAO return $this->isActivated; } - public function closeResource() { + public function canRelease() { + switch ($this->getStatus()) { + case DrydockResourceStatus::STATUS_CLOSED: + case DrydockResourceStatus::STATUS_DESTROYED: + return false; + default: + return true; + } + } - // TODO: This is super broken and will race other lease writers! + public function scheduleUpdate() { + PhabricatorWorker::scheduleTask( + 'DrydockResourceUpdateWorker', + array( + 'resourcePHID' => $this->getPHID(), + ), + array( + 'objectPHID' => $this->getPHID(), + )); + } - $this->openTransaction(); - $statuses = array( - DrydockLeaseStatus::STATUS_PENDING, - DrydockLeaseStatus::STATUS_ACTIVE, - ); + private function didActivate() { + $viewer = PhabricatorUser::getOmnipotentUser(); - $leases = id(new DrydockLeaseQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withResourceIDs(array($this->getID())) - ->withStatuses($statuses) - ->execute(); + $need_update = false; - foreach ($leases as $lease) { - switch ($lease->getStatus()) { - case DrydockLeaseStatus::STATUS_PENDING: - $message = pht('Breaking pending lease (resource closing).'); - $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN); - break; - case DrydockLeaseStatus::STATUS_ACTIVE: - $message = pht('Releasing active lease (resource closing).'); - $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED); - break; - } - DrydockBlueprintImplementation::writeLog($this, $lease, $message); - $lease->save(); - } + $commands = id(new DrydockCommandQuery()) + ->setViewer($viewer) + ->withTargetPHIDs(array($this->getPHID())) + ->withConsumed(false) + ->execute(); + if ($commands) { + $need_update = true; + } - $this->setStatus(DrydockResourceStatus::STATUS_CLOSED); - $this->save(); - - DrydockSlotLock::releaseLocks($this->getPHID()); - - $this->saveTransaction(); + if ($need_update) { + $this->scheduleUpdate(); + } } @@ -216,12 +217,15 @@ final class DrydockResource extends DrydockDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: + case PhabricatorPolicyCapability::CAN_EDIT: + // TODO: Implement reasonable policies. return PhabricatorPolicies::getMostOpenPolicy(); } } diff --git a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php new file mode 100644 index 0000000000..5e96a608f6 --- /dev/null +++ b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php @@ -0,0 +1,60 @@ +getTaskDataValue('leasePHID'); + + $hash = PhabricatorHash::digestForIndex($lease_phid); + $lock_key = 'drydock.lease:'.$hash; + + $lock = PhabricatorGlobalLock::newLock($lock_key) + ->lock(1); + + $lease = $this->loadLease($lease_phid); + $this->updateLease($lease); + + $lock->unlock(); + } + + private function updateLease(DrydockLease $lease) { + $commands = $this->loadCommands($lease->getPHID()); + foreach ($commands as $command) { + if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { + // Leases can't receive commands before they activate or after they + // release. + break; + } + + $this->processCommand($lease, $command); + $command + ->setIsConsumed(true) + ->save(); + } + } + + private function processCommand( + DrydockLease $lease, + DrydockCommand $command) { + switch ($command->getCommand()) { + case DrydockCommand::COMMAND_RELEASE: + $this->releaseLease($lease); + break; + } + } + + private function releaseLease(DrydockLease $lease) { + $lease->openTransaction(); + $lease + ->setStatus(DrydockLeaseStatus::STATUS_RELEASED) + ->save(); + + // TODO: Hold slot locks until destruction? + DrydockSlotLock::releaseLocks($lease->getPHID()); + $lease->saveTransaction(); + + // TODO: Hook for resource release behaviors. + // TODO: Schedule lease destruction. + } + +} diff --git a/src/applications/drydock/worker/DrydockResourceUpdateWorker.php b/src/applications/drydock/worker/DrydockResourceUpdateWorker.php new file mode 100644 index 0000000000..78c5726b32 --- /dev/null +++ b/src/applications/drydock/worker/DrydockResourceUpdateWorker.php @@ -0,0 +1,92 @@ +getTaskDataValue('resourcePHID'); + + $hash = PhabricatorHash::digestForIndex($resource_phid); + $lock_key = 'drydock.resource:'.$hash; + + $lock = PhabricatorGlobalLock::newLock($lock_key) + ->lock(1); + + $resource = $this->loadResource($resource_phid); + $this->updateResource($resource); + + $lock->unlock(); + } + + private function updateResource(DrydockResource $resource) { + $commands = $this->loadCommands($resource->getPHID()); + foreach ($commands as $command) { + if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { + // Resources can't receive commands before they activate or after they + // release. + break; + } + + $this->processCommand($resource, $command); + + $command + ->setIsConsumed(true) + ->save(); + } + } + + private function processCommand( + DrydockResource $resource, + DrydockCommand $command) { + + switch ($command->getCommand()) { + case DrydockCommand::COMMAND_RELEASE: + $this->releaseResource($resource); + break; + } + } + + private function releaseResource(DrydockResource $resource) { + if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { + // If we had multiple release commands + // This command is only meaningful to resources in the "Open" state. + return; + } + + $viewer = $this->getViewer(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $resource->openTransaction(); + $resource + ->setStatus(DrydockResourceStatus::STATUS_CLOSED) + ->save(); + + // TODO: Hold slot locks until destruction? + DrydockSlotLock::releaseLocks($resource->getPHID()); + $resource->saveTransaction(); + + $statuses = array( + DrydockLeaseStatus::STATUS_PENDING, + DrydockLeaseStatus::STATUS_ACQUIRED, + DrydockLeaseStatus::STATUS_ACTIVE, + ); + + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withResourceIDs(array($resource->getID())) + ->withStatuses($statuses) + ->execute(); + + foreach ($leases as $lease) { + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); + } + + // TODO: Schedule resource destruction. + } + +} diff --git a/src/applications/drydock/worker/DrydockWorker.php b/src/applications/drydock/worker/DrydockWorker.php index abff7bcd39..d41643de47 100644 --- a/src/applications/drydock/worker/DrydockWorker.php +++ b/src/applications/drydock/worker/DrydockWorker.php @@ -36,4 +36,18 @@ abstract class DrydockWorker extends PhabricatorWorker { return $resource; } + protected function loadCommands($target_phid) { + $viewer = $this->getViewer(); + + $commands = id(new DrydockCommandQuery()) + ->setViewer($viewer) + ->withTargetPHIDs(array($target_phid)) + ->withConsumed(false) + ->execute(); + + $commands = msort($commands, 'getID'); + + return $commands; + } + } diff --git a/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php index 73d00844af..ce109eb498 100644 --- a/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php +++ b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php @@ -62,13 +62,16 @@ final class HarbormasterHostArtifact extends HarbormasterArtifact { public function releaseArtifact(PhabricatorUser $actor) { $lease = $this->loadArtifactLease($actor); - $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - - if ($lease->isActive()) { - $blueprint->releaseLease($resource, $lease); + if (!$lease->canRelease()) { + return; } + + $command = DrydockCommand::initializeNewCommand($actor) + ->setTargetPHID($lease->getPHID()) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); } - }