3 final class HeraldWebhookWorker
4 extends PhabricatorWorker
{
6 protected function doWork() {
7 $viewer = PhabricatorUser
::getOmnipotentUser();
9 $data = $this->getTaskData();
10 $request_phid = idx($data, 'webhookRequestPHID');
12 $request = id(new HeraldWebhookRequestQuery())
14 ->withPHIDs(array($request_phid))
17 throw new PhabricatorWorkerPermanentFailureException(
19 'Unable to load webhook request ("%s"). It may have been '.
24 $status = $request->getStatus();
25 if ($status !== HeraldWebhookRequest
::STATUS_QUEUED
) {
26 throw new PhabricatorWorkerPermanentFailureException(
28 'Webhook request ("%s") is not in "%s" status (actual '.
29 'status is "%s"). Declining call to hook.',
31 HeraldWebhookRequest
::STATUS_QUEUED
,
35 // If we're in silent mode, permanently fail the webhook request and then
36 // return to complete this task.
37 if (PhabricatorEnv
::getEnvConfig('phabricator.silent')) {
40 HeraldWebhookRequest
::ERRORTYPE_HOOK
,
41 HeraldWebhookRequest
::ERROR_SILENT
);
45 $hook = $request->getWebhook();
47 if ($hook->isDisabled()) {
50 HeraldWebhookRequest
::ERRORTYPE_HOOK
,
51 HeraldWebhookRequest
::ERROR_DISABLED
);
52 throw new PhabricatorWorkerPermanentFailureException(
54 'Associated hook ("%s") for webhook request ("%s") is disabled.',
59 $uri = $hook->getWebhookURI();
61 PhabricatorEnv
::requireValidRemoteURIForFetch(
67 } catch (Exception
$ex) {
70 HeraldWebhookRequest
::ERRORTYPE_HOOK
,
71 HeraldWebhookRequest
::ERROR_URI
);
72 throw new PhabricatorWorkerPermanentFailureException(
74 'Associated hook ("%s") for webhook request ("%s") has invalid '.
81 $object_phid = $request->getObjectPHID();
83 $object = id(new PhabricatorObjectQuery())
85 ->withPHIDs(array($object_phid))
90 HeraldWebhookRequest
::ERRORTYPE_HOOK
,
91 HeraldWebhookRequest
::ERROR_OBJECT
);
93 throw new PhabricatorWorkerPermanentFailureException(
95 'Unable to load object ("%s") for webhook request ("%s").',
100 $xaction_query = PhabricatorApplicationTransactionQuery
::newQueryForObject(
102 $xaction_phids = $request->getTransactionPHIDs();
103 if ($xaction_phids) {
104 $xactions = $xaction_query
106 ->withObjectPHIDs(array($object_phid))
107 ->withPHIDs($xaction_phids)
109 $xactions = mpull($xactions, null, 'getPHID');
114 // To prevent thundering herd issues for high volume webhooks (where
115 // a large number of workers might try to work through a request backlog
116 // simultaneously, before the error backoff can catch up), we never
117 // parallelize requests to a particular webhook.
119 $lock_key = 'webhook('.$hook->getPHID().')';
120 $lock = PhabricatorGlobalLock
::newLock($lock_key);
124 } catch (Exception
$ex) {
126 throw new PhabricatorWorkerYieldException(15);
131 $this->callWebhookWithLock($hook, $request, $object, $xactions);
132 } catch (Exception
$ex) {
143 private function callWebhookWithLock(
145 HeraldWebhookRequest
$request,
148 $viewer = PhabricatorUser
::getOmnipotentUser();
150 if ($hook->isInErrorBackoff($viewer)) {
151 throw new PhabricatorWorkerYieldException($hook->getErrorBackoffWindow());
154 $xaction_data = array();
155 foreach ($xactions as $xaction) {
156 $xaction_data[] = array(
157 'phid' => $xaction->getPHID(),
161 $trigger_data = array();
162 foreach ($request->getTriggerPHIDs() as $trigger_phid) {
163 $trigger_data[] = array(
164 'phid' => $trigger_phid,
170 'type' => phid_get_type($object->getPHID()),
171 'phid' => $object->getPHID(),
173 'triggers' => $trigger_data,
175 'test' => $request->getIsTestAction(),
176 'silent' => $request->getIsSilentAction(),
177 'secure' => $request->getIsSecureAction(),
178 'epoch' => (int)$request->getDateCreated(),
180 'transactions' => $xaction_data,
183 $payload = id(new PhutilJSON())->encodeFormatted($payload);
184 $key = $hook->getHmacKey();
185 $signature = PhabricatorHash
::digestHMACSHA256($payload, $key);
186 $uri = $hook->getWebhookURI();
188 $future = id(new HTTPSFuture($uri))
190 ->addHeader('Content-Type', 'application/json')
191 ->addHeader('X-Phabricator-Webhook-Signature', $signature)
195 list($status) = $future->resolve();
197 if ($status->isTimeout()) {
198 $error_type = HeraldWebhookRequest
::ERRORTYPE_TIMEOUT
;
200 $error_type = HeraldWebhookRequest
::ERRORTYPE_HTTP
;
202 $error_code = $status->getStatusCode();
205 ->setErrorType($error_type)
206 ->setErrorCode($error_code)
207 ->setLastRequestEpoch(PhabricatorTime
::getNow());
209 $retry_forever = HeraldWebhookRequest
::RETRY_FOREVER
;
210 if ($status->isTimeout() ||
$status->isError()) {
211 $should_retry = ($request->getRetryMode() === $retry_forever);
214 ->setLastRequestResult(HeraldWebhookRequest
::RESULT_FAIL
);
221 'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
229 ->setStatus(HeraldWebhookRequest
::STATUS_FAILED
)
232 throw new PhabricatorWorkerPermanentFailureException(
234 'Webhook request ("%s", to "%s") failed (%s / %s). The request '.
235 'will not be retried.',
243 ->setLastRequestResult(HeraldWebhookRequest
::RESULT_OKAY
)
244 ->setStatus(HeraldWebhookRequest
::STATUS_SENT
)
249 private function failRequest(
250 HeraldWebhookRequest
$request,
255 ->setStatus(HeraldWebhookRequest
::STATUS_FAILED
)
256 ->setErrorType($error_type)
257 ->setErrorCode($error_code)
258 ->setLastRequestResult(HeraldWebhookRequest
::RESULT_NONE
)
259 ->setLastRequestEpoch(0)