Path: blob/master/src/applications/herald/worker/HeraldWebhookWorker.php
12256 views
<?php12final class HeraldWebhookWorker3extends PhabricatorWorker {45protected function doWork() {6$viewer = PhabricatorUser::getOmnipotentUser();78$data = $this->getTaskData();9$request_phid = idx($data, 'webhookRequestPHID');1011$request = id(new HeraldWebhookRequestQuery())12->setViewer($viewer)13->withPHIDs(array($request_phid))14->executeOne();15if (!$request) {16throw new PhabricatorWorkerPermanentFailureException(17pht(18'Unable to load webhook request ("%s"). It may have been '.19'garbage collected.',20$request_phid));21}2223$status = $request->getStatus();24if ($status !== HeraldWebhookRequest::STATUS_QUEUED) {25throw new PhabricatorWorkerPermanentFailureException(26pht(27'Webhook request ("%s") is not in "%s" status (actual '.28'status is "%s"). Declining call to hook.',29$request_phid,30HeraldWebhookRequest::STATUS_QUEUED,31$status));32}3334// If we're in silent mode, permanently fail the webhook request and then35// return to complete this task.36if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {37$this->failRequest(38$request,39HeraldWebhookRequest::ERRORTYPE_HOOK,40HeraldWebhookRequest::ERROR_SILENT);41return;42}4344$hook = $request->getWebhook();4546if ($hook->isDisabled()) {47$this->failRequest(48$request,49HeraldWebhookRequest::ERRORTYPE_HOOK,50HeraldWebhookRequest::ERROR_DISABLED);51throw new PhabricatorWorkerPermanentFailureException(52pht(53'Associated hook ("%s") for webhook request ("%s") is disabled.',54$hook->getPHID(),55$request_phid));56}5758$uri = $hook->getWebhookURI();59try {60PhabricatorEnv::requireValidRemoteURIForFetch(61$uri,62array(63'http',64'https',65));66} catch (Exception $ex) {67$this->failRequest(68$request,69HeraldWebhookRequest::ERRORTYPE_HOOK,70HeraldWebhookRequest::ERROR_URI);71throw new PhabricatorWorkerPermanentFailureException(72pht(73'Associated hook ("%s") for webhook request ("%s") has invalid '.74'fetch URI: %s',75$hook->getPHID(),76$request_phid,77$ex->getMessage()));78}7980$object_phid = $request->getObjectPHID();8182$object = id(new PhabricatorObjectQuery())83->setViewer($viewer)84->withPHIDs(array($object_phid))85->executeOne();86if (!$object) {87$this->failRequest(88$request,89HeraldWebhookRequest::ERRORTYPE_HOOK,90HeraldWebhookRequest::ERROR_OBJECT);9192throw new PhabricatorWorkerPermanentFailureException(93pht(94'Unable to load object ("%s") for webhook request ("%s").',95$object_phid,96$request_phid));97}9899$xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject(100$object);101$xaction_phids = $request->getTransactionPHIDs();102if ($xaction_phids) {103$xactions = $xaction_query104->setViewer($viewer)105->withObjectPHIDs(array($object_phid))106->withPHIDs($xaction_phids)107->execute();108$xactions = mpull($xactions, null, 'getPHID');109} else {110$xactions = array();111}112113// To prevent thundering herd issues for high volume webhooks (where114// a large number of workers might try to work through a request backlog115// simultaneously, before the error backoff can catch up), we never116// parallelize requests to a particular webhook.117118$lock_key = 'webhook('.$hook->getPHID().')';119$lock = PhabricatorGlobalLock::newLock($lock_key);120121try {122$lock->lock();123} catch (Exception $ex) {124phlog($ex);125throw new PhabricatorWorkerYieldException(15);126}127128$caught = null;129try {130$this->callWebhookWithLock($hook, $request, $object, $xactions);131} catch (Exception $ex) {132$caught = $ex;133}134135$lock->unlock();136137if ($caught) {138throw $caught;139}140}141142private function callWebhookWithLock(143HeraldWebhook $hook,144HeraldWebhookRequest $request,145$object,146array $xactions) {147$viewer = PhabricatorUser::getOmnipotentUser();148149if ($hook->isInErrorBackoff($viewer)) {150throw new PhabricatorWorkerYieldException($hook->getErrorBackoffWindow());151}152153$xaction_data = array();154foreach ($xactions as $xaction) {155$xaction_data[] = array(156'phid' => $xaction->getPHID(),157);158}159160$trigger_data = array();161foreach ($request->getTriggerPHIDs() as $trigger_phid) {162$trigger_data[] = array(163'phid' => $trigger_phid,164);165}166167$payload = array(168'object' => array(169'type' => phid_get_type($object->getPHID()),170'phid' => $object->getPHID(),171),172'triggers' => $trigger_data,173'action' => array(174'test' => $request->getIsTestAction(),175'silent' => $request->getIsSilentAction(),176'secure' => $request->getIsSecureAction(),177'epoch' => (int)$request->getDateCreated(),178),179'transactions' => $xaction_data,180);181182$payload = id(new PhutilJSON())->encodeFormatted($payload);183$key = $hook->getHmacKey();184$signature = PhabricatorHash::digestHMACSHA256($payload, $key);185$uri = $hook->getWebhookURI();186187$future = id(new HTTPSFuture($uri))188->setMethod('POST')189->addHeader('Content-Type', 'application/json')190->addHeader('X-Phabricator-Webhook-Signature', $signature)191->setTimeout(15)192->setData($payload);193194list($status) = $future->resolve();195196if ($status->isTimeout()) {197$error_type = HeraldWebhookRequest::ERRORTYPE_TIMEOUT;198} else {199$error_type = HeraldWebhookRequest::ERRORTYPE_HTTP;200}201$error_code = $status->getStatusCode();202203$request204->setErrorType($error_type)205->setErrorCode($error_code)206->setLastRequestEpoch(PhabricatorTime::getNow());207208$retry_forever = HeraldWebhookRequest::RETRY_FOREVER;209if ($status->isTimeout() || $status->isError()) {210$should_retry = ($request->getRetryMode() === $retry_forever);211212$request213->setLastRequestResult(HeraldWebhookRequest::RESULT_FAIL);214215if ($should_retry) {216$request->save();217218throw new Exception(219pht(220'Webhook request ("%s", to "%s") failed (%s / %s). The request '.221'will be retried.',222$request->getPHID(),223$uri,224$error_type,225$error_code));226} else {227$request228->setStatus(HeraldWebhookRequest::STATUS_FAILED)229->save();230231throw new PhabricatorWorkerPermanentFailureException(232pht(233'Webhook request ("%s", to "%s") failed (%s / %s). The request '.234'will not be retried.',235$request->getPHID(),236$uri,237$error_type,238$error_code));239}240} else {241$request242->setLastRequestResult(HeraldWebhookRequest::RESULT_OKAY)243->setStatus(HeraldWebhookRequest::STATUS_SENT)244->save();245}246}247248private function failRequest(249HeraldWebhookRequest $request,250$error_type,251$error_code) {252253$request254->setStatus(HeraldWebhookRequest::STATUS_FAILED)255->setErrorType($error_type)256->setErrorCode($error_code)257->setLastRequestResult(HeraldWebhookRequest::RESULT_NONE)258->setLastRequestEpoch(0)259->save();260}261262}263264265