@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at upstream/main 263 lines 7.6 kB view raw
1<?php 2 3final class HeraldWebhookWorker 4 extends PhabricatorWorker { 5 6 protected function doWork() { 7 $viewer = PhabricatorUser::getOmnipotentUser(); 8 9 $data = $this->getTaskData(); 10 $request_phid = idx($data, 'webhookRequestPHID'); 11 12 $request = id(new HeraldWebhookRequestQuery()) 13 ->setViewer($viewer) 14 ->withPHIDs(array($request_phid)) 15 ->executeOne(); 16 if (!$request) { 17 throw new PhabricatorWorkerPermanentFailureException( 18 pht( 19 'Unable to load webhook request ("%s"). It may have been '. 20 'garbage collected.', 21 $request_phid)); 22 } 23 24 $status = $request->getStatus(); 25 if ($status !== HeraldWebhookRequest::STATUS_QUEUED) { 26 throw new PhabricatorWorkerPermanentFailureException( 27 pht( 28 'Webhook request ("%s") is not in "%s" status (actual '. 29 'status is "%s"). Declining call to hook.', 30 $request_phid, 31 HeraldWebhookRequest::STATUS_QUEUED, 32 $status)); 33 } 34 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')) { 38 $this->failRequest( 39 $request, 40 HeraldWebhookRequest::ERRORTYPE_HOOK, 41 HeraldWebhookRequest::ERROR_SILENT); 42 return; 43 } 44 45 $hook = $request->getWebhook(); 46 47 if ($hook->isDisabled()) { 48 $this->failRequest( 49 $request, 50 HeraldWebhookRequest::ERRORTYPE_HOOK, 51 HeraldWebhookRequest::ERROR_DISABLED); 52 throw new PhabricatorWorkerPermanentFailureException( 53 pht( 54 'Associated hook ("%s") for webhook request ("%s") is disabled.', 55 $hook->getPHID(), 56 $request_phid)); 57 } 58 59 $uri = $hook->getWebhookURI(); 60 try { 61 PhabricatorEnv::requireValidRemoteURIForFetch( 62 $uri, 63 array( 64 'http', 65 'https', 66 )); 67 } catch (Exception $ex) { 68 $this->failRequest( 69 $request, 70 HeraldWebhookRequest::ERRORTYPE_HOOK, 71 HeraldWebhookRequest::ERROR_URI); 72 throw new PhabricatorWorkerPermanentFailureException( 73 pht( 74 'Associated hook ("%s") for webhook request ("%s") has invalid '. 75 'fetch URI: %s', 76 $hook->getPHID(), 77 $request_phid, 78 $ex->getMessage())); 79 } 80 81 $object_phid = $request->getObjectPHID(); 82 83 $object = id(new PhabricatorObjectQuery()) 84 ->setViewer($viewer) 85 ->withPHIDs(array($object_phid)) 86 ->executeOne(); 87 if (!$object) { 88 $this->failRequest( 89 $request, 90 HeraldWebhookRequest::ERRORTYPE_HOOK, 91 HeraldWebhookRequest::ERROR_OBJECT); 92 93 throw new PhabricatorWorkerPermanentFailureException( 94 pht( 95 'Unable to load object ("%s") for webhook request ("%s").', 96 $object_phid, 97 $request_phid)); 98 } 99 100 $xaction_query = PhabricatorApplicationTransactionQuery::newQueryForObject( 101 $object); 102 $xaction_phids = $request->getTransactionPHIDs(); 103 if ($xaction_phids) { 104 $xactions = $xaction_query 105 ->setViewer($viewer) 106 ->withObjectPHIDs(array($object_phid)) 107 ->withPHIDs($xaction_phids) 108 ->execute(); 109 $xactions = mpull($xactions, null, 'getPHID'); 110 } else { 111 $xactions = array(); 112 } 113 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. 118 119 $lock_key = 'webhook('.$hook->getPHID().')'; 120 $lock = PhabricatorGlobalLock::newLock($lock_key); 121 122 try { 123 $lock->lock(); 124 } catch (Exception $ex) { 125 phlog($ex); 126 throw new PhabricatorWorkerYieldException(15); 127 } 128 129 $caught = null; 130 try { 131 $this->callWebhookWithLock($hook, $request, $object, $xactions); 132 } catch (Exception $ex) { 133 $caught = $ex; 134 } 135 136 $lock->unlock(); 137 138 if ($caught) { 139 throw $caught; 140 } 141 } 142 143 private function callWebhookWithLock( 144 HeraldWebhook $hook, 145 HeraldWebhookRequest $request, 146 $object, 147 array $xactions) { 148 $viewer = PhabricatorUser::getOmnipotentUser(); 149 150 if ($hook->isInErrorBackoff($viewer)) { 151 throw new PhabricatorWorkerYieldException($hook->getErrorBackoffWindow()); 152 } 153 154 $xaction_data = array(); 155 foreach ($xactions as $xaction) { 156 $xaction_data[] = array( 157 'phid' => $xaction->getPHID(), 158 ); 159 } 160 161 $trigger_data = array(); 162 foreach ($request->getTriggerPHIDs() as $trigger_phid) { 163 $trigger_data[] = array( 164 'phid' => $trigger_phid, 165 ); 166 } 167 168 $payload = array( 169 'object' => array( 170 'type' => phid_get_type($object->getPHID()), 171 'phid' => $object->getPHID(), 172 ), 173 'triggers' => $trigger_data, 174 'action' => array( 175 'test' => $request->getIsTestAction(), 176 'silent' => $request->getIsSilentAction(), 177 'secure' => $request->getIsSecureAction(), 178 'epoch' => (int)$request->getDateCreated(), 179 ), 180 'transactions' => $xaction_data, 181 ); 182 183 $payload = id(new PhutilJSON())->encodeFormatted($payload); 184 $key = $hook->getHmacKey(); 185 $signature = PhabricatorHash::digestHMACSHA256($payload, $key); 186 $uri = $hook->getWebhookURI(); 187 188 $future = id(new HTTPSFuture($uri)) 189 ->setMethod('POST') 190 ->addHeader('Content-Type', 'application/json') 191 ->addHeader('X-Phabricator-Webhook-Signature', $signature) 192 ->setTimeout(15) 193 ->setData($payload); 194 195 list($status) = $future->resolve(); 196 197 if ($status->isTimeout()) { 198 $error_type = HeraldWebhookRequest::ERRORTYPE_TIMEOUT; 199 } else { 200 $error_type = HeraldWebhookRequest::ERRORTYPE_HTTP; 201 } 202 $error_code = $status->getStatusCode(); 203 204 $request 205 ->setErrorType($error_type) 206 ->setErrorCode($error_code) 207 ->setLastRequestEpoch(PhabricatorTime::getNow()); 208 209 $retry_forever = HeraldWebhookRequest::RETRY_FOREVER; 210 if ($status->isTimeout() || $status->isError()) { 211 $should_retry = ($request->getRetryMode() === $retry_forever); 212 213 $request 214 ->setLastRequestResult(HeraldWebhookRequest::RESULT_FAIL); 215 216 if ($should_retry) { 217 $request->save(); 218 219 throw new Exception( 220 pht( 221 'Webhook request ("%s", to "%s") failed (%s / %s). The request '. 222 'will be retried.', 223 $request->getPHID(), 224 $uri, 225 $error_type, 226 $error_code)); 227 } else { 228 $request 229 ->setStatus(HeraldWebhookRequest::STATUS_FAILED) 230 ->save(); 231 232 throw new PhabricatorWorkerPermanentFailureException( 233 pht( 234 'Webhook request ("%s", to "%s") failed (%s / %s). The request '. 235 'will not be retried.', 236 $request->getPHID(), 237 $uri, 238 $error_type, 239 $error_code)); 240 } 241 } else { 242 $request 243 ->setLastRequestResult(HeraldWebhookRequest::RESULT_OKAY) 244 ->setStatus(HeraldWebhookRequest::STATUS_SENT) 245 ->save(); 246 } 247 } 248 249 private function failRequest( 250 HeraldWebhookRequest $request, 251 $error_type, 252 $error_code) { 253 254 $request 255 ->setStatus(HeraldWebhookRequest::STATUS_FAILED) 256 ->setErrorType($error_type) 257 ->setErrorCode($error_code) 258 ->setLastRequestResult(HeraldWebhookRequest::RESULT_NONE) 259 ->setLastRequestEpoch(0) 260 ->save(); 261 } 262 263}