From d8a3f95f71e6ac21aa8a5568bf0b6c7c7cadc6bb Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 8 Nov 2023 13:48:40 +0600 Subject: [PATCH 1/4] added retry with exponential backoff --- .../foundation/e/apps/data/NetworkHandler.kt | 41 +++++++++++++++++++ .../apps/data/login/PlayStoreAuthenticator.kt | 20 +++++---- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index fd736daed..882d8d33e 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -21,12 +21,17 @@ package foundation.e.apps.data import foundation.e.apps.data.playstore.utils.GPlayHttpClient import foundation.e.apps.data.playstore.utils.GplayHttpRequestException import foundation.e.apps.data.login.exceptions.GPlayException +import kotlinx.coroutines.delay +import timber.log.Timber import java.net.SocketTimeoutException private const val TIMEOUT = "Timeout" private const val UNKNOWN = "Unknown" private const val STATUS = "Status:" private const val ERROR_GPLAY_API = "Gplay api has faced error!" +private const val REGEX_CONTAIN_429_OR_401 = "429|401" +private const val MAX_RETRY_DELAY_IN_SECONDS = 300 +private const val ONE_SECOND_IN_MILLIS = 1000L suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { return try { @@ -71,3 +76,39 @@ private fun extractErrorMessage(e: Exception): String { } return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } ?: ERROR_GPLAY_API) + " $STATUS $status" } + +suspend fun retryWithBackoff(operation: suspend () -> T, retryDelayInSecond: Int = -1): T? { + try { + if (retryDelayInSecond > 0) { + delay(ONE_SECOND_IN_MILLIS * retryDelayInSecond) + } + + val result = operation() + + if (shouldRetry(result, retryDelayInSecond)) { + Timber.w("Retrying...: $retryDelayInSecond") + return retryWithBackoff(operation, calculateRetryDelay(retryDelayInSecond)) + } + + return result + } catch (e: Exception) { + if (retryDelayInSecond < MAX_RETRY_DELAY_IN_SECONDS) { + return retryWithBackoff(operation, calculateRetryDelay(retryDelayInSecond)) + } + } + + return null +} + +private fun calculateRetryDelay(retryDelayInSecond: Int) = + if (retryDelayInSecond < 0) 5 else retryDelayInSecond * 2 + + +private fun shouldRetry(result: T, retryDelayInSecond: Int) = + result is ResultSupreme<*> && !result.isSuccess() && retryDelayInSecond < MAX_RETRY_DELAY_IN_SECONDS + && isExceptionAllowedToRetry(result.exception) + +private fun isExceptionAllowedToRetry(exception: Exception?): Boolean { + val message = exception?.message + return message?.contains(Regex(REGEX_CONTAIN_429_OR_401)) != true // Here, (value != true) is used, because value can be null also and we want to allow retry for null message +} diff --git a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt index 0c89311e6..821f36d2f 100644 --- a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt +++ b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt @@ -28,6 +28,7 @@ import foundation.e.apps.data.login.api.PlayStoreLoginManagerFactory import foundation.e.apps.data.login.api.PlayStoreLoginManager import foundation.e.apps.data.login.api.GoogleLoginManager import foundation.e.apps.data.login.api.PlayStoreLoginWrapper +import foundation.e.apps.data.retryWithBackoff import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -77,25 +78,26 @@ class PlayStoreAuthenticator @Inject constructor( override suspend fun login(): AuthObject.GPlayAuth { val savedAuth = getSavedAuthData() - val authData = ( - savedAuth ?: run { - // if no saved data, then generate new auth data. - generateAuthData().let { + val authData = savedAuth ?: run { + // if no saved data, then generate new auth data. + retryWithBackoff({ generateAuthData() }).let { result -> + result?.let { if (it.isSuccess()) it.data!! else return AuthObject.GPlayAuth(it, user) } } - ) + } + - val formattedAuthData = formatAuthData(authData) - formattedAuthData.locale = locale + val formattedAuthData = authData?.let { formatAuthData(it) } + formattedAuthData?.locale = locale val result: ResultSupreme = ResultSupreme.create( status = ResultStatus.OK, data = formattedAuthData ) - result.otherPayload = formattedAuthData.email + result.otherPayload = formattedAuthData?.email - if (savedAuth == null) { + if (savedAuth == null && formattedAuthData != null) { saveAuthData(formattedAuthData) } -- GitLab From eaaa9adfef23f3e51e7d536646d7290033f2deb8 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 8 Nov 2023 20:18:08 +0600 Subject: [PATCH 2/4] fixed: code smell --- .../foundation/e/apps/data/NetworkHandler.kt | 30 ++++++++++--------- .../apps/data/login/PlayStoreAuthenticator.kt | 13 ++++---- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index 882d8d33e..6dae19de6 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -32,6 +32,7 @@ private const val ERROR_GPLAY_API = "Gplay api has faced error!" private const val REGEX_CONTAIN_429_OR_401 = "429|401" private const val MAX_RETRY_DELAY_IN_SECONDS = 300 private const val ONE_SECOND_IN_MILLIS = 1000L +private const val INITIAL_DELAY_RETRY_IN_SECONDS = 5 suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { return try { @@ -77,31 +78,32 @@ private fun extractErrorMessage(e: Exception): String { return (e.localizedMessage?.ifBlank { ERROR_GPLAY_API } ?: ERROR_GPLAY_API) + " $STATUS $status" } -suspend fun retryWithBackoff(operation: suspend () -> T, retryDelayInSecond: Int = -1): T? { +suspend fun retryWithBackoff(retryDelayInSeconds: Int = -1, operation: suspend () -> T): T? { + var result: T? = null try { - if (retryDelayInSecond > 0) { - delay(ONE_SECOND_IN_MILLIS * retryDelayInSecond) + if (retryDelayInSeconds > 0) { + delay(ONE_SECOND_IN_MILLIS * retryDelayInSeconds) } - val result = operation() + result = operation() - if (shouldRetry(result, retryDelayInSecond)) { - Timber.w("Retrying...: $retryDelayInSecond") - return retryWithBackoff(operation, calculateRetryDelay(retryDelayInSecond)) + if (shouldRetry(result, retryDelayInSeconds)) { + Timber.w("Retrying...: $retryDelayInSeconds") + result = retryWithBackoff(calculateRetryDelay(retryDelayInSeconds), operation) } - return result } catch (e: Exception) { - if (retryDelayInSecond < MAX_RETRY_DELAY_IN_SECONDS) { - return retryWithBackoff(operation, calculateRetryDelay(retryDelayInSecond)) + Timber.e(e) + if (retryDelayInSeconds < MAX_RETRY_DELAY_IN_SECONDS) { + return retryWithBackoff(calculateRetryDelay(retryDelayInSeconds), operation) } } - return null + return result } private fun calculateRetryDelay(retryDelayInSecond: Int) = - if (retryDelayInSecond < 0) 5 else retryDelayInSecond * 2 + if (retryDelayInSecond < 0) INITIAL_DELAY_RETRY_IN_SECONDS else retryDelayInSecond * 2 private fun shouldRetry(result: T, retryDelayInSecond: Int) = @@ -109,6 +111,6 @@ private fun shouldRetry(result: T, retryDelayInSecond: Int) = && isExceptionAllowedToRetry(result.exception) private fun isExceptionAllowedToRetry(exception: Exception?): Boolean { - val message = exception?.message - return message?.contains(Regex(REGEX_CONTAIN_429_OR_401)) != true // Here, (value != true) is used, because value can be null also and we want to allow retry for null message + // Here, (value != true) is used, because value can be null also and we want to allow retry for null message + return exception?.message?.contains(Regex(REGEX_CONTAIN_429_OR_401)) != true } diff --git a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt index 821f36d2f..b408ff7f9 100644 --- a/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt +++ b/app/src/main/java/foundation/e/apps/data/login/PlayStoreAuthenticator.kt @@ -80,14 +80,15 @@ class PlayStoreAuthenticator @Inject constructor( val authData = savedAuth ?: run { // if no saved data, then generate new auth data. - retryWithBackoff({ generateAuthData() }).let { result -> - result?.let { - if (it.isSuccess()) it.data!! - else return AuthObject.GPlayAuth(it, user) - } + val result = retryWithBackoff { + generateAuthData() } - } + result?.let { + if (it.isSuccess()) it.data!! + else return AuthObject.GPlayAuth(it, user) + } + } val formattedAuthData = authData?.let { formatAuthData(it) } formattedAuthData?.locale = locale -- GitLab From 039f400bef103a720469ef479e52053e43fea624 Mon Sep 17 00:00:00 2001 From: Hasib Prince Date: Wed, 15 Nov 2023 17:51:16 +0600 Subject: [PATCH 3/4] changed initial delay for exp. backoff --- app/src/main/java/foundation/e/apps/data/NetworkHandler.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index 6dae19de6..522c926cc 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -32,7 +32,7 @@ private const val ERROR_GPLAY_API = "Gplay api has faced error!" private const val REGEX_CONTAIN_429_OR_401 = "429|401" private const val MAX_RETRY_DELAY_IN_SECONDS = 300 private const val ONE_SECOND_IN_MILLIS = 1000L -private const val INITIAL_DELAY_RETRY_IN_SECONDS = 5 +private const val INITIAL_DELAY_RETRY_IN_SECONDS = 10 suspend fun handleNetworkResult(call: suspend () -> T): ResultSupreme { return try { -- GitLab From e933c9c87dffdc853686cb204b279bd2f9fe0a96 Mon Sep 17 00:00:00 2001 From: Vincent Bourgmayer Date: Wed, 15 Nov 2023 17:04:05 +0000 Subject: [PATCH 4/4] renamed REGEX_429_OR_401 --- app/src/main/java/foundation/e/apps/data/NetworkHandler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt index 522c926cc..f5c5cdd50 100644 --- a/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt +++ b/app/src/main/java/foundation/e/apps/data/NetworkHandler.kt @@ -29,7 +29,7 @@ private const val TIMEOUT = "Timeout" private const val UNKNOWN = "Unknown" private const val STATUS = "Status:" private const val ERROR_GPLAY_API = "Gplay api has faced error!" -private const val REGEX_CONTAIN_429_OR_401 = "429|401" +private const val REGEX_429_OR_401 = "429|401" private const val MAX_RETRY_DELAY_IN_SECONDS = 300 private const val ONE_SECOND_IN_MILLIS = 1000L private const val INITIAL_DELAY_RETRY_IN_SECONDS = 10 @@ -112,5 +112,5 @@ private fun shouldRetry(result: T, retryDelayInSecond: Int) = private fun isExceptionAllowedToRetry(exception: Exception?): Boolean { // Here, (value != true) is used, because value can be null also and we want to allow retry for null message - return exception?.message?.contains(Regex(REGEX_CONTAIN_429_OR_401)) != true + return exception?.message?.contains(Regex(REGEX_429_OR_401)) != true } -- GitLab