PageRenderTime 40ms CodeModel.GetById 20ms app.highlight 16ms RepoModel.GetById 1ms app.codeStats 0ms

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