All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.pubnub.internal.retry.RetryableBase.kt Maven / Gradle / Ivy

package com.pubnub.internal.retry

import com.pubnub.api.retry.RetryConfiguration
import com.pubnub.api.retry.RetryableEndpointGroup
import retrofit2.Response
import java.io.IOException
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.net.ssl.SSLHandshakeException
import kotlin.math.pow
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds

internal abstract class RetryableBase(
    private val retryConfiguration: RetryConfiguration,
    private val endpointGroupName: RetryableEndpointGroup,
) {
    companion object {
        const val MAX_RANDOM_DELAY_IN_MILLIS = 1000 // random delay should be between 0 and 1000 milliseconds
        private const val TOO_MANY_REQUESTS = 429
        const val SERVICE_UNAVAILABLE = 503
        internal const val RETRY_AFTER_HEADER_NAME = "Retry-After"
        private val retryableStatusCodes =
            mapOf(
                429 to "TOO_MANY_REQUESTS",
                500 to "HTTP_INTERNAL_ERROR",
                502 to "HTTP_BAD_GATEWAY",
                503 to "HTTP_UNAVAILABLE",
                504 to "HTTP_GATEWAY_TIMEOUT",
                507 to "INSUFFICIENT_STORAGE",
                508 to "LOOP_DETECTED",
                510 to "NOT_EXTENDED",
                511 to "NETWORK_AUTHENTICATION_REQUIRED",
            )
        internal val retryableExceptions =
            listOf(
                UnknownHostException::class.java,
                SocketTimeoutException::class.java,
                ConnectException::class.java,
                SSLHandshakeException::class.java,
                IOException::class.java,
            )
    }

    private var exponentialMultiplier = 0.0
    protected val random = Random.Default

    internal val isRetryConfSetForThisRestCall =
        when (retryConfiguration) {
            is RetryConfiguration.None -> false

            is RetryConfiguration.Linear -> {
                val excludedOperations = retryConfiguration.excludedOperations
                endpointIsNotExcludedFromRetryConfiguration(excludedOperations)
            }

            is RetryConfiguration.Exponential -> {
                val excludedOperations = retryConfiguration.excludedOperations
                endpointIsNotExcludedFromRetryConfiguration(excludedOperations)
            }
        }

    internal fun getDelayBasedOnResponse(response: Response): Duration {
        val effectiveDelay: Duration =
            if (response.raw().code == TOO_MANY_REQUESTS) {
                calculateDelayForTooManyRequestError(response)
            } else {
                getDelayFromRetryConfiguration()
            }
        return effectiveDelay
    }

    private fun getDelayBasedOnErrorCode(
        errorCode: Int,
        retryAfterHeaderValueInSec: Int,
    ): Duration {
        return if (errorCode == TOO_MANY_REQUESTS) {
            getDelayForRetryAfterHeaderValue(retryAfterHeaderValueInSec)
        } else {
            getDelayFromRetryConfiguration()
        }
    }

    internal fun getDelayFromRetryConfiguration(): Duration {
        return when (retryConfiguration) {
            is RetryConfiguration.None -> 0.seconds
            is RetryConfiguration.Linear -> retryConfiguration.delayInSec
            is RetryConfiguration.Exponential -> {
                val delay: Duration = retryConfiguration.minDelayInSec * 2.0.pow(exponentialMultiplier)
                exponentialMultiplier++
                minOf(delay, retryConfiguration.maxDelayInSec)
            }
        }
    }

    protected val maxRetryNumberFromConfiguration: Int =
        when (retryConfiguration) {
            is RetryConfiguration.None -> 0
            is RetryConfiguration.Linear -> retryConfiguration.maxRetryNumber
            is RetryConfiguration.Exponential -> retryConfiguration.maxRetryNumber
        }

    protected fun isErrorCodeRetryable(errorCode: Int) = retryableStatusCodes.containsKey(errorCode)

    protected fun getEffectiveDelay(
        statusCode: Int,
        retryAfterHeaderValue: Int,
    ): Duration {
        val delayBasedOnStatusCode =
            getDelayBasedOnErrorCode(
                errorCode = statusCode,
                retryAfterHeaderValueInSec = retryAfterHeaderValue,
            )
        val randomDelayInMillis = random.nextInt(MAX_RANDOM_DELAY_IN_MILLIS).milliseconds
        return delayBasedOnStatusCode + randomDelayInMillis
    }

    private fun calculateDelayForTooManyRequestError(response: Response): Duration {
        val retryAfterInSec: String? = response.headers()[RETRY_AFTER_HEADER_NAME]
        val delayInSeconds = retryAfterInSec?.toIntOrNull()

        return getDelayForRetryAfterHeaderValue(delayInSeconds)
    }

    private fun getDelayForRetryAfterHeaderValue(delayInSeconds: Int?): Duration {
        return when {
            delayInSeconds != null && delayInSeconds > 0 -> {
                delayInSeconds.seconds
            }

            else -> {
                getDelayFromRetryConfiguration()
            }
        }
    }

    private fun endpointIsNotExcludedFromRetryConfiguration(excludedOperations: List): Boolean =
        excludedOperations.contains(endpointGroupName).not()
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy