/**
 * Copyright (c) 2020, 2021 Oracle and/or its affiliates.  All rights reserved.
 * This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.
 */ import { Readable } from "stream" ; import { ExponentialBackoffDelayStrategy , MaxAttemptsTerminationStrategy , WaiterConfigurationDetails , delay , WaitContextImpl , ExponentialBackoffDelayStrategyWithJitter } from "./waiter" ; import { HttpClient } from "./http" ; import { HttpRequest } from "./http-request" ; import { isReadableStream } from "./helper" ; import { handleErrorBody , handleErrorResponse } from "./helper" ; import { OciError } from ".." ; import { Logger } from "./log" ; import { BooleanString } from "./constants" ; /**
 * This class implements the retrier
 * NOTE : Retries are not supported for requests that have binary or stream bodies
 * this also affects UploadManager operations
 * For all requests with binary/stream bodies, retry attempts will be made if RetryConfigurationDetails.backupBinaryBody
 * is set to true, or if the original stream body is able to be retried
 *
 */ export type RetryConfiguration = Partial < RetryConfigurationDetails > ; export interface RetryConfigurationDetails extends WaiterConfigurationDetails { retryCondition : ( response : OciError ) => boolean ; backupBinaryBody : boolean ; } export class DefaultRetryCondition { /**
     * Default retry condition for Retry mechanism
     * NOTE : Retries are not supported for requests that have binary or stream bodies
     */ private static RETRYABLE_SERVICE_ERRORS : Map < number , string > = new Map ( [ [ 409 , "IncorrectState" ] , [ 429 , "TooManyRequests" ] ] ) ; static shouldBeRetried ( error ) { return ( error . statusCode === 500 || error . statusCode === 502 || error . statusCode === 503 || error . statusCode === 504 || error . statusCode == - 1 || isNaN ( error . statusCode ) || ( DefaultRetryCondition . RETRYABLE_SERVICE_ERRORS . has ( error . statusCode ) && DefaultRetryCondition . RETRYABLE_SERVICE_ERRORS . get ( error . statusCode ) === error . serviceCode ) ) ; } } const NO_RETRY_MAXIMUM_NUMBER_OF_ATTEMPTS = 1 ; const NO_RETRY_MAXIMUM_DELAY_IN_SECONDS = 30 ; const OCI_SDK_DEFAULT_RETRY_MAXIMUM_NUMBER_OF_ATTEMPTS = 8 ; const OCI_SDK_DEFAULT_RETRY_MAXIMUM_DELAY_IN_SECONDS = 30 ; export const NoRetryConfigurationDetails = { terminationStrategy : new MaxAttemptsTerminationStrategy ( NO_RETRY_MAXIMUM_NUMBER_OF_ATTEMPTS ) , delayStrategy : new ExponentialBackoffDelayStrategyWithJitter ( NO_RETRY_MAXIMUM_DELAY_IN_SECONDS ) , retryCondition : DefaultRetryCondition . shouldBeRetried , backupBinaryBody : false } ; export const OciSdkDefaultRetryConfiguration = { terminationStrategy : new MaxAttemptsTerminationStrategy ( OCI_SDK_DEFAULT_RETRY_MAXIMUM_NUMBER_OF_ATTEMPTS ) , delayStrategy : new ExponentialBackoffDelayStrategyWithJitter ( OCI_SDK_DEFAULT_RETRY_MAXIMUM_DELAY_IN_SECONDS ) , retryCondition : DefaultRetryCondition . shouldBeRetried , backupBinaryBody : false } ; export class GenericRetrier { private _retryConfiguration : RetryConfigurationDetails ; private _logger : Logger = ( undefined as unknown ) as Logger ; private static OPC_CLIENT_RETRIES_HEADER = "opc-client-retries" ; private static OCI_SDK_DEFAULT_RETRY_ENABLED = "OCI_SDK_DEFAULT_RETRY_ENABLED" ; constructor ( retryConfiguration ) { const preferredRetryConfig = { ... NoRetryConfigurationDetails , ... retryConfiguration } ; this . _retryConfiguration = preferredRetryConfig ; } static get envVariableCheckForDefaultRetry ( ) : RetryConfiguration | null { if ( process . env [ GenericRetrier . OCI_SDK_DEFAULT_RETRY_ENABLED ] === BooleanString . FALSE ) { return NoRetryConfigurationDetails ; } else if ( process . env [ GenericRetrier . OCI_SDK_DEFAULT_RETRY_ENABLED ] === BooleanString . TRUE ) { return OciSdkDefaultRetryConfiguration ; } else return null ; } private static DefaultRetryConfiguration : RetryConfiguration | null = GenericRetrier . envVariableCheckForDefaultRetry ; static get defaultRetryConfiguration ( ) : RetryConfiguration | null { return GenericRetrier . DefaultRetryConfiguration ; } static set defaultRetryConfiguration ( retryConfig ) { GenericRetrier . DefaultRetryConfiguration = { ... GenericRetrier . DefaultRetryConfiguration , ... retryConfig } ; } public set logger ( logger ) { this . _logger = logger ; } public get backUpBinaryBody ( ) : boolean { return this . retryConfiguration . backupBinaryBody ; } public get retryConfiguration ( ) : RetryConfigurationDetails { return this . _retryConfiguration ; } public static createPreferredRetrier ( clientRetryConfiguration ? , requestRetryConfiguration ? , specRetryConfiguration ? ) { let retryConfigToUse = [ requestRetryConfiguration , clientRetryConfiguration , GenericRetrier . defaultRetryConfiguration ] . filter ( configuration => configuration !== null && configuration !== undefined ) [ 0 ] ; retryConfigToUse = { ... specRetryConfiguration , ... retryConfigToUse } ; return new GenericRetrier ( retryConfigToUse ) ; } public async makeServiceCall ( httpClient , request , excludeBody ? ) { const waitContext = new WaitContextImpl ( ) ; let lastKnownError ! ; let shouldBeRetried = true ; while ( true ) { try { this . addOpcClientRetryHeader ( request ) ; const response = await httpClient . send ( request , excludeBody ) ; if ( response . status && response . status >= 200 && response . status <= 299 ) { return response ; } else if ( ( response as any ) . code === "EOPENBREAKER" ) { const circuitBreakerError = response ; const errorObject = new OciError ( circuitBreakerError . code , "unknown code" , circuitBreakerError . message , "unknown" ) ; shouldBeRetried = this . retryConfiguration . retryCondition ( errorObject ) ; lastKnownError = errorObject ; } else { const errBody = await handleErrorBody ( response ) ; const errorObject = handleErrorResponse ( response , errBody ) ; shouldBeRetried = this . retryConfiguration . retryCondition ( errorObject ) ; lastKnownError = errorObject ; } } catch ( err ) { lastKnownError = new OciError ( err . code , "unknown code" , err . message , "unknown" ) ; shouldBeRetried = true ; } if ( ! shouldBeRetried || ! GenericRetrier . isRequestRetryable ( request ) ) { console . warn ( `Request cannot be retried. Not Retrying. Exception occurred : ${ lastKnownError }` ) ; throw lastKnownError ; } else if ( this . retryConfiguration . terminationStrategy . shouldTerminate ( waitContext ) ) { console . warn ( `All retry attempts have exhausted. Total Attempts : ${ waitContext . attemptCount + 1 }. Last exception occurred : ${ lastKnownError }` ) ; throw lastKnownError ; } const delayTime = this . retryConfiguration . delayStrategy . delay ( waitContext ) ; waitContext . attemptCount ++ ; console . warn ( `Request failed with Exception : ${ lastKnownError }\nRetrying request -> Total Attempts : ${ waitContext . attemptCount }, Retrying after ${ delayTime } seconds...` ) ; await delay ( delayTime ) ; GenericRetrier . refreshRequest ( request ) ; } } private static refreshRequest ( request ) { request . headers . set ( "x-date" , new Date ( ) . toUTCString ( ) ) ; } private addOpcClientRetryHeader ( request ) { const terminationStrategy = this . retryConfiguration . terminationStrategy ; const opcClientRetryHeader = request . headers . get ( GenericRetrier . OPC_CLIENT_RETRIES_HEADER ) ; if ( terminationStrategy instanceof MaxAttemptsTerminationStrategy && terminationStrategy . maxAttempts > 1 && ( opcClientRetryHeader === undefined || opcClientRetryHeader === null ) ) { request . headers . set ( GenericRetrier . OPC_CLIENT_RETRIES_HEADER , "true" ) ; } } private static isRequestRetryable ( request ) { if ( ! request . body ) return true ; else if ( request . body ) { return this . isRetryableStream ( request . body ) ; } } private static isRetryableStream ( obj ) { if ( obj instanceof Uint8Array || obj instanceof Buffer || typeof obj === "string" ) { return true ; } return false ; } }