Differential D13115 Diff 31770 src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
Changeset View
Changeset View
Standalone View
Standalone View
src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php
| <?php | <?php | ||||
| /** | /** | ||||
| * | |||||
| * Publishing and Managing State | |||||
| * ====== | |||||
| * | |||||
| * After applying changes, the Editor queues a worker to publish mail, feed, | |||||
| * and notifications, and to perform other background work like updating search | |||||
| * indexes. This allows it to do this work without impacting performance for | |||||
| * users. | |||||
| * | |||||
| * When work is moved to the daemons, the Editor state is serialized by | |||||
| * @{method:getWorkerState}, then reloaded in a daemon process by | |||||
| * @{method:loadWorkerState}. **This is fragile.** | |||||
| * | |||||
| * State is not persisted into the daemons by default, because we can not send | |||||
| * arbitrary objects into the queue. This means the default behavior of any | |||||
| * state properties is to reset to their defaults without warning prior to | |||||
| * publishing. | |||||
| * | |||||
| * The easiest way to avoid this is to keep Editors stateless: the overwhelming | |||||
| * majority of Editors can be written statelessly. If you need to maintain | |||||
| * state, you can either: | |||||
| * | |||||
| * - not require state to exist during publishing; or | |||||
| * - pass state to the daemons by implementing @{method:getCustomWorkerState} | |||||
| * and @{method:loadCustomWorkerState}. | |||||
| * | |||||
| * This architecture isn't ideal, and we may eventually split this class into | |||||
| * "Editor" and "Publisher" parts to make it more robust. See T6367 for some | |||||
| * discussion and context. | |||||
| * | |||||
| * @task mail Sending Mail | * @task mail Sending Mail | ||||
| * @task feed Publishing Feed Stories | * @task feed Publishing Feed Stories | ||||
| * @task search Search Index | * @task search Search Index | ||||
| * @task files Integration with Files | * @task files Integration with Files | ||||
| * @task workers Managing Workers | * @task workers Managing Workers | ||||
| */ | */ | ||||
| abstract class PhabricatorApplicationTransactionEditor | abstract class PhabricatorApplicationTransactionEditor | ||||
| extends PhabricatorEditor { | extends PhabricatorEditor { | ||||
| Show All 17 Lines | abstract class PhabricatorApplicationTransactionEditor | ||||
| private $isHeraldEditor; | private $isHeraldEditor; | ||||
| private $isInverseEdgeEditor; | private $isInverseEdgeEditor; | ||||
| private $actingAsPHID; | private $actingAsPHID; | ||||
| private $disableEmail; | private $disableEmail; | ||||
| private $heraldEmailPHIDs = array(); | private $heraldEmailPHIDs = array(); | ||||
| private $heraldForcedEmailPHIDs = array(); | private $heraldForcedEmailPHIDs = array(); | ||||
| private $heraldHeader; | private $heraldHeader; | ||||
| private $mailToPHIDs = array(); | |||||
| private $mailCCPHIDs = array(); | |||||
| private $feedNotifyPHIDs = array(); | |||||
| private $feedRelatedPHIDs = array(); | |||||
| /** | /** | ||||
| * Get the class name for the application this editor is a part of. | * Get the class name for the application this editor is a part of. | ||||
| * | * | ||||
| * Uninstalling the application will disable the editor. | * Uninstalling the application will disable the editor. | ||||
| * | * | ||||
| * @return string Editor's application class name. | * @return string Editor's application class name. | ||||
| */ | */ | ||||
| ▲ Show 20 Lines • Show All 857 Lines • ▼ Show 20 Lines | if ($herald_xscript) { | ||||
| $object->getPHID(), | $object->getPHID(), | ||||
| $herald_header); | $herald_header); | ||||
| } else { | } else { | ||||
| $herald_header = HeraldTranscript::loadXHeraldRulesHeader( | $herald_header = HeraldTranscript::loadXHeraldRulesHeader( | ||||
| $object->getPHID()); | $object->getPHID()); | ||||
| } | } | ||||
| $this->heraldHeader = $herald_header; | $this->heraldHeader = $herald_header; | ||||
| if ($this->supportsWorkers()) { | // We're going to compute some of the data we'll use to publish these | ||||
| // transactions here, before queueing a worker. | |||||
| // | |||||
| // Primarily, this is more correct: we want to publish the object as it | |||||
| // exists right now. The worker may not execute for some time, and we want | |||||
| // to use the current To/CC list, not respect any changes which may occur | |||||
| // between now and when the worker executes. | |||||
| // | |||||
| // As a secondary benefit, this tends to reduce the amount of state that | |||||
| // Editors need to pass into workers. | |||||
| $object = $this->willPublish($object, $xactions); | |||||
| if (!$this->getDisableEmail()) { | |||||
| if ($this->shouldSendMail($object, $xactions)) { | |||||
| $this->mailToPHIDs = $this->getMailTo($object); | |||||
| $this->mailCCPHIDs = $this->getMailCC($object); | |||||
| } | |||||
| } | |||||
| if ($this->shouldPublishFeedStory($object, $xactions)) { | |||||
| $this->feedRelatedPHIDs = $this->getFeedRelatedPHIDs($object, $xactions); | |||||
| $this->feedNotifyPHIDs = $this->getFeedNotifyPHIDs($object, $xactions); | |||||
| } | |||||
| PhabricatorWorker::scheduleTask( | PhabricatorWorker::scheduleTask( | ||||
| 'PhabricatorApplicationTransactionPublishWorker', | 'PhabricatorApplicationTransactionPublishWorker', | ||||
| array( | array( | ||||
| 'objectPHID' => $object->getPHID(), | 'objectPHID' => $object->getPHID(), | ||||
| 'actorPHID' => $this->getActingAsPHID(), | 'actorPHID' => $this->getActingAsPHID(), | ||||
| 'xactionPHIDs' => mpull($xactions, 'getPHID'), | 'xactionPHIDs' => mpull($xactions, 'getPHID'), | ||||
| 'state' => $this->getWorkerState(), | 'state' => $this->getWorkerState(), | ||||
| ), | ), | ||||
| array( | array( | ||||
| 'objectPHID' => $object->getPHID(), | 'objectPHID' => $object->getPHID(), | ||||
| 'priority' => PhabricatorWorker::PRIORITY_ALERTS, | 'priority' => PhabricatorWorker::PRIORITY_ALERTS, | ||||
| )); | )); | ||||
| } else { | |||||
| $this->publishTransactions($object, $xactions); | |||||
| } | |||||
| return $xactions; | return $xactions; | ||||
| } | } | ||||
| public function publishTransactions( | public function publishTransactions( | ||||
| PhabricatorLiskDAO $object, | PhabricatorLiskDAO $object, | ||||
| array $xactions) { | array $xactions) { | ||||
| // Before sending mail or publishing feed stories, reload the object | // Before sending mail or publishing feed stories, reload the object | ||||
| // subscribers to pick up changes caused by Herald (or by other side effects | // subscribers to pick up changes caused by Herald (or by other side effects | ||||
| // in various transaction phases). | // in various transaction phases). | ||||
| $this->loadSubscribers($object); | $this->loadSubscribers($object); | ||||
| // Hook for other edges that may need (re-)loading | // Hook for other edges that may need (re-)loading | ||||
| $this->loadEdges($object, $xactions); | $object = $this->willPublish($object, $xactions); | ||||
| $this->loadHandles($xactions); | $this->loadHandles($xactions); | ||||
| $mail = null; | $mail = null; | ||||
| if (!$this->getDisableEmail()) { | if (!$this->getDisableEmail()) { | ||||
| if ($this->shouldSendMail($object, $xactions)) { | if ($this->shouldSendMail($object, $xactions)) { | ||||
| $mail = $this->sendMail($object, $xactions); | $mail = $this->sendMail($object, $xactions); | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 89 Lines • ▼ Show 20 Lines | if ($object->getPHID() && | ||||
| $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID( | $subs = PhabricatorSubscribersQuery::loadSubscribersForPHID( | ||||
| $object->getPHID()); | $object->getPHID()); | ||||
| $this->subscribers = array_fuse($subs); | $this->subscribers = array_fuse($subs); | ||||
| } else { | } else { | ||||
| $this->subscribers = array(); | $this->subscribers = array(); | ||||
| } | } | ||||
| } | } | ||||
| protected function loadEdges( | |||||
| PhabricatorLiskDAO $object, | |||||
| array $xactions) { | |||||
| return; | |||||
| } | |||||
| private function validateEditParameters( | private function validateEditParameters( | ||||
| PhabricatorLiskDAO $object, | PhabricatorLiskDAO $object, | ||||
| array $xactions) { | array $xactions) { | ||||
| if (!$this->getContentSource()) { | if (!$this->getContentSource()) { | ||||
| throw new PhutilInvalidStateException('setContentSource'); | throw new PhutilInvalidStateException('setContentSource'); | ||||
| } | } | ||||
| ▲ Show 20 Lines • Show All 1,020 Lines • ▼ Show 20 Lines | foreach ($xactions as $xaction) { | ||||
| } | } | ||||
| } | } | ||||
| if (!$any_visible) { | if (!$any_visible) { | ||||
| return; | return; | ||||
| } | } | ||||
| $email_force = array(); | $email_force = array(); | ||||
| $email_to = $this->getMailTo($object); | $email_to = $this->mailToPHIDs; | ||||
| $email_cc = $this->getMailCC($object); | $email_cc = $this->mailCCPHIDs; | ||||
| $email_cc = array_merge($email_cc, $this->heraldEmailPHIDs); | $email_cc = array_merge($email_cc, $this->heraldEmailPHIDs); | ||||
| $email_force = $this->heraldForcedEmailPHIDs; | $email_force = $this->heraldForcedEmailPHIDs; | ||||
| $phids = array_merge($email_to, $email_cc); | $phids = array_merge($email_to, $email_cc); | ||||
| $handles = id(new PhabricatorHandleQuery()) | $handles = id(new PhabricatorHandleQuery()) | ||||
| ->setViewer($this->requireActor()) | ->setViewer($this->requireActor()) | ||||
| ->withPHIDs($phids) | ->withPHIDs($phids) | ||||
| ▲ Show 20 Lines • Show All 409 Lines • ▼ Show 20 Lines | protected function publishFeedStory( | ||||
| array $mailed_phids) { | array $mailed_phids) { | ||||
| $xactions = mfilter($xactions, 'shouldHideForFeed', true); | $xactions = mfilter($xactions, 'shouldHideForFeed', true); | ||||
| if (!$xactions) { | if (!$xactions) { | ||||
| return; | return; | ||||
| } | } | ||||
| $related_phids = $this->getFeedRelatedPHIDs($object, $xactions); | $related_phids = $this->feedRelatedPHIDs; | ||||
| $subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions); | $subscribed_phids = $this->feedNotifyPHIDs; | ||||
| $story_type = $this->getFeedStoryType(); | $story_type = $this->getFeedStoryType(); | ||||
| $story_data = $this->getFeedStoryData($object, $xactions); | $story_data = $this->getFeedStoryData($object, $xactions); | ||||
| id(new PhabricatorFeedStoryPublisher()) | id(new PhabricatorFeedStoryPublisher()) | ||||
| ->setStoryType($story_type) | ->setStoryType($story_type) | ||||
| ->setStoryData($story_data) | ->setStoryData($story_data) | ||||
| ->setStoryTime(time()) | ->setStoryTime(time()) | ||||
| ▲ Show 20 Lines • Show All 271 Lines • ▼ Show 20 Lines | private function applyInverseEdgeTransactions( | ||||
| } | } | ||||
| } | } | ||||
| /* -( Workers )------------------------------------------------------------ */ | /* -( Workers )------------------------------------------------------------ */ | ||||
| /** | /** | ||||
| * Load any object state which is required to publish transactions. | |||||
| * | |||||
| * This hook is invoked in the main process before we compute data related | |||||
| * to publishing transactions (like email "To" and "CC" lists), and again in | |||||
| * the worker before publishing occurs. | |||||
| * | |||||
| * @return object Publishable object. | |||||
| * @task workers | * @task workers | ||||
| */ | */ | ||||
| protected function supportsWorkers() { | protected function willPublish(PhabricatorLiskDAO $object, array $xactions) { | ||||
| // TODO: Remove this method once everything supports workers. | return $object; | ||||
| return false; | |||||
| } | } | ||||
| /** | /** | ||||
| * Convert the editor state to a serializable dictionary which can be passed | * Convert the editor state to a serializable dictionary which can be passed | ||||
| * to a worker. | * to a worker. | ||||
| * | * | ||||
| * This data will be loaded with @{method:loadWorkerState} in the worker. | * This data will be loaded with @{method:loadWorkerState} in the worker. | ||||
| * | * | ||||
| * @return dict<string, wild> Serializable editor state. | * @return dict<string, wild> Serializable editor state. | ||||
| * @task workers | * @task workers | ||||
| */ | */ | ||||
| final private function getWorkerState() { | final private function getWorkerState() { | ||||
| $state = array(); | $state = array(); | ||||
| foreach ($this->getAutomaticStateProperties() as $property) { | foreach ($this->getAutomaticStateProperties() as $property) { | ||||
| $state[$property] = $this->$property; | $state[$property] = $this->$property; | ||||
| } | } | ||||
| $state += array( | $state += array( | ||||
| 'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(), | 'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(), | ||||
| ); | |||||
| return $state + array( | |||||
| 'custom' => $this->getCustomWorkerState(), | 'custom' => $this->getCustomWorkerState(), | ||||
| ); | ); | ||||
| return $state; | |||||
| } | } | ||||
| /** | /** | ||||
| * Hook; return custom properties which need to be passed to workers. | * Hook; return custom properties which need to be passed to workers. | ||||
| * | * | ||||
| * @return dict<string, wild> Custom properties. | * @return dict<string, wild> Custom properties. | ||||
| * @task workers | * @task workers | ||||
| ▲ Show 20 Lines • Show All 54 Lines • ▼ Show 20 Lines | /* -( Workers )------------------------------------------------------------ */ | ||||
| private function getAutomaticStateProperties() { | private function getAutomaticStateProperties() { | ||||
| return array( | return array( | ||||
| 'parentMessageID', | 'parentMessageID', | ||||
| 'disableEmail', | 'disableEmail', | ||||
| 'isNewObject', | 'isNewObject', | ||||
| 'heraldEmailPHIDs', | 'heraldEmailPHIDs', | ||||
| 'heraldForcedEmailPHIDs', | 'heraldForcedEmailPHIDs', | ||||
| 'heraldHeader', | 'heraldHeader', | ||||
| 'mailToPHIDs', | |||||
| 'mailCCPHIDs', | |||||
| 'feedNotifyPHIDs', | |||||
| 'feedRelatedPHIDs', | |||||
| ); | ); | ||||
| } | } | ||||
| } | } | ||||