-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
687ddfd
commit 8289f0c
Showing
7 changed files
with
240 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,4 +79,4 @@ class LocalBackup( | |
private fun id(context: UnleashContext): String { | ||
return context.hashCode().toString() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
unleashandroidsdk/src/main/java/io/getunleash/android/http/Throttler.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package io.getunleash.android.http | ||
|
||
import android.util.Log | ||
import java.net.HttpURLConnection | ||
import java.util.concurrent.atomic.AtomicLong | ||
import kotlin.math.max | ||
import kotlin.math.min | ||
|
||
|
||
class Throttler( | ||
private val intervalLengthInSeconds: Long, | ||
longestAcceptableIntervalSeconds: Long, | ||
private val target: String | ||
) { | ||
companion object { | ||
private const val TAG = "Throttler" | ||
} | ||
private val maxSkips = max( | ||
longestAcceptableIntervalSeconds / max( | ||
intervalLengthInSeconds, 1 | ||
), 1 | ||
) | ||
|
||
private val skips = AtomicLong(0) | ||
private val failures = AtomicLong(0) | ||
|
||
/** | ||
* We've had one successful call, so if we had 10 failures in a row, this will reduce the skips | ||
* down to 9, so that we gradually start polling more often, instead of doing max load | ||
* immediately after a sequence of errors. | ||
*/ | ||
internal fun decrementFailureCountAndResetSkips() { | ||
if (failures.get() > 0) { | ||
skips.set(max(failures.decrementAndGet(), 0L)) | ||
} | ||
} | ||
|
||
/** | ||
* We've gotten the message to back off (usually a 429 or a 50x). If we have successive | ||
* failures, failure count here will be incremented higher and higher which will handle | ||
* increasing our backoff, since we set the skip count to the failure count after every reset | ||
*/ | ||
private fun increaseSkipCount() { | ||
skips.set(min(failures.incrementAndGet(), maxSkips)) | ||
} | ||
|
||
/** | ||
* We've received an error code that we don't expect to change, which means we've already logged | ||
* an ERROR. To avoid hammering the server that just told us we did something wrong and to avoid | ||
* flooding the logs, we'll increase our skip count to maximum | ||
*/ | ||
private fun maximizeSkips() { | ||
skips.set(maxSkips) | ||
failures.incrementAndGet() | ||
} | ||
|
||
fun performAction(): Boolean { | ||
return skips.get() <= 0 | ||
} | ||
|
||
fun skipped() { | ||
skips.decrementAndGet() | ||
} | ||
|
||
internal fun handleHttpErrorCodes(responseCode: Int) { | ||
if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED | ||
|| responseCode == HttpURLConnection.HTTP_FORBIDDEN | ||
) { | ||
maximizeSkips() | ||
Log.e(TAG, | ||
"Client was not authorized to talk to the Unleash API at $target. Backing off to $maxSkips times our poll interval (of $intervalLengthInSeconds seconds) to avoid overloading server", | ||
) | ||
} | ||
if (responseCode == HttpURLConnection.HTTP_NOT_FOUND) { | ||
maximizeSkips() | ||
Log.e(TAG, | ||
"Server said that the endpoint at $target does not exist. Backing off to $maxSkips times our poll interval (of $intervalLengthInSeconds seconds) to avoid overloading server", | ||
) | ||
} else if (responseCode == 429) { | ||
increaseSkipCount() | ||
Log.i(TAG, | ||
"RATE LIMITED for the ${failures.get()}. time. Further backing off. Current backoff at ${skips.get()} times our interval (of $intervalLengthInSeconds seconds)", | ||
) | ||
} else if (responseCode >= HttpURLConnection.HTTP_INTERNAL_ERROR) { | ||
increaseSkipCount() | ||
Log.i(TAG, | ||
"Server failed with a $responseCode status code. Backing off. Current backoff at ${skips.get()} times our poll interval (of $intervalLengthInSeconds seconds)", | ||
) | ||
} | ||
} | ||
|
||
fun getSkips(): Long { | ||
return skips.get() | ||
} | ||
|
||
fun getFailures(): Long { | ||
return failures.get() | ||
} | ||
|
||
fun handle(statusCode: Int) { | ||
if (statusCode in 200..399) { | ||
decrementFailureCountAndResetSkips(); | ||
} | ||
if (statusCode >= 400) { | ||
handleHttpErrorCodes(statusCode); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
unleashandroidsdk/src/test/java/io/getunleash/android/http/ThrottlerTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
package io.getunleash.android.http | ||
|
||
import io.getunleash.android.BaseTest | ||
import org.assertj.core.api.Assertions.assertThat | ||
import org.junit.Test | ||
import java.net.MalformedURLException | ||
|
||
class ThrottlerTest : BaseTest(){ | ||
|
||
@Test | ||
@Throws(MalformedURLException::class) | ||
fun shouldNeverDecrementFailuresOrSkipsBelowZero() { | ||
val throttler = | ||
Throttler(10, 300, "https://localhost:1500/api"); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
assertThat(throttler.getSkips()).isEqualTo(0); | ||
assertThat(throttler.getFailures()).isEqualTo(0); | ||
} | ||
|
||
@Test | ||
@Throws(MalformedURLException::class) | ||
fun setToMaxShouldReduceDownEventually() { | ||
val throttler = | ||
Throttler(150, 300, "https://localhost:1500/api"); | ||
throttler.handleHttpErrorCodes(404); | ||
assertThat(throttler.getSkips()).isEqualTo(2); | ||
assertThat(throttler.getFailures()).isEqualTo(1); | ||
throttler.skipped(); | ||
assertThat(throttler.getSkips()).isEqualTo(1); | ||
assertThat(throttler.getFailures()).isEqualTo(1); | ||
throttler.skipped(); | ||
assertThat(throttler.getSkips()).isEqualTo(0); | ||
assertThat(throttler.getFailures()).isEqualTo(1); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
assertThat(throttler.getSkips()).isEqualTo(0); | ||
assertThat(throttler.getFailures()).isEqualTo(0); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
assertThat(throttler.getSkips()).isEqualTo(0); | ||
assertThat(throttler.getFailures()).isEqualTo(0); | ||
} | ||
|
||
@Test | ||
@Throws(MalformedURLException::class) | ||
fun handleIntermittentFailures() { | ||
val throttler = | ||
Throttler(50, 300, "https://localhost:1500/api"); | ||
throttler.handleHttpErrorCodes(429); | ||
throttler.handleHttpErrorCodes(429); | ||
throttler.handleHttpErrorCodes(503); | ||
throttler.handleHttpErrorCodes(429); | ||
assertThat(throttler.getSkips()).isEqualTo(4); | ||
assertThat(throttler.getFailures()).isEqualTo(4); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
assertThat(throttler.getSkips()).isEqualTo(3); | ||
assertThat(throttler.getFailures()).isEqualTo(3); | ||
throttler.handleHttpErrorCodes(429); | ||
assertThat(throttler.getSkips()).isEqualTo(4); | ||
assertThat(throttler.getFailures()).isEqualTo(4); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
throttler.decrementFailureCountAndResetSkips(); | ||
assertThat(throttler.getSkips()).isEqualTo(0); | ||
assertThat(throttler.getFailures()).isEqualTo(0); | ||
} | ||
} |