diff --git a/src/applications/nuance/command/NuanceCommandImplementation.php b/src/applications/nuance/command/NuanceCommandImplementation.php index 46220266ff..2b47e40654 100644 --- a/src/applications/nuance/command/NuanceCommandImplementation.php +++ b/src/applications/nuance/command/NuanceCommandImplementation.php @@ -1,107 +1,113 @@ actor = $actor; return $this; } final public function getActor() { return $this->actor; } abstract public function getCommandName(); abstract public function canApplyToItem(NuanceItem $item); + public function canApplyImmediately( + NuanceItem $item, + NuanceItemCommand $command) { + return false; + } + abstract protected function executeCommand( NuanceItem $item, NuanceItemCommand $command); final public function applyCommand( NuanceItem $item, NuanceItemCommand $command) { $command_key = $command->getCommand(); $implementation_key = $this->getCommandKey(); if ($command_key !== $implementation_key) { throw new Exception( pht( 'This command implementation("%s") can not apply a command of a '. 'different type ("%s").', $implementation_key, $command_key)); } if (!$this->canApplyToItem($item)) { throw new Exception( pht( 'This command implementation ("%s") can not be applied to an '. 'item of type "%s".', $implementation_key, $item->getItemType())); } $this->transactionQueue = array(); $command_type = NuanceItemCommandTransaction::TRANSACTIONTYPE; $command_xaction = $this->newTransaction($command_type); $result = $this->executeCommand($item, $command); $xactions = $this->transactionQueue; $this->transactionQueue = array(); $command_xaction->setNewValue( array( 'command' => $command->getCommand(), 'parameters' => $command->getParameters(), 'result' => $result, )); // TODO: Maybe preserve the actor's original content source? $source = PhabricatorContentSource::newForSource( PhabricatorDaemonContentSource::SOURCECONST); $actor = $this->getActor(); id(new NuanceItemEditor()) ->setActor($actor) ->setActingAsPHID($command->getAuthorPHID()) ->setContentSource($source) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) ->applyTransactions($item, $xactions); } final public function getCommandKey() { return $this->getPhobjectClassConstant('COMMANDKEY'); } final public static function getAllCommands() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getCommandKey') ->execute(); } protected function newTransaction($type) { $xaction = id(new NuanceItemTransaction()) ->setTransactionType($type); $this->transactionQueue[] = $xaction; return $xaction; } protected function newStatusTransaction($status) { return $this->newTransaction(NuanceItemStatusTransaction::TRANSACTIONTYPE) ->setNewValue($status); } } diff --git a/src/applications/nuance/command/NuanceTrashCommand.php b/src/applications/nuance/command/NuanceTrashCommand.php index 1f8a729277..b5ac851539 100644 --- a/src/applications/nuance/command/NuanceTrashCommand.php +++ b/src/applications/nuance/command/NuanceTrashCommand.php @@ -1,23 +1,29 @@ getImplementation(); return ($type instanceof NuanceFormItemType); } + public function canApplyImmediately( + NuanceItem $item, + NuanceItemCommand $command) { + return true; + } + protected function executeCommand( NuanceItem $item, NuanceItemCommand $command) { $this->newStatusTransaction(NuanceItem::STATUS_CLOSED); } } diff --git a/src/applications/nuance/controller/NuanceItemActionController.php b/src/applications/nuance/controller/NuanceItemActionController.php index c6dc139b11..39ae8fd995 100644 --- a/src/applications/nuance/controller/NuanceItemActionController.php +++ b/src/applications/nuance/controller/NuanceItemActionController.php @@ -1,90 +1,121 @@ getViewer(); $id = $request->getURIData('id'); if (!$request->validateCSRF()) { return new Aphront400Response(); } // NOTE: This controller can be reached from an individual item (usually // by a user) or while working through a queue (usually by staff). When // a command originates from a queue, the URI will have a queue ID. $item = id(new NuanceItemQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$item) { return new Aphront404Response(); } $cancel_uri = $item->getURI(); $queue_id = $request->getURIData('queueID'); $queue = null; if ($queue_id) { $queue = id(new NuanceQueueQuery()) ->setViewer($viewer) ->withIDs(array($queue_id)) ->executeOne(); if (!$queue) { return new Aphront404Response(); } $item_queue = $item->getQueue(); if (!$item_queue || ($item_queue->getPHID() != $queue->getPHID())) { return $this->newDialog() ->setTitle(pht('Wrong Queue')) ->appendParagraph( pht( 'You are trying to act on this item from the wrong queue: it '. 'is currently in a different queue.')) ->addCancelButton($cancel_uri); } } $action = $request->getURIData('action'); $impl = $item->getImplementation(); $impl->setViewer($viewer); $impl->setController($this); + $executors = NuanceCommandImplementation::getAllCommands(); + $executor = idx($executors, $action); + if (!$executor) { + return new Aphront404Response(); + } + + $executor = id(clone $executor) + ->setActor($viewer); + + if (!$executor->canApplyToItem($item)) { + return $this->newDialog() + ->setTitle(pht('Command Not Supported')) + ->appendParagraph( + pht( + 'This item does not support the specified command ("%s").', + $action)) + ->addCancelButton($cancel_uri); + } + $command = NuanceItemCommand::initializeNewCommand() ->setItemPHID($item->getPHID()) ->setAuthorPHID($viewer->getPHID()) ->setCommand($action); if ($queue) { $command->setQueuePHID($queue->getPHID()); } $command->save(); - // TODO: Here, we should check if the command should be tried immediately, - // and just defer it to the daemons if not. If we're going to try to apply - // the command directly, we should first acquire the worker lock. If we - // can not, we should defer the command even if it's an immediate command. - // For the moment, skip all this stuff by deferring unconditionally. + // If this command can be applied immediately, try to apply it now. - $should_defer = true; - if ($should_defer) { + // In most cases, local commands (like closing an item) can be applied + // immediately. + + // Commands that require making a call to a remote system (for example, + // to reply to a tweet or close a remote object) are usually done in the + // background so the user doesn't have to wait for the operation to + // complete before they can continue work. + + $did_apply = false; + $immediate = $executor->canApplyImmediately($item, $command); + if ($immediate) { + // TODO: Move this stuff to a new Engine, and have the controller and + // worker both call into the Engine. + $worker = new NuanceItemUpdateWorker(array()); + $did_apply = $worker->executeCommands($item, array($command)); + } + + // If this can't be applied immediately or we were unable to get a lock + // fast enough, do the update in the background instead. + if (!$did_apply) { $item->scheduleUpdate(); - } else { - // ... } if ($queue) { $done_uri = $queue->getWorkURI(); } else { $done_uri = $item->getURI(); } return id(new AphrontRedirectResponse()) ->setURI($done_uri); } } diff --git a/src/applications/nuance/worker/NuanceItemUpdateWorker.php b/src/applications/nuance/worker/NuanceItemUpdateWorker.php index 30d5621872..cd6637187c 100644 --- a/src/applications/nuance/worker/NuanceItemUpdateWorker.php +++ b/src/applications/nuance/worker/NuanceItemUpdateWorker.php @@ -1,108 +1,161 @@ getTaskDataValue('itemPHID'); - $hash = PhabricatorHash::digestForIndex($item_phid); - $lock_key = "nuance.item.{$hash}"; - $lock = PhabricatorGlobalLock::newLock($lock_key); + $lock = $this->newLock($item_phid); $lock->lock(1); try { $item = $this->loadItem($item_phid); $this->updateItem($item); $this->routeItem($item); $this->applyCommands($item); } catch (Exception $ex) { $lock->unlock(); throw $ex; } $lock->unlock(); } private function updateItem(NuanceItem $item) { $impl = $item->getImplementation(); if (!$impl->canUpdateItems()) { return null; } $viewer = $this->getViewer(); $impl->setViewer($viewer); $impl->updateItem($item); } private function routeItem(NuanceItem $item) { $status = $item->getStatus(); if ($status != NuanceItem::STATUS_ROUTING) { return; } $source = $item->getSource(); // For now, always route items into the source's default queue. $item ->setQueuePHID($source->getDefaultQueuePHID()) ->setStatus(NuanceItem::STATUS_OPEN) ->save(); } private function applyCommands(NuanceItem $item) { $viewer = $this->getViewer(); - $impl = $item->getImplementation(); - $impl->setViewer($viewer); - $commands = id(new NuanceItemCommandQuery()) ->setViewer($viewer) ->withItemPHIDs(array($item->getPHID())) ->withStatuses( array( NuanceItemCommand::STATUS_ISSUED, )) ->execute(); $commands = msort($commands, 'getID'); + $this->executeCommandList($item, $commands); + } + + public function executeCommands(NuanceItem $item, array $commands) { + if (!$commands) { + return true; + } + + $item_phid = $item->getPHID(); + $viewer = $this->getViewer(); + + $lock = $this->newLock($item_phid); + try { + $lock->lock(1); + } catch (PhutilLockException $ex) { + return false; + } + + try { + $item = $this->loadItem($item_phid); + + // Reload commands now that we have a lock, to make sure we don't + // execute any commands twice by mistake. + $commands = id(new NuanceItemCommandQuery()) + ->setViewer($viewer) + ->withIDs(mpull($commands, 'getID')) + ->execute(); + + $this->executeCommandList($item, $commands); + } catch (Exception $ex) { + $lock->unlock(); + throw $ex; + } + + $lock->unlock(); + + return true; + } + + private function executeCommandList(NuanceItem $item, array $commands) { + $viewer = $this->getViewer(); + $executors = NuanceCommandImplementation::getAllCommands(); foreach ($commands as $command) { + if ($command->getItemPHID() !== $item->getPHID()) { + throw new Exception( + pht('Trying to apply a command to the wrong item!')); + } + + if ($command->getStatus() !== NuanceItemCommand::STATUS_ISSUED) { + // Never execute commands which have already been issued. + continue; + } + $command ->setStatus(NuanceItemCommand::STATUS_EXECUTING) ->save(); try { $command_key = $command->getCommand(); $executor = idx($executors, $command_key); if (!$executor) { throw new Exception( pht( 'Unable to execute command "%s": this command does not have '. 'a recognized command implementation.', $command_key)); } $executor = clone $executor; $executor ->setActor($viewer) ->applyCommand($item, $command); $command ->setStatus(NuanceItemCommand::STATUS_DONE) ->save(); } catch (Exception $ex) { $command ->setStatus(NuanceItemCommand::STATUS_FAILED) ->save(); throw $ex; } } } + private function newLock($item_phid) { + $hash = PhabricatorHash::digestForIndex($item_phid); + $lock_key = "nuance.item.{$hash}"; + return PhabricatorGlobalLock::newLock($lock_key); + } + }