/src/main/java/org/kohsuke/github/GHRateLimit.java

http://github.com/kohsuke/github-api · Java · 635 lines · 281 code · 63 blank · 291 comment · 65 complexity · 18a2499363efeaba22cdad505e35833b MD5 · raw file

  1. package org.kohsuke.github;
  2. import com.fasterxml.jackson.annotation.JacksonInject;
  3. import com.fasterxml.jackson.annotation.JsonCreator;
  4. import com.fasterxml.jackson.annotation.JsonProperty;
  5. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  6. import org.apache.commons.lang3.StringUtils;
  7. import org.kohsuke.github.connector.GitHubConnectorResponse;
  8. import java.time.Duration;
  9. import java.time.ZonedDateTime;
  10. import java.time.format.DateTimeFormatter;
  11. import java.time.format.DateTimeParseException;
  12. import java.util.Date;
  13. import java.util.Objects;
  14. import java.util.concurrent.atomic.AtomicReference;
  15. import java.util.logging.Logger;
  16. import javax.annotation.CheckForNull;
  17. import javax.annotation.Nonnull;
  18. import static java.util.logging.Level.FINEST;
  19. /**
  20. * Rate limit.
  21. *
  22. * @author Kohsuke Kawaguchi
  23. */
  24. @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "JSON API")
  25. public class GHRateLimit {
  26. /**
  27. * Remaining calls that can be made.
  28. *
  29. * @deprecated This field should never have been made public. Use {@link #getRemaining()}
  30. */
  31. @Deprecated
  32. public int remaining;
  33. /**
  34. * Allotted API call per hour.
  35. *
  36. * @deprecated This field should never have been made public. Use {@link #getLimit()}
  37. */
  38. @Deprecated
  39. public int limit;
  40. /**
  41. * The time at which the current rate limit window resets in UTC epoch seconds. WARNING: this field was implemented
  42. * using {@link Date#Date(long)} which expects UTC epoch milliseconds, so this Date instance is meaningless as a
  43. * date. To use this field in any meaningful way, it must be converted to a long using {@link Date#getTime()}
  44. * multiplied by 1000.
  45. *
  46. * @deprecated This field should never have been made public. Use {@link #getResetDate()}
  47. */
  48. @Deprecated
  49. public Date reset;
  50. @Nonnull
  51. private final Record core;
  52. @Nonnull
  53. private final Record search;
  54. @Nonnull
  55. private final Record graphql;
  56. @Nonnull
  57. private final Record integrationManifest;
  58. /**
  59. * The default GHRateLimit provided to new {@link GitHubClient}s.
  60. *
  61. * Contains all expired records that will cause {@link GitHubClient#rateLimit(RateLimitTarget)} to refresh with new
  62. * data when called.
  63. *
  64. * Private, but made internal for testing.
  65. */
  66. @Nonnull
  67. static final GHRateLimit DEFAULT = new GHRateLimit(UnknownLimitRecord.DEFAULT,
  68. UnknownLimitRecord.DEFAULT,
  69. UnknownLimitRecord.DEFAULT,
  70. UnknownLimitRecord.DEFAULT);
  71. /**
  72. * Creates a new {@link GHRateLimit} from a single record for the specified endpoint with place holders for other
  73. * records.
  74. *
  75. * This is used to create {@link GHRateLimit} instances that can merged with other instances.
  76. *
  77. * @param record
  78. * the rate limit record. Can be a regular {@link Record} constructed from header information or an
  79. * {@link UnknownLimitRecord} placeholder.
  80. * @param rateLimitTarget
  81. * which rate limit record to fill
  82. * @return a new {@link GHRateLimit} instance containing the supplied record
  83. */
  84. @Nonnull
  85. static GHRateLimit fromRecord(@Nonnull Record record, @Nonnull RateLimitTarget rateLimitTarget) {
  86. if (rateLimitTarget == RateLimitTarget.CORE || rateLimitTarget == RateLimitTarget.NONE) {
  87. return new GHRateLimit(record,
  88. UnknownLimitRecord.DEFAULT,
  89. UnknownLimitRecord.DEFAULT,
  90. UnknownLimitRecord.DEFAULT);
  91. } else if (rateLimitTarget == RateLimitTarget.SEARCH) {
  92. return new GHRateLimit(UnknownLimitRecord.DEFAULT,
  93. record,
  94. UnknownLimitRecord.DEFAULT,
  95. UnknownLimitRecord.DEFAULT);
  96. } else if (rateLimitTarget == RateLimitTarget.GRAPHQL) {
  97. return new GHRateLimit(UnknownLimitRecord.DEFAULT,
  98. UnknownLimitRecord.DEFAULT,
  99. record,
  100. UnknownLimitRecord.DEFAULT);
  101. } else if (rateLimitTarget == RateLimitTarget.INTEGRATION_MANIFEST) {
  102. return new GHRateLimit(UnknownLimitRecord.DEFAULT,
  103. UnknownLimitRecord.DEFAULT,
  104. UnknownLimitRecord.DEFAULT,
  105. record);
  106. } else {
  107. throw new IllegalArgumentException("Unknown rate limit target: " + rateLimitTarget.toString());
  108. }
  109. }
  110. @JsonCreator
  111. GHRateLimit(@Nonnull @JsonProperty("core") Record core,
  112. @Nonnull @JsonProperty("search") Record search,
  113. @Nonnull @JsonProperty("graphql") Record graphql,
  114. @Nonnull @JsonProperty("integration_manifest") Record integrationManifest) {
  115. // The Nonnull annotation is ignored by Jackson, we have to check manually
  116. Objects.requireNonNull(core);
  117. Objects.requireNonNull(search);
  118. Objects.requireNonNull(graphql);
  119. Objects.requireNonNull(integrationManifest);
  120. this.core = core;
  121. this.search = search;
  122. this.graphql = graphql;
  123. this.integrationManifest = integrationManifest;
  124. // Deprecated fields
  125. this.remaining = core.getRemaining();
  126. this.limit = core.getLimit();
  127. // This is wrong but is how this was implemented. Kept for backward compat.
  128. this.reset = new Date(core.getResetEpochSeconds());
  129. }
  130. /**
  131. * Returns the date at which the Core API rate limit will reset.
  132. *
  133. * @return the calculated date at which the rate limit has or will reset.
  134. */
  135. @Nonnull
  136. public Date getResetDate() {
  137. return getCore().getResetDate();
  138. }
  139. /**
  140. * Gets the remaining number of Core APIs requests allowed before this connection will be throttled.
  141. *
  142. * @return an integer
  143. * @since 1.100
  144. */
  145. public int getRemaining() {
  146. return getCore().getRemaining();
  147. }
  148. /**
  149. * Gets the total number of Core API calls per hour allotted for this connection.
  150. *
  151. * @return an integer
  152. * @since 1.100
  153. */
  154. public int getLimit() {
  155. return getCore().getLimit();
  156. }
  157. /**
  158. * Gets the time in epoch seconds when the Core API rate limit will reset.
  159. *
  160. * @return a long
  161. * @since 1.100
  162. */
  163. public long getResetEpochSeconds() {
  164. return getCore().getResetEpochSeconds();
  165. }
  166. /**
  167. * Whether the reset date for the Core API rate limit has passed.
  168. *
  169. * @return true if the rate limit reset date has passed. Otherwise false.
  170. * @since 1.100
  171. */
  172. public boolean isExpired() {
  173. return getCore().isExpired();
  174. }
  175. /**
  176. * The core object provides the rate limit status for all non-search-related resources in the REST API.
  177. *
  178. * @return a rate limit record
  179. * @since 1.100
  180. */
  181. @Nonnull
  182. public Record getCore() {
  183. return core;
  184. }
  185. /**
  186. * The search record provides the rate limit status for the Search API.
  187. *
  188. * @return a rate limit record
  189. * @since 1.115
  190. */
  191. @Nonnull
  192. public Record getSearch() {
  193. return search;
  194. }
  195. /**
  196. * The graphql record provides the rate limit status for the GraphQL API.
  197. *
  198. * @return a rate limit record
  199. * @since 1.115
  200. */
  201. @Nonnull
  202. public Record getGraphQL() {
  203. return graphql;
  204. }
  205. /**
  206. * The integration manifest record provides the rate limit status for the GitHub App Manifest code conversion
  207. * endpoint.
  208. *
  209. * @return a rate limit record
  210. * @since 1.115
  211. */
  212. @Nonnull
  213. public Record getIntegrationManifest() {
  214. return integrationManifest;
  215. }
  216. @Override
  217. public String toString() {
  218. return "GHRateLimit {" + "core " + getCore().toString() + ", search " + getSearch().toString() + ", graphql "
  219. + getGraphQL().toString() + ", integrationManifest " + getIntegrationManifest().toString() + "}";
  220. }
  221. @Override
  222. public boolean equals(Object o) {
  223. if (this == o) {
  224. return true;
  225. }
  226. if (o == null || getClass() != o.getClass()) {
  227. return false;
  228. }
  229. GHRateLimit rateLimit = (GHRateLimit) o;
  230. return getCore().equals(rateLimit.getCore()) && getSearch().equals(rateLimit.getSearch())
  231. && getGraphQL().equals(rateLimit.getGraphQL())
  232. && getIntegrationManifest().equals(rateLimit.getIntegrationManifest());
  233. }
  234. @Override
  235. public int hashCode() {
  236. return Objects.hash(getCore(), getSearch(), getGraphQL(), getIntegrationManifest());
  237. }
  238. /**
  239. * Merge a {@link GHRateLimit} with another one to create a new {@link GHRateLimit} keeping the latest
  240. * {@link Record}s from each.
  241. *
  242. * @param newLimit
  243. * {@link GHRateLimit} with potentially updated {@link Record}s.
  244. * @return a merged {@link GHRateLimit} with the latest {@link Record}s from these two instances. If the merged
  245. * instance is equal to the current instance, the current instance is returned.
  246. */
  247. @Nonnull
  248. GHRateLimit getMergedRateLimit(@Nonnull GHRateLimit newLimit) {
  249. GHRateLimit merged = new GHRateLimit(getCore().currentOrUpdated(newLimit.getCore()),
  250. getSearch().currentOrUpdated(newLimit.getSearch()),
  251. getGraphQL().currentOrUpdated(newLimit.getGraphQL()),
  252. getIntegrationManifest().currentOrUpdated(newLimit.getIntegrationManifest()));
  253. if (merged.equals(this)) {
  254. merged = this;
  255. }
  256. return merged;
  257. }
  258. /**
  259. * Gets the specified {@link Record}.
  260. *
  261. * {@link RateLimitTarget#NONE} will return {@link UnknownLimitRecord#DEFAULT} to prevent any clients from
  262. * accidentally waiting on that record to reset before continuing.
  263. *
  264. * @param rateLimitTarget
  265. * the target rate limit record
  266. * @return the target {@link Record} from this instance.
  267. */
  268. @Nonnull
  269. Record getRecord(@Nonnull RateLimitTarget rateLimitTarget) {
  270. if (rateLimitTarget == RateLimitTarget.CORE) {
  271. return getCore();
  272. } else if (rateLimitTarget == RateLimitTarget.SEARCH) {
  273. return getSearch();
  274. } else if (rateLimitTarget == RateLimitTarget.GRAPHQL) {
  275. return getGraphQL();
  276. } else if (rateLimitTarget == RateLimitTarget.INTEGRATION_MANIFEST) {
  277. return getIntegrationManifest();
  278. } else if (rateLimitTarget == RateLimitTarget.NONE) {
  279. return UnknownLimitRecord.DEFAULT;
  280. } else {
  281. throw new IllegalArgumentException("Unknown rate limit target: " + rateLimitTarget.toString());
  282. }
  283. }
  284. /**
  285. * A limit record used as a placeholder when the the actual limit is not known.
  286. *
  287. * @since 1.100
  288. */
  289. public static class UnknownLimitRecord extends Record {
  290. private static final long defaultUnknownLimitResetSeconds = Duration.ofSeconds(30).getSeconds();
  291. /**
  292. * The number of seconds until a {@link UnknownLimitRecord} will expire.
  293. *
  294. * This is set to a somewhat short duration, rather than a long one. This avoids
  295. * {@link {@link GitHubClient#rateLimit(RateLimitTarget)}} requesting rate limit updates continuously, but also
  296. * avoids holding on to stale unknown records indefinitely.
  297. *
  298. * When merging {@link GHRateLimit} instances, {@link UnknownLimitRecord}s will be superseded by incoming
  299. * regular {@link Record}s.
  300. *
  301. * @see GHRateLimit#getMergedRateLimit(GHRateLimit)
  302. */
  303. static long unknownLimitResetSeconds = defaultUnknownLimitResetSeconds;
  304. static final int unknownLimit = 1000000;
  305. static final int unknownRemaining = 999999;
  306. // The default UnknownLimitRecord is an expired record.
  307. private static final UnknownLimitRecord DEFAULT = new UnknownLimitRecord(Long.MIN_VALUE);
  308. // The starting current UnknownLimitRecord is an expired record.
  309. private static final AtomicReference<UnknownLimitRecord> current = new AtomicReference<>(DEFAULT);
  310. /**
  311. * Create a new unknown record that resets at the specified time.
  312. *
  313. * @param resetEpochSeconds
  314. * the epoch second time when this record will expire.
  315. */
  316. private UnknownLimitRecord(long resetEpochSeconds) {
  317. super(unknownLimit, unknownRemaining, resetEpochSeconds);
  318. }
  319. static Record current() {
  320. Record result = current.get();
  321. if (result.isExpired()) {
  322. current.set(new UnknownLimitRecord(System.currentTimeMillis() / 1000L + unknownLimitResetSeconds));
  323. result = current.get();
  324. }
  325. return result;
  326. }
  327. /**
  328. * Reset the current UnknownLimitRecord. For use during testing only.
  329. */
  330. static void reset() {
  331. current.set(DEFAULT);
  332. unknownLimitResetSeconds = defaultUnknownLimitResetSeconds;
  333. }
  334. }
  335. /**
  336. * A rate limit record.
  337. *
  338. * @since 1.100
  339. */
  340. public static class Record {
  341. /**
  342. * Remaining calls that can be made.
  343. */
  344. private final int remaining;
  345. /**
  346. * Allotted API call per time period.
  347. */
  348. private final int limit;
  349. /**
  350. * The time at which the current rate limit window resets in UTC epoch seconds.
  351. */
  352. private final long resetEpochSeconds;
  353. /**
  354. * EpochSeconds time (UTC) at which this instance was created.
  355. */
  356. private final long createdAtEpochSeconds = System.currentTimeMillis() / 1000;
  357. /**
  358. * The date at which the rate limit will reset, adjusted to local machine time if the local machine's clock not
  359. * synchronized with to the same clock as the GitHub server.
  360. *
  361. * @see #calculateResetDate(String)
  362. * @see #getResetDate()
  363. */
  364. @Nonnull
  365. private final Date resetDate;
  366. /**
  367. * Instantiates a new Record.
  368. *
  369. * @param limit
  370. * the limit
  371. * @param remaining
  372. * the remaining
  373. * @param resetEpochSeconds
  374. * the reset epoch seconds
  375. */
  376. public Record(@JsonProperty(value = "limit", required = true) int limit,
  377. @JsonProperty(value = "remaining", required = true) int remaining,
  378. @JsonProperty(value = "reset", required = true) long resetEpochSeconds) {
  379. this(limit, remaining, resetEpochSeconds, null);
  380. }
  381. /**
  382. * Instantiates a new Record. Called by Jackson data binding or during header parsing.
  383. *
  384. * @param limit
  385. * the limit
  386. * @param remaining
  387. * the remaining
  388. * @param resetEpochSeconds
  389. * the reset epoch seconds
  390. * @param connectorResponse
  391. * the response info
  392. */
  393. @JsonCreator
  394. Record(@JsonProperty(value = "limit", required = true) int limit,
  395. @JsonProperty(value = "remaining", required = true) int remaining,
  396. @JsonProperty(value = "reset", required = true) long resetEpochSeconds,
  397. @JacksonInject @CheckForNull GitHubConnectorResponse connectorResponse) {
  398. this.limit = limit;
  399. this.remaining = remaining;
  400. this.resetEpochSeconds = resetEpochSeconds;
  401. String updatedAt = null;
  402. if (connectorResponse != null) {
  403. updatedAt = connectorResponse.header("Date");
  404. }
  405. this.resetDate = calculateResetDate(updatedAt);
  406. }
  407. /**
  408. * Determine if the current {@link Record} is outdated compared to another. Rate Limit dates are only accurate
  409. * to the second, so we look at other information in the record as well.
  410. *
  411. * {@link Record}s with earlier {@link #getResetEpochSeconds()} are replaced by those with later.
  412. * {@link Record}s with the same {@link #getResetEpochSeconds()} are replaced by those with less remaining
  413. * count.
  414. *
  415. * {@link UnknownLimitRecord}s compare with each other like regular {@link Record}s.
  416. *
  417. * {@link Record}s are replaced by {@link UnknownLimitRecord}s only when the current {@link Record} is expired
  418. * and the {@link UnknownLimitRecord} is not. Otherwise Regular {@link Record}s are not replaced by
  419. * {@link UnknownLimitRecord}s.
  420. *
  421. * Expiration is only considered after other checks, meaning expired records may sometimes be replaced by other
  422. * expired records.
  423. *
  424. * @param other
  425. * the other {@link Record}
  426. * @return the {@link Record} that is most current
  427. */
  428. Record currentOrUpdated(@Nonnull Record other) {
  429. // This set of checks avoids most calls to isExpired()
  430. // Depends on UnknownLimitRecord.current() to prevent continuous updating of GHRateLimit rateLimit()
  431. if (getResetEpochSeconds() > other.getResetEpochSeconds()
  432. || (getResetEpochSeconds() == other.getResetEpochSeconds()
  433. && getRemaining() <= other.getRemaining())) {
  434. // If the current record has a later reset
  435. // or the current record has the same reset and fewer or same requests remaining
  436. // Then it is most recent
  437. return this;
  438. } else if (!(other instanceof UnknownLimitRecord)) {
  439. // If the above is not the case that means other has a later reset
  440. // or the same resent and fewer requests remaining.
  441. // If the other record is not an unknown record, the the other is more recent
  442. return other;
  443. } else if (this.isExpired() && !other.isExpired()) {
  444. // The other is an unknown record.
  445. // If the current record has expired and the other hasn't, return the other.
  446. return other;
  447. }
  448. // If none of the above, the current record is most valid.
  449. return this;
  450. }
  451. /**
  452. * Recalculates the {@link #resetDate} relative to the local machine clock.
  453. * <p>
  454. * {@link RateLimitChecker}s and {@link RateLimitHandler}s use {@link #getResetDate()} to make decisions about
  455. * how long to wait for until for the rate limit to reset. That means that {@link #getResetDate()} needs to be
  456. * calculated based on the local machine clock.
  457. * </p>
  458. * <p>
  459. * When we say that the clock on two machines is "synchronized", we mean that the UTC time returned from
  460. * {@link System#currentTimeMillis()} on each machine is basically the same. For the purposes of rate limits an
  461. * differences of up to a second can be ignored.
  462. * </p>
  463. * <p>
  464. * When the clock on the local machine is synchronized to the same time as the clock on the GitHub server (via a
  465. * time service for example), the {@link #resetDate} generated directly from {@link #resetEpochSeconds} will be
  466. * accurate for the local machine as well.
  467. * </p>
  468. * <p>
  469. * When the clock on the local machine is not synchronized with the server, the {@link #resetDate} must be
  470. * recalculated relative to the local machine clock. This is done by taking the number of seconds between the
  471. * response "Date" header and {@link #resetEpochSeconds} and then adding that to this record's
  472. * {@link #createdAtEpochSeconds}.
  473. *
  474. * @param updatedAt
  475. * a string date in RFC 1123
  476. * @return reset date based on the passed date
  477. */
  478. @Nonnull
  479. private Date calculateResetDate(@CheckForNull String updatedAt) {
  480. long updatedAtEpochSeconds = createdAtEpochSeconds;
  481. if (!StringUtils.isBlank(updatedAt)) {
  482. try {
  483. // Get the server date and reset data, will always return a time in GMT
  484. updatedAtEpochSeconds = ZonedDateTime.parse(updatedAt, DateTimeFormatter.RFC_1123_DATE_TIME)
  485. .toEpochSecond();
  486. } catch (DateTimeParseException e) {
  487. if (LOGGER.isLoggable(FINEST)) {
  488. LOGGER.log(FINEST, "Malformed Date header value " + updatedAt, e);
  489. }
  490. }
  491. }
  492. // This may seem odd but it results in an accurate or slightly pessimistic reset date
  493. // based on system time rather than assuming the system time synchronized with the server
  494. long calculatedSecondsUntilReset = resetEpochSeconds - updatedAtEpochSeconds;
  495. return new Date((createdAtEpochSeconds + calculatedSecondsUntilReset) * 1000);
  496. }
  497. /**
  498. * Gets the remaining number of requests allowed before this connection will be throttled.
  499. *
  500. * @return an integer
  501. */
  502. public int getRemaining() {
  503. return remaining;
  504. }
  505. /**
  506. * Gets the total number of API calls per hour allotted for this connection.
  507. *
  508. * @return an integer
  509. */
  510. public int getLimit() {
  511. return limit;
  512. }
  513. /**
  514. * Gets the time in epoch seconds when the rate limit will reset.
  515. *
  516. * This is the raw value returned by the server. This value is not adjusted if local machine time is not
  517. * synchronized with server time. If attempting to check when the rate limit will reset, use
  518. * {@link #getResetDate()} or implement a {@link RateLimitChecker} instead.
  519. *
  520. * @return a long representing the time in epoch seconds when the rate limit will reset
  521. * @see #getResetDate()
  522. */
  523. public long getResetEpochSeconds() {
  524. return resetEpochSeconds;
  525. }
  526. /**
  527. * Whether the rate limit reset date indicated by this instance is expired
  528. *
  529. * If attempting to wait for the rate limit to reset, consider implementing a {@link RateLimitChecker} instead.
  530. *
  531. * @return true if the rate limit reset date has passed. Otherwise false.
  532. */
  533. public boolean isExpired() {
  534. return getResetDate().getTime() < System.currentTimeMillis();
  535. }
  536. /**
  537. * The date at which the rate limit will reset, adjusted to local machine time if the local machine's clock not
  538. * synchronized with to the same clock as the GitHub server.
  539. *
  540. * If attempting to wait for the rate limit to reset, consider implementing a {@link RateLimitChecker} instead.
  541. *
  542. * @return the calculated date at which the rate limit has or will reset.
  543. */
  544. @Nonnull
  545. public Date getResetDate() {
  546. return new Date(resetDate.getTime());
  547. }
  548. @Override
  549. public String toString() {
  550. return "{" + "remaining=" + getRemaining() + ", limit=" + getLimit() + ", resetDate=" + getResetDate()
  551. + '}';
  552. }
  553. @Override
  554. public boolean equals(Object o) {
  555. if (this == o) {
  556. return true;
  557. }
  558. if (o == null || getClass() != o.getClass()) {
  559. return false;
  560. }
  561. Record record = (Record) o;
  562. return getRemaining() == record.getRemaining() && getLimit() == record.getLimit()
  563. && getResetEpochSeconds() == record.getResetEpochSeconds()
  564. && getResetDate().equals(record.getResetDate());
  565. }
  566. @Override
  567. public int hashCode() {
  568. return Objects.hash(getRemaining(), getLimit(), getResetEpochSeconds(), getResetDate());
  569. }
  570. }
  571. private static final Logger LOGGER = Logger.getLogger(Requester.class.getName());
  572. }