1 /** 2 * Copyright (c) 2013-2023, jcabi.com 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 1) Redistributions of source code must retain the above 8 * copyright notice, this list of conditions and the following 9 * disclaimer. 2) Redistributions in binary form must reproduce the above 10 * copyright notice, this list of conditions and the following 11 * disclaimer in the documentation and/or other materials provided 12 * with the distribution. 3) Neither the name of the jcabi.com nor 13 * the names of its contributors may be used to endorse or promote 14 * products derived from this software without specific prior written 15 * permission. 16 * 17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 19 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 20 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL 21 * THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 22 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 25 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 26 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 28 * OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 package com.jcabi.github.wire; 31 32 import com.jcabi.aspects.Immutable; 33 import com.jcabi.http.Request; 34 import com.jcabi.http.Response; 35 import com.jcabi.http.Wire; 36 import com.jcabi.log.Logger; 37 import java.io.IOException; 38 import java.io.InputStream; 39 import java.util.Collection; 40 import java.util.List; 41 import java.util.Map; 42 import java.util.concurrent.TimeUnit; 43 import lombok.EqualsAndHashCode; 44 import lombok.ToString; 45 46 /** 47 * Wire that waits if number of remaining request per hour is less than 48 * a given threshold. 49 * 50 * <p>Github sets following headers in each response: 51 * {@code X-RateLimit-Limit}, {@code X-RateLimit-Remaining}, and 52 * {@code X-RateLimit-Reset}. If {@code X-RateLimit-Remaining} is 53 * less than a given threshold, {@code CarefulWire} will sleep until a time 54 * specified in the {@code X-RateLimit-Reset} header. For further information 55 * about the Github rate limiting see 56 * <a href="https://developer.github.com/v3/#rate-limiting">API 57 * documentation</a>. 58 * 59 * <p>You can use {@code CarefulWire} with a {@link com.jcabi.github.Github} 60 * object: 61 * <pre> 62 * {@code 63 * Github github = new RtGithub( 64 * new RtGithub().entry().through(CarefulWire.class, 50) 65 * ); 66 * } 67 * </pre> 68 * 69 * @author Alexander Sinyagin (sinyagin.alexander@gmail.com) 70 * @version $Id: cb6ac78caff6221a20b35c8a3656f45c91156a7f $ 71 */ 72 @Immutable 73 @ToString 74 @EqualsAndHashCode(of = { "origin", "threshold" }) 75 @SuppressWarnings("PMD.AvoidDuplicateLiterals") 76 public final class CarefulWire implements Wire { 77 78 /** 79 * Original wire. 80 */ 81 private final transient Wire origin; 82 83 /** 84 * Threshold of number of remaining requests, below which requests are 85 * blocked until reset. 86 */ 87 private final transient int threshold; 88 89 /** 90 * Public ctor. 91 * 92 * @param wire Original wire 93 * @param thrshld Threshold of number of remaining requests, below which 94 * requests are blocked until reset 95 */ 96 public CarefulWire(final Wire wire, final int thrshld) { 97 this.origin = wire; 98 this.threshold = thrshld; 99 } 100 101 @Override 102 // @checkstyle ParameterNumber (8 lines) 103 public Response send( 104 final Request req, 105 final String home, 106 final String method, 107 final Collection<Map.Entry<String, String>> headers, 108 final InputStream content, 109 final int connect, final int read 110 ) throws IOException { 111 final Response resp = this.origin 112 .send(req, home, method, headers, content, connect, read); 113 final int remaining = this.remainingHeader(resp); 114 if (remaining < this.threshold) { 115 final long reset = this.resetHeader(resp); 116 final long now = TimeUnit.MILLISECONDS 117 .toSeconds(System.currentTimeMillis()); 118 if (reset > now) { 119 final long length = reset - now; 120 Logger.info( 121 this, 122 // @checkstyle LineLength (1 line) 123 "Remaining number of requests per hour is less than %d. Waiting for %d seconds.", 124 this.threshold, length 125 ); 126 try { 127 TimeUnit.SECONDS.sleep(length); 128 } catch (final InterruptedException ex) { 129 Thread.currentThread().interrupt(); 130 throw new IllegalStateException(ex); 131 } 132 } 133 } 134 return resp; 135 } 136 137 /** 138 * Get the header with the given name from the response. 139 * If there is no such header, returns null. 140 * @param resp Response to get header from 141 * @param headername Name of header to get 142 * @return The value of the first header with the given name, or null. 143 */ 144 private String headerOrNull( 145 final Response resp, 146 final String headername) { 147 final List<String> values = resp.headers().get(headername); 148 String value = null; 149 if (values != null && !values.isEmpty()) { 150 value = values.get(0); 151 } 152 return value; 153 } 154 155 /** 156 * Returns the value of the X-RateLimit-Remaining header. 157 * If there is no such header, returns Integer.MAX_VALUE (no rate limit). 158 * @param resp Response to get header from 159 * @return Number of requests remaining before the rate limit will be hit 160 */ 161 private int remainingHeader( 162 final Response resp) { 163 final String remainingstr = this.headerOrNull( 164 resp, 165 "X-RateLimit-Remaining" 166 ); 167 int remaining = Integer.MAX_VALUE; 168 if (remainingstr != null) { 169 remaining = Integer.parseInt(remainingstr); 170 } 171 return remaining; 172 } 173 174 /** 175 * Returns the value of the X-RateLimit-Reset header. 176 * If there is no such header, returns 0 (reset immediately). 177 * @param resp Response to get header from 178 * @return Timestamp (in seconds) at which the rate limit will reset 179 */ 180 private long resetHeader( 181 final Response resp) { 182 final String resetstr = this.headerOrNull(resp, "X-RateLimit-Reset"); 183 long reset = 0; 184 if (resetstr != null) { 185 reset = Long.parseLong(resetstr); 186 } 187 return reset; 188 } 189 190 }