@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

Improve Asana API error handling in Doorkeeper

Summary:
Ref T2852. We need to distinguish between an API call which worked but got back nothing (404) and an API call which failed.

In particular, Asana hit a sync issue which was likely the result of treating a 500 (or some other error) as a 404.

Also clean up a couple small things.

Test Plan: Ran syncs against deleted tasks and saw successful syncs of non-tasks, and simulated random failures and saw them get handled correctly.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2852

Differential Revision: https://secure.phabricator.com/D6470

+56 -26
+3 -2
src/applications/differential/query/DifferentialRevisionQuery.php
··· 941 941 foreach ($revision_edges as $user_phid => $edge) { 942 942 $data = $edge['data']; 943 943 $reviewers[] = new DifferentialReviewer( 944 - $user_phid, $data['status'], idx($data, 'diff', null) 945 - ); 944 + $user_phid, 945 + idx($data, 'status'), 946 + idx($data, 'diff')); 946 947 } 947 948 948 949 $revision->attachReviewerStatus($reviewers);
+17 -1
src/applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php
··· 64 64 } 65 65 66 66 $results = array(); 67 + $failed = array(); 67 68 foreach (Futures($futures) as $key => $future) { 68 69 try { 69 70 $results[$key] = $future->resolve(); 70 71 } catch (Exception $ex) { 71 - // TODO: For now, ignore this stuff. 72 + if (($ex instanceof HTTPFutureResponseStatus) && 73 + ($ex->getStatusCode() == 404)) { 74 + // This indicates that the object has been deleted (or never existed, 75 + // or isn't visible to the current user) but it's a successful sync of 76 + // an object which isn't visible. 77 + } else { 78 + // This is something else, so consider it a synchronization failure. 79 + phlog($ex); 80 + $failed[$key] = $ex; 81 + } 72 82 } 73 83 } 74 84 75 85 foreach ($refs as $ref) { 76 86 $ref->setAttribute('name', pht('Asana Task %s', $ref->getObjectID())); 87 + 88 + $failed = idx($failed, $ref->getObjectKey()); 89 + if ($failed) { 90 + $ref->setSyncFailed(true); 91 + continue; 92 + } 77 93 78 94 $result = idx($results, $ref->getObjectKey()); 79 95 if (!$result) {
+10
src/applications/doorkeeper/engine/DoorkeeperObjectRef.php
··· 9 9 private $objectID; 10 10 private $attributes = array(); 11 11 private $isVisible; 12 + private $syncFailed; 12 13 private $externalObject; 13 14 14 15 public function newExternalObject() { ··· 41 42 42 43 public function getIsVisible() { 43 44 return $this->isVisible; 45 + } 46 + 47 + public function setSyncFailed($sync_failed) { 48 + $this->syncFailed = $sync_failed; 49 + return $this; 50 + } 51 + 52 + public function getSyncFailed() { 53 + return $this->syncFailed; 44 54 } 45 55 46 56 public function getAttribute($key, $default = null) {
+26 -23
src/applications/doorkeeper/worker/DoorkeeperFeedWorkerAsana.php
··· 185 185 $phids = $this->getRelatedUserPHIDs($object); 186 186 list($owner_phid, $active_phids, $passive_phids, $follow_phids) = $phids; 187 187 188 - $all_follow_phids = array_merge( 188 + $all_phids = array(); 189 + $all_phids = array_merge( 190 + array($owner_phid), 189 191 $active_phids, 190 192 $passive_phids, 191 193 $follow_phids); 192 - $all_follow_phids = array_unique(array_filter($all_follow_phids)); 193 - 194 - $all_phids = array(); 195 - $all_phids = array_merge( 196 - array($owner_phid), 197 - $all_follow_phids); 198 194 $all_phids = array_unique(array_filter($all_phids)); 199 195 200 196 $phid_aid_map = $this->lookupAsanaUserIDs($all_phids); 201 - 202 197 if (!$phid_aid_map) { 203 198 throw new PhabricatorWorkerPermanentFailureException( 204 199 'No related users have linked Asana accounts.'); 205 200 } 206 201 207 202 $owner_asana_id = idx($phid_aid_map, $owner_phid); 208 - $all_follow_asana_ids = array_select_keys($phid_aid_map, $all_follow_phids); 209 - $all_follow_asana_ids = array_values($all_follow_asana_ids); 203 + $all_asana_ids = array_select_keys($phid_aid_map, $all_phids); 204 + $all_asana_ids = array_values($all_asana_ids); 210 205 211 206 // Even if the actor isn't a reviewer, etc., try to use their account so 212 207 // we can post in the correct voice. If we miss, we'll try all the other ··· 243 238 244 239 $main_data = $this->getAsanaTaskData($object) + array( 245 240 'assignee' => $owner_asana_id, 246 - 'followers' => $all_follow_asana_ids, 241 + 'followers' => $all_asana_ids, 247 242 ); 248 243 249 244 $extra_data = array(); ··· 261 256 'DoorkeeperExternalObject could not be loaded.'); 262 257 } 263 258 264 - if (!$parent_ref->getIsVisible()) { 259 + if ($parent_ref->getSyncFailed()) { 260 + throw new Exception( 261 + 'Synchronization of parent task from Asana failed!'); 262 + } else if (!$parent_ref->getIsVisible()) { 265 263 $this->log("Skipping main task update, object is no longer visible.\n"); 266 264 $extra_data['gone'] = true; 267 265 } else { ··· 341 339 'this likely indicates the Asana task has been deleted.'); 342 340 } 343 341 344 - // Post the feed story itself to the main Asana task. 345 - 346 - $this->makeAsanaAPICall( 347 - $oauth_token, 348 - 'tasks/'.$parent_ref->getObjectID().'/stories', 349 - 'POST', 350 - array( 351 - 'text' => $story->renderText(), 352 - )); 353 - 354 - 355 342 // Now, handle the subtasks. 356 343 357 344 $sub_editor = id(new PhabricatorEdgeEditor()) ··· 371 358 ->execute(); 372 359 373 360 foreach ($refs as $ref) { 361 + if ($ref->getSyncFailed()) { 362 + throw new Exception( 363 + 'Synchronization of child task from Asana failed!'); 364 + } 374 365 if (!$ref->getIsVisible()) { 375 366 $ref->getExternalObject()->delete(); 376 367 continue; ··· 497 488 498 489 $sub_editor->save(); 499 490 491 + 492 + // Post the feed story itself to the main Asana task. We do this last 493 + // because everything else is idempotent, so this is the only effect we 494 + // can't safely run more than once. 495 + 496 + $this->makeAsanaAPICall( 497 + $oauth_token, 498 + 'tasks/'.$parent_ref->getObjectID().'/stories', 499 + 'POST', 500 + array( 501 + 'text' => $story->renderText(), 502 + )); 500 503 } 501 504 502 505 private function lookupAsanaUserIDs($all_phids) {