3131import java .io .IOException ;
3232import java .io .InputStream ;
3333import java .io .InputStreamReader ;
34+ import java .io .InterruptedIOException ;
3435import java .io .Reader ;
3536import java .io .UnsupportedEncodingException ;
3637import java .lang .reflect .Array ;
3738import java .lang .reflect .Field ;
3839import java .net .HttpURLConnection ;
3940import java .net .MalformedURLException ;
4041import java .net .ProtocolException ;
42+ import java .net .SocketException ;
4143import java .net .SocketTimeoutException ;
4244import java .net .URI ;
4345import java .net .URISyntaxException ;
7476 * @author Kohsuke Kawaguchi
7577 */
7678class Requester {
79+ public static final int CONNECTION_ERROR_RETRIES = 2 ;
7780 private final GitHub root ;
7881 private final List <Entry > args = new ArrayList <Entry >();
7982 private final Map <String , String > headers = new LinkedHashMap <String , String >();
@@ -104,6 +107,11 @@ private Entry(String key, Object value) {
104107 }
105108 }
106109
110+ /**
111+ * If timeout issues let's retry after milliseconds.
112+ */
113+ private static final int retryTimeoutMillis = 500 ;
114+
107115 Requester (GitHub root ) {
108116 this .root = root ;
109117 }
@@ -492,8 +500,7 @@ private <T> T _fetch(String tailApiUrl, URL url, SupplierThrows<T, IOException>
492500 uc = setupConnection (url );
493501
494502 try {
495- retryInvalidCached404Response ();
496- return supplier .get ();
503+ return _fetchOrRetry (supplier , CONNECTION_ERROR_RETRIES );
497504 } catch (IOException e ) {
498505 handleApiError (e );
499506 } finally {
@@ -502,6 +509,55 @@ private <T> T _fetch(String tailApiUrl, URL url, SupplierThrows<T, IOException>
502509 }
503510 }
504511
512+ private <T > T _fetchOrRetry (SupplierThrows <T , IOException > supplier , int retries ) throws IOException {
513+ int responseCode = -1 ;
514+ String responseMessage = null ;
515+ // When retries equal 0 the previous call must return or throw, not retry again
516+ if (retries < 0 ) {
517+ throw new IllegalArgumentException ("'retries' cannot be less than 0" );
518+ }
519+
520+ try {
521+ // This is where the request is sent and response is processing starts
522+ responseCode = uc .getResponseCode ();
523+ responseMessage = uc .getResponseMessage ();
524+
525+ // If we are caching and get an invalid cached 404, retry it.
526+ if (!retryInvalidCached404Response (responseCode , retries )) {
527+ return supplier .get ();
528+ }
529+ } catch (FileNotFoundException e ) {
530+ // java.net.URLConnection handles 404 exception as FileNotFoundException,
531+ // don't wrap exception in HttpException to preserve backward compatibility
532+ throw e ;
533+ } catch (IOException e ) {
534+ if (!retrySocketException (e , retries )) {
535+ throw new HttpException (responseCode , responseMessage , uc .getURL (), e );
536+ }
537+ }
538+
539+ // We did not fetch or throw, retry
540+ return _fetchOrRetry (supplier , retries - 1 );
541+
542+ }
543+
544+ private boolean retrySocketException (IOException e , int retries ) throws IOException {
545+ if ((e instanceof SocketException || e instanceof SocketTimeoutException ) && retries > 0 ) {
546+ LOGGER .log (INFO ,
547+ "timed out accessing " + uc .getURL () + ". Sleeping " + Requester .retryTimeoutMillis
548+ + " milliseconds before retrying... ; will try " + retries + " more time(s)" ,
549+ e );
550+ try {
551+ Thread .sleep (Requester .retryTimeoutMillis );
552+ } catch (InterruptedException ie ) {
553+ throw (IOException ) new InterruptedIOException ().initCause (e );
554+ }
555+ uc = setupConnection (uc .getURL ());
556+ return true ;
557+ }
558+ return false ;
559+ }
560+
505561 private <T > T [] concatenatePages (Class <T []> type , List <T []> pages , int totalLength ) {
506562
507563 T [] result = type .cast (Array .newInstance (type .getComponentType (), totalLength ));
@@ -851,10 +907,8 @@ private <T> T parse(Class<T> type, T instance) throws IOException {
851907 private <T > T parse (Class <T > type , T instance , int timeouts ) throws IOException {
852908 InputStreamReader r = null ;
853909 int responseCode = -1 ;
854- String responseMessage = null ;
855910 try {
856911 responseCode = uc .getResponseCode ();
857- responseMessage = uc .getResponseMessage ();
858912 if (responseCode == 304 ) {
859913 return null ; // special case handling for 304 unmodified, as the content will be ""
860914 }
@@ -898,22 +952,12 @@ private <T> T parse(Class<T> type, T instance, int timeouts) throws IOException
898952 return setResponseHeaders (MAPPER .readerForUpdating (instance ).<T >readValue (data ));
899953 }
900954 return null ;
901- } catch (FileNotFoundException e ) {
902- // java.net.URLConnection handles 404 exception as FileNotFoundException,
903- // don't wrap exception in HttpException to preserve backward compatibility
904- throw e ;
905- } catch (IOException e ) {
906- if (e instanceof SocketTimeoutException && timeouts > 0 ) {
907- LOGGER .log (INFO , "timed out accessing " + uc .getURL () + "; will try " + timeouts + " more time(s)" , e );
908- return parse (type , instance , timeouts - 1 );
909- }
910- throw new HttpException (responseCode , responseMessage , uc .getURL (), e );
911955 } finally {
912956 IOUtils .closeQuietly (r );
913957 }
914958 }
915959
916- private void retryInvalidCached404Response () throws IOException {
960+ private boolean retryInvalidCached404Response (int responseCode , int retries ) throws IOException {
917961 // WORKAROUND FOR ISSUE #669:
918962 // When the Requester detects a 404 response with an ETag (only happpens when the server's 304
919963 // is bogus and would cause cache corruption), try the query again with new request header
@@ -924,16 +968,20 @@ private void retryInvalidCached404Response() throws IOException {
924968 // scenarios. If GitHub ever fixes their issue and/or begins providing accurate ETags to
925969 // their 404 responses, this will result in at worst two requests being made for each 404
926970 // responses. However, only the second request will count against rate limit.
927- int responseCode = uc .getResponseCode ();
928971 if (responseCode == 404 && Objects .equals (uc .getRequestMethod (), "GET" ) && uc .getHeaderField ("ETag" ) != null
929- && !Objects .equals (uc .getRequestProperty ("Cache-Control" ), "no-cache" )) {
972+ && !Objects .equals (uc .getRequestProperty ("Cache-Control" ), "no-cache" ) && retries > 0 ) {
973+ LOGGER .log (FINE ,
974+ "Encountered GitHub invalid cached 404 from " + uc .getURL ()
975+ + ". Retrying with \" Cache-Control\" =\" no-cache\" ..." );
976+
930977 uc = setupConnection (uc .getURL ());
931978 // Setting "Cache-Control" to "no-cache" stops the cache from supplying
932979 // "If-Modified-Since" or "If-None-Match" values.
933980 // This makes GitHub give us current data (not incorrectly cached data)
934981 uc .setRequestProperty ("Cache-Control" , "no-cache" );
935- uc . getResponseCode () ;
982+ return true ;
936983 }
984+ return false ;
937985 }
938986
939987 private <T > T setResponseHeaders (T readValue ) {
0 commit comments