From 960007daa58af17c273010f4e4411f97234cbe84 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 25 Oct 2023 12:08:17 +0300 Subject: [PATCH] fix(YouTube - ReturnYouTubeDislike): Use API back off if client connection fails or times out --- .../ReturnYouTubeDislike.java | 2 +- .../requests/ReturnYouTubeDislikeApi.java | 86 +++++++++---------- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java index da8cd810a9..9890708622 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/ReturnYouTubeDislike.java @@ -80,7 +80,7 @@ public enum Vote { * How long to retain unsuccessful RYD fetches, * and also the minimum time before retrying again. */ - private static final long CACHE_TIMEOUT_FAILURE_MILLISECONDS = 2 * 60 * 1000; // 2 Minutes + private static final long CACHE_TIMEOUT_FAILURE_MILLISECONDS = 3 * 60 * 1000; // 3 Minutes /** * Unique placeholder character, used to detect if a segmented span already has dislikes added to it. diff --git a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java index f10ed8210f..3ffc087489 100644 --- a/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java +++ b/app/src/main/java/app/revanced/integrations/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -32,13 +32,13 @@ public class ReturnYouTubeDislikeApi { /** * {@link #fetchVotes(String)} TCP connection timeout */ - private static final int API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS = 2000; + private static final int API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS = 2 * 1000; // 2 Seconds. /** * {@link #fetchVotes(String)} HTTP read timeout. * To locally debug and force timeouts, change this to a very small number (ie: 100) */ - private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 5000; + private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 5 * 1000; // 5 Seconds. /** * Default connection and response timeout for voting and registration. @@ -46,7 +46,7 @@ public class ReturnYouTubeDislikeApi { * Voting and user registration runs in the background and has has no urgency * so this can be a larger value. */ - private static final int API_REGISTER_VOTE_TIMEOUT_MILLISECONDS = 90000; + private static final int API_REGISTER_VOTE_TIMEOUT_MILLISECONDS = 60 * 1000; // 60 Seconds. /** * Response code of a successful API call @@ -60,25 +60,30 @@ public class ReturnYouTubeDislikeApi { private static final int HTTP_STATUS_CODE_NOT_FOUND = 404; /** - * Indicates a client rate limit has been reached + * Indicates a client rate limit has been reached and the client must back off. */ - private static final int RATE_LIMIT_HTTP_STATUS_CODE = 429; + private static final int HTTP_STATUS_CODE_RATE_LIMIT = 429; /** - * How long to wait until API calls are resumed, if a rate limit is hit. - * No clear guideline of how long to backoff. Using 2 minutes for now. + * How long to wait until API calls are resumed, if the API requested a back off. + * No clear guideline of how long to wait until resuming. */ - private static final int RATE_LIMIT_BACKOFF_SECONDS = 120; + private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 4 * 60 * 1000; // 4 Minutes. /** - * Last time a {@link #RATE_LIMIT_HTTP_STATUS_CODE} was reached. - * zero if has not been reached. + * How long to wait until API calls are resumed, if any connection error occurs. */ - private static volatile long lastTimeRateLimitWasHit; // must be volatile, since different threads read/write to this + private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 60 * 1000; // 60 Seconds. /** - * Number of times {@link #RATE_LIMIT_HTTP_STATUS_CODE} was requested by RYD api. - * Does not include network calls attempted while rate limit is in effect + * If non zero, then the system time of when API calls can resume. + */ + private static volatile long timeToResumeAPICalls; // must be volatile, since different threads read/write to this + + /** + * Number of times {@link #HTTP_STATUS_CODE_RATE_LIMIT} was requested by RYD api. + * Does not include network calls attempted while rate limit is in effect, + * and does not include rate limit imposed if a fetch fails. */ private static volatile int numberOfRateLimitRequestsEncountered; @@ -165,16 +170,16 @@ private static long randomlyWaitIfLocallyDebugging() { * @return True, if api rate limit is in effect. */ private static boolean checkIfRateLimitInEffect(String apiEndPointName) { - if (lastTimeRateLimitWasHit == 0) { + if (timeToResumeAPICalls == 0) { return false; } - final long numberOfSecondsSinceLastRateLimit = (System.currentTimeMillis() - lastTimeRateLimitWasHit) / 1000; - if (numberOfSecondsSinceLastRateLimit < RATE_LIMIT_BACKOFF_SECONDS) { - LogHelper.printDebug(() -> "Ignoring api call " + apiEndPointName + " as only " - + numberOfSecondsSinceLastRateLimit + " seconds has passed since last rate limit."); - return true; + final long now = System.currentTimeMillis(); + if (now > timeToResumeAPICalls) { + timeToResumeAPICalls = 0; + return false; } - return false; + LogHelper.printDebug(() -> "Ignoring api call " + apiEndPointName + " as rate limit is in effect"); + return true; } /** @@ -186,37 +191,33 @@ private static boolean checkIfRateLimitWasHit(int httpResponseCode) { final double RANDOM_RATE_LIMIT_PERCENTAGE = 0.2; // 20% chance of a triggering a rate limit if (Math.random() < RANDOM_RATE_LIMIT_PERCENTAGE) { LogHelper.printDebug(() -> "Artificially triggering rate limit for debug purposes"); - httpResponseCode = RATE_LIMIT_HTTP_STATUS_CODE; + httpResponseCode = HTTP_STATUS_CODE_RATE_LIMIT; } } - - if (httpResponseCode == RATE_LIMIT_HTTP_STATUS_CODE) { - lastTimeRateLimitWasHit = System.currentTimeMillis(); - //noinspection NonAtomicOperationOnVolatileField // don't care, field is used only as an estimate - numberOfRateLimitRequestsEncountered++; - LogHelper.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " - + RATE_LIMIT_BACKOFF_SECONDS + " seconds"); - ReVancedUtils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); - return true; - } - return false; + return httpResponseCode == HTTP_STATUS_CODE_RATE_LIMIT; } - @SuppressWarnings("NonAtomicOperationOnVolatileField") // do not want to pay performance cost of full synchronization for debug fields that are only estimates anyways - private static void updateStatistics(long timeNetworkCallStarted, long timeNetworkCallEnded, boolean connectionError, boolean rateLimitHit) { + @SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are estimates. + private static void updateRateLimitAndStats(long timeNetworkCallStarted, boolean connectionError, boolean rateLimitHit) { if (connectionError && rateLimitHit) { throw new IllegalArgumentException(); } - final long responseTimeOfFetchCall = timeNetworkCallEnded - timeNetworkCallStarted; + final long responseTimeOfFetchCall = System.currentTimeMillis() - timeNetworkCallStarted; fetchCallResponseTimeTotal += responseTimeOfFetchCall; fetchCallResponseTimeMin = (fetchCallResponseTimeMin == 0) ? responseTimeOfFetchCall : Math.min(responseTimeOfFetchCall, fetchCallResponseTimeMin); fetchCallResponseTimeMax = Math.max(responseTimeOfFetchCall, fetchCallResponseTimeMax); fetchCallCount++; if (connectionError) { + timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS; fetchCallResponseTimeLast = responseTimeOfFetchCall; fetchCallNumberOfFailures++; } else if (rateLimitHit) { + LogHelper.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " + + BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds"); + timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS; + numberOfRateLimitRequestsEncountered++; fetchCallResponseTimeLast = FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT; + ReVancedUtils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); } else { fetchCallResponseTimeLast = responseTimeOfFetchCall; } @@ -262,17 +263,16 @@ public static RYDVoteData fetchVotes(String videoId) { final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { connection.disconnect(); // rate limit hit, should disconnect - updateStatistics(timeNetworkCallStarted, System.currentTimeMillis(),false, true); + updateRateLimitAndStats(timeNetworkCallStarted, false, true); return null; } if (responseCode == HTTP_STATUS_CODE_SUCCESS) { - final long timeNetworkCallEnded = System.currentTimeMillis(); // record end time before parsing // do not disconnect, the same server connection will likely be used again soon JSONObject json = Requester.parseJSONObject(connection); try { RYDVoteData votingData = new RYDVoteData(json); - updateStatistics(timeNetworkCallStarted, timeNetworkCallEnded, false, false); + updateRateLimitAndStats(timeNetworkCallStarted, false, false); LogHelper.printDebug(() -> "Voting data fetched: " + votingData); return votingData; } catch (JSONException ex) { @@ -280,9 +280,9 @@ public static RYDVoteData fetchVotes(String videoId) { // fall thru to update statistics } } else if (responseCode == HTTP_STATUS_CODE_NOT_FOUND) { - // normal response when viewing YouTube Stories (cannot vote for these) - LogHelper.printDebug(() -> "Video has no like/dislikes (video is a YouTube Story?): " + videoId); - return null; // do not updated connection statistics + // Previously would happen with YouTube Stories and was normal. + // But Stories are no more so this condition should not happen. + LogHelper.printDebug(() -> "Video has no like/dislikes: " + videoId); } else { handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); } @@ -296,7 +296,7 @@ public static RYDVoteData fetchVotes(String videoId) { LogHelper.printException(() -> "Failed to fetch votes", ex, str("revanced_ryd_failure_generic", ex.getMessage())); } - updateStatistics(timeNetworkCallStarted, System.currentTimeMillis(), true, false); + updateRateLimitAndStats(timeNetworkCallStarted, true, false); return null; } @@ -311,7 +311,7 @@ public static String registerAsNewUser() { return null; } String userId = randomString(36); - LogHelper.printDebug(() -> "Trying to register new user: " + userId); + LogHelper.printDebug(() -> "Trying to register new user"); HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId); connection.setRequestProperty("Accept", "application/json");