Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/nuance/cursor/NuanceGitHubImportCursor.php
12256 views
1
<?php
2
3
abstract class NuanceGitHubImportCursor
4
extends NuanceImportCursor {
5
6
abstract protected function getGitHubAPIEndpointURI($user, $repository);
7
abstract protected function newNuanceItemFromGitHubRecord(array $record);
8
9
protected function getMaximumPage() {
10
return 100;
11
}
12
13
protected function getPageSize() {
14
return 100;
15
}
16
17
protected function getMinimumDelayBetweenPolls() {
18
// Even if GitHub says we can, don't poll more than once every few seconds.
19
// In particular, the Issue Events API does not advertise a poll interval
20
// in a header.
21
return 5;
22
}
23
24
final protected function shouldPullDataFromSource() {
25
$now = PhabricatorTime::getNow();
26
27
// Respect GitHub's poll interval header. If we made a request recently,
28
// don't make another one until we've waited long enough.
29
$ttl = $this->getCursorProperty('github.poll.ttl');
30
if ($ttl && ($ttl >= $now)) {
31
$this->logInfo(
32
pht(
33
'Respecting "%s" or minimum poll delay: waiting for %s second(s) '.
34
'to poll GitHub.',
35
'X-Poll-Interval',
36
new PhutilNumber(1 + ($ttl - $now))));
37
38
return false;
39
}
40
41
// Respect GitHub's API rate limiting. If we've exceeded the rate limit,
42
// wait until it resets to try again.
43
$limit = $this->getCursorProperty('github.limit.ttl');
44
if ($limit && ($limit >= $now)) {
45
$this->logInfo(
46
pht(
47
'Respecting "%s": waiting for %s second(s) to poll GitHub.',
48
'X-RateLimit-Reset',
49
new PhutilNumber(1 + ($limit - $now))));
50
return false;
51
}
52
53
return true;
54
}
55
56
final protected function pullDataFromSource() {
57
$viewer = $this->getViewer();
58
$now = PhabricatorTime::getNow();
59
60
$source = $this->getSource();
61
62
$user = $source->getSourceProperty('github.user');
63
$repository = $source->getSourceProperty('github.repository');
64
$api_token = $source->getSourceProperty('github.token');
65
66
// This API only supports fetching 10 pages of 30 events each, for a total
67
// of 300 events.
68
$etag = null;
69
$new_items = array();
70
$hit_known_items = false;
71
72
$max_page = $this->getMaximumPage();
73
$page_size = $this->getPageSize();
74
75
for ($page = 1; $page <= $max_page; $page++) {
76
$uri = $this->getGitHubAPIEndpointURI($user, $repository);
77
78
$data = array(
79
'page' => $page,
80
'per_page' => $page_size,
81
);
82
83
$future = id(new PhutilGitHubFuture())
84
->setAccessToken($api_token)
85
->setRawGitHubQuery($uri, $data);
86
87
if ($page == 1) {
88
$cursor_etag = $this->getCursorProperty('github.poll.etag');
89
if ($cursor_etag) {
90
$future->addHeader('If-None-Match', $cursor_etag);
91
}
92
}
93
94
$this->logInfo(
95
pht(
96
'Polling GitHub Repository API endpoint "%s".',
97
$uri));
98
$response = $future->resolve();
99
100
// Do this first: if we hit the rate limit, we get a response but the
101
// body isn't valid.
102
$this->updateRateLimits($response);
103
104
if ($response->getStatus()->getStatusCode() == 304) {
105
$this->logInfo(
106
pht(
107
'Received a 304 Not Modified from GitHub, no new events.'));
108
}
109
110
// This means we hit a rate limit or a "Not Modified" because of the
111
// "ETag" header. In either case, we should bail out.
112
if ($response->getStatus()->isError()) {
113
$this->updatePolling($response, $now, false);
114
$this->getCursorData()->save();
115
return false;
116
}
117
118
if ($page == 1) {
119
$etag = $response->getHeaderValue('ETag');
120
}
121
122
$records = $response->getBody();
123
foreach ($records as $record) {
124
$item = $this->newNuanceItemFromGitHubRecord($record);
125
$item_key = $item->getItemKey();
126
127
$this->logInfo(
128
pht(
129
'Fetched event "%s".',
130
$item_key));
131
132
$new_items[$item->getItemKey()] = $item;
133
}
134
135
if ($new_items) {
136
$existing = id(new NuanceItemQuery())
137
->setViewer($viewer)
138
->withSourcePHIDs(array($source->getPHID()))
139
->withItemKeys(array_keys($new_items))
140
->execute();
141
$existing = mpull($existing, null, 'getItemKey');
142
foreach ($new_items as $key => $new_item) {
143
if (isset($existing[$key])) {
144
unset($new_items[$key]);
145
$hit_known_items = true;
146
147
$this->logInfo(
148
pht(
149
'Event "%s" is previously known.',
150
$key));
151
}
152
}
153
}
154
155
if ($hit_known_items) {
156
break;
157
}
158
159
if (count($records) < $page_size) {
160
break;
161
}
162
}
163
164
// TODO: When we go through the whole queue without hitting anything we
165
// have seen before, we should record some sort of global event so we
166
// can tell the user when the bridging started or was interrupted?
167
if (!$hit_known_items) {
168
$already_polled = $this->getCursorProperty('github.polled');
169
if ($already_polled) {
170
// TODO: This is bad: we missed some items, maybe because too much
171
// stuff happened too fast or the daemons were broken for a long
172
// time.
173
} else {
174
// TODO: This is OK, we're doing the initial import.
175
}
176
}
177
178
if ($etag !== null) {
179
$this->updateETag($etag);
180
}
181
182
$this->updatePolling($response, $now, true);
183
184
// Reverse the new items so we insert them in chronological order.
185
$new_items = array_reverse($new_items);
186
187
$source->openTransaction();
188
foreach ($new_items as $new_item) {
189
$new_item->save();
190
}
191
$this->getCursorData()->save();
192
$source->saveTransaction();
193
194
foreach ($new_items as $new_item) {
195
$new_item->scheduleUpdate();
196
}
197
198
return false;
199
}
200
201
private function updateRateLimits(PhutilGitHubResponse $response) {
202
$remaining = $response->getHeaderValue('X-RateLimit-Remaining');
203
$limit_reset = $response->getHeaderValue('X-RateLimit-Reset');
204
$now = PhabricatorTime::getNow();
205
206
$limit_ttl = null;
207
if (strlen($remaining)) {
208
$remaining = (int)$remaining;
209
if (!$remaining) {
210
$limit_ttl = (int)$limit_reset;
211
}
212
}
213
214
$this->setCursorProperty('github.limit.ttl', $limit_ttl);
215
216
$this->logInfo(
217
pht(
218
'This key has %s remaining API request(s), '.
219
'limit resets in %s second(s).',
220
new PhutilNumber($remaining),
221
new PhutilNumber($limit_reset - $now)));
222
}
223
224
private function updateETag($etag) {
225
226
$this->setCursorProperty('github.poll.etag', $etag);
227
228
$this->logInfo(
229
pht(
230
'ETag for this request was "%s".',
231
$etag));
232
}
233
234
private function updatePolling(
235
PhutilGitHubResponse $response,
236
$start,
237
$success) {
238
239
if ($success) {
240
$this->setCursorProperty('github.polled', true);
241
}
242
243
$poll_interval = (int)$response->getHeaderValue('X-Poll-Interval');
244
$poll_interval = max($this->getMinimumDelayBetweenPolls(), $poll_interval);
245
246
$poll_ttl = $start + $poll_interval;
247
$this->setCursorProperty('github.poll.ttl', $poll_ttl);
248
249
$now = PhabricatorTime::getNow();
250
251
$this->logInfo(
252
pht(
253
'Set API poll TTL to +%s second(s) (%s second(s) from now).',
254
new PhutilNumber($poll_interval),
255
new PhutilNumber($poll_ttl - $now)));
256
}
257
258
}
259
260