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

commonMain.aws.smithy.kotlin.runtime.http.middleware.RetryMiddleware.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

package aws.smithy.kotlin.runtime.http.middleware

import aws.smithy.kotlin.runtime.businessmetrics.SmithyBusinessMetric
import aws.smithy.kotlin.runtime.businessmetrics.emitBusinessMetric
import aws.smithy.kotlin.runtime.http.interceptors.InterceptorExecutor
import aws.smithy.kotlin.runtime.http.operation.*
import aws.smithy.kotlin.runtime.http.operation.deepCopy
import aws.smithy.kotlin.runtime.http.request.HttpRequestBuilder
import aws.smithy.kotlin.runtime.http.request.immutableView
import aws.smithy.kotlin.runtime.http.request.toBuilder
import aws.smithy.kotlin.runtime.io.Handler
import aws.smithy.kotlin.runtime.io.middleware.Middleware
import aws.smithy.kotlin.runtime.retries.AdaptiveRetryStrategy
import aws.smithy.kotlin.runtime.retries.RetryStrategy
import aws.smithy.kotlin.runtime.retries.StandardRetryStrategy
import aws.smithy.kotlin.runtime.retries.policy.RetryDirective
import aws.smithy.kotlin.runtime.retries.policy.RetryPolicy
import aws.smithy.kotlin.runtime.retries.toResult
import aws.smithy.kotlin.runtime.telemetry.logging.debug
import aws.smithy.kotlin.runtime.telemetry.trace.withSpan
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.coroutineContext

/**
 * Retry requests with the given strategy and policy
 * @param strategy the [RetryStrategy] to retry failed requests with
 * @param policy the [RetryPolicy] used to determine when to retry
 * @param interceptors the internal execution handler for operation interceptors
 */
internal class RetryMiddleware(
    private val strategy: RetryStrategy,
    private val policy: RetryPolicy,
    private val interceptors: InterceptorExecutor,
) : Middleware {
    override suspend fun > handle(request: SdkHttpRequest, next: H): O {
        val modified = interceptors.modifyBeforeRetryLoop(request.subject.immutableView(true))
            .let { request.copy(subject = it.toBuilder()) }

        var attempt = 1
        val result = if (modified.subject.isRetryable) {
            // FIXME this is the wrong span/context because we want the fresh one from inside each attempt but there's no way to
            // wire that through without changing the `RetryPolicy` interface
            val wrappedPolicy = PolicyLogger(policy, coroutineContext)

            val outcome = strategy.retry(wrappedPolicy) {
                withSpan, _>("Attempt-$attempt") {
                    when (strategy::class) {
                        StandardRetryStrategy::class -> modified.context.emitBusinessMetric(SmithyBusinessMetric.RETRY_MODE_STANDARD)
                        AdaptiveRetryStrategy::class -> modified.context.emitBusinessMetric(SmithyBusinessMetric.RETRY_MODE_ADAPTIVE)
                    }

                    if (attempt > 1) {
                        coroutineContext.debug> { "retrying request, attempt $attempt" }
                    }

                    // Deep copy the request because later middlewares (e.g., signing) mutate it
                    val requestCopy = modified.deepCopy()

                    val attemptResult = tryAttempt(requestCopy, next, attempt)
                    attempt++
                    attemptResult.getOrThrow()
                }
            }
            outcome.toResult()
        } else {
            // Create a child span even though we won't retry
            withSpan, _>("Non-retryable attempt") {
                tryAttempt(modified, next, attempt)
            }
        }

        return result.getOrThrow()
    }

    private suspend fun tryAttempt(
        request: SdkHttpRequest,
        next: Handler,
        attempt: Int,
    ): Result {
        val result = interceptors.readBeforeAttempt(request.subject.immutableView())
            .mapCatching {
                next.call(request)
            }

        // get the http call for this attempt (if we made it that far)
        val callList = request.context.getOrNull(HttpOperationContext.HttpCallList) ?: emptyList()
        val call = callList.getOrNull(attempt - 1)

        val httpRequest = request.subject.immutableView()
        val modified = interceptors.modifyBeforeAttemptCompletion(result, httpRequest, call?.response)

        interceptors.readAfterAttempt(modified, httpRequest, call?.response)
        return modified
    }
}

/**
 * Wrapper around [policy] that logs termination decisions
 */
private class PolicyLogger(
    private val policy: RetryPolicy,
    private val coroutineContext: CoroutineContext,
) : RetryPolicy {
    override fun evaluate(result: Result): RetryDirective = policy.evaluate(result).also {
        if (it is RetryDirective.TerminateAndFail) {
            coroutineContext.debug> { "request failed with non-retryable error" }
        }
    }
}

/**
 * Indicates whether this HTTP request could be retried. Some requests with streaming bodies are unsuitable for
 * retries.
 */
private val HttpRequestBuilder.isRetryable: Boolean
    get() = !body.isOneShot




© 2015 - 2024 Weber Informatics LLC | Privacy Policy