Path: blob/master/src/applications/nuance/cursor/NuanceGitHubImportCursor.php
12256 views
<?php12abstract class NuanceGitHubImportCursor3extends NuanceImportCursor {45abstract protected function getGitHubAPIEndpointURI($user, $repository);6abstract protected function newNuanceItemFromGitHubRecord(array $record);78protected function getMaximumPage() {9return 100;10}1112protected function getPageSize() {13return 100;14}1516protected function getMinimumDelayBetweenPolls() {17// Even if GitHub says we can, don't poll more than once every few seconds.18// In particular, the Issue Events API does not advertise a poll interval19// in a header.20return 5;21}2223final protected function shouldPullDataFromSource() {24$now = PhabricatorTime::getNow();2526// Respect GitHub's poll interval header. If we made a request recently,27// don't make another one until we've waited long enough.28$ttl = $this->getCursorProperty('github.poll.ttl');29if ($ttl && ($ttl >= $now)) {30$this->logInfo(31pht(32'Respecting "%s" or minimum poll delay: waiting for %s second(s) '.33'to poll GitHub.',34'X-Poll-Interval',35new PhutilNumber(1 + ($ttl - $now))));3637return false;38}3940// Respect GitHub's API rate limiting. If we've exceeded the rate limit,41// wait until it resets to try again.42$limit = $this->getCursorProperty('github.limit.ttl');43if ($limit && ($limit >= $now)) {44$this->logInfo(45pht(46'Respecting "%s": waiting for %s second(s) to poll GitHub.',47'X-RateLimit-Reset',48new PhutilNumber(1 + ($limit - $now))));49return false;50}5152return true;53}5455final protected function pullDataFromSource() {56$viewer = $this->getViewer();57$now = PhabricatorTime::getNow();5859$source = $this->getSource();6061$user = $source->getSourceProperty('github.user');62$repository = $source->getSourceProperty('github.repository');63$api_token = $source->getSourceProperty('github.token');6465// This API only supports fetching 10 pages of 30 events each, for a total66// of 300 events.67$etag = null;68$new_items = array();69$hit_known_items = false;7071$max_page = $this->getMaximumPage();72$page_size = $this->getPageSize();7374for ($page = 1; $page <= $max_page; $page++) {75$uri = $this->getGitHubAPIEndpointURI($user, $repository);7677$data = array(78'page' => $page,79'per_page' => $page_size,80);8182$future = id(new PhutilGitHubFuture())83->setAccessToken($api_token)84->setRawGitHubQuery($uri, $data);8586if ($page == 1) {87$cursor_etag = $this->getCursorProperty('github.poll.etag');88if ($cursor_etag) {89$future->addHeader('If-None-Match', $cursor_etag);90}91}9293$this->logInfo(94pht(95'Polling GitHub Repository API endpoint "%s".',96$uri));97$response = $future->resolve();9899// Do this first: if we hit the rate limit, we get a response but the100// body isn't valid.101$this->updateRateLimits($response);102103if ($response->getStatus()->getStatusCode() == 304) {104$this->logInfo(105pht(106'Received a 304 Not Modified from GitHub, no new events.'));107}108109// This means we hit a rate limit or a "Not Modified" because of the110// "ETag" header. In either case, we should bail out.111if ($response->getStatus()->isError()) {112$this->updatePolling($response, $now, false);113$this->getCursorData()->save();114return false;115}116117if ($page == 1) {118$etag = $response->getHeaderValue('ETag');119}120121$records = $response->getBody();122foreach ($records as $record) {123$item = $this->newNuanceItemFromGitHubRecord($record);124$item_key = $item->getItemKey();125126$this->logInfo(127pht(128'Fetched event "%s".',129$item_key));130131$new_items[$item->getItemKey()] = $item;132}133134if ($new_items) {135$existing = id(new NuanceItemQuery())136->setViewer($viewer)137->withSourcePHIDs(array($source->getPHID()))138->withItemKeys(array_keys($new_items))139->execute();140$existing = mpull($existing, null, 'getItemKey');141foreach ($new_items as $key => $new_item) {142if (isset($existing[$key])) {143unset($new_items[$key]);144$hit_known_items = true;145146$this->logInfo(147pht(148'Event "%s" is previously known.',149$key));150}151}152}153154if ($hit_known_items) {155break;156}157158if (count($records) < $page_size) {159break;160}161}162163// TODO: When we go through the whole queue without hitting anything we164// have seen before, we should record some sort of global event so we165// can tell the user when the bridging started or was interrupted?166if (!$hit_known_items) {167$already_polled = $this->getCursorProperty('github.polled');168if ($already_polled) {169// TODO: This is bad: we missed some items, maybe because too much170// stuff happened too fast or the daemons were broken for a long171// time.172} else {173// TODO: This is OK, we're doing the initial import.174}175}176177if ($etag !== null) {178$this->updateETag($etag);179}180181$this->updatePolling($response, $now, true);182183// Reverse the new items so we insert them in chronological order.184$new_items = array_reverse($new_items);185186$source->openTransaction();187foreach ($new_items as $new_item) {188$new_item->save();189}190$this->getCursorData()->save();191$source->saveTransaction();192193foreach ($new_items as $new_item) {194$new_item->scheduleUpdate();195}196197return false;198}199200private function updateRateLimits(PhutilGitHubResponse $response) {201$remaining = $response->getHeaderValue('X-RateLimit-Remaining');202$limit_reset = $response->getHeaderValue('X-RateLimit-Reset');203$now = PhabricatorTime::getNow();204205$limit_ttl = null;206if (strlen($remaining)) {207$remaining = (int)$remaining;208if (!$remaining) {209$limit_ttl = (int)$limit_reset;210}211}212213$this->setCursorProperty('github.limit.ttl', $limit_ttl);214215$this->logInfo(216pht(217'This key has %s remaining API request(s), '.218'limit resets in %s second(s).',219new PhutilNumber($remaining),220new PhutilNumber($limit_reset - $now)));221}222223private function updateETag($etag) {224225$this->setCursorProperty('github.poll.etag', $etag);226227$this->logInfo(228pht(229'ETag for this request was "%s".',230$etag));231}232233private function updatePolling(234PhutilGitHubResponse $response,235$start,236$success) {237238if ($success) {239$this->setCursorProperty('github.polled', true);240}241242$poll_interval = (int)$response->getHeaderValue('X-Poll-Interval');243$poll_interval = max($this->getMinimumDelayBetweenPolls(), $poll_interval);244245$poll_ttl = $start + $poll_interval;246$this->setCursorProperty('github.poll.ttl', $poll_ttl);247248$now = PhabricatorTime::getNow();249250$this->logInfo(251pht(252'Set API poll TTL to +%s second(s) (%s second(s) from now).',253new PhutilNumber($poll_interval),254new PhutilNumber($poll_ttl - $now)));255}256257}258259260