TP-75284 | added same idempotencyKey for each request in case of api Polling (#11946)

Co-authored-by: Kishan Kumar <kishan.kumar@navi.com>
This commit is contained in:
Soumya Ranjan Patra
2024-08-13 15:25:15 +05:30
committed by GitHub
parent 3a3f0add83
commit 12404a0276
8 changed files with 83 additions and 28 deletions

View File

@@ -66,7 +66,9 @@ import com.navi.ap.utils.toMap
import com.navi.ap.utils.toMutableMap
import com.navi.base.AppServiceManager
import com.navi.base.model.CtaData
import com.navi.base.utils.EMPTY
import com.navi.base.utils.isNotNull
import com.navi.base.utils.isNull
import com.navi.base.utils.orElse
import com.navi.base.utils.orFalse
import com.navi.common.constants.API_SUCCESS_CODE
@@ -82,6 +84,7 @@ import com.navi.uitron.model.UiTronResponse
import com.navi.uitron.model.action.TriggerApiAction
import com.navi.uitron.model.data.UiTronActionData
import com.navi.uitron.utils.orVal
import java.util.UUID
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -520,7 +523,9 @@ abstract class ApplicationPlatformVM(
action: String = queryMap[APP_ACTION] ?: ApNavigationActions.NEXT.name,
isScreenDefinitionRequired: Boolean = true,
sourceScreen: String = DEFAULT_SOURCE_SCREEN,
apiAction: TriggerApiAction? = null
apiAction: TriggerApiAction? = null,
idempotencyKey: String = EMPTY, // It is non-empty for polling requests
isPolling: Boolean = false // It is true when this is a polling request
) {
viewModelScope.launch(Dispatchers.IO) {
_ctaResponseState.value = GetCtaState.Loading
@@ -535,7 +540,8 @@ abstract class ApplicationPlatformVM(
sourceScreen = sourceScreen,
currentScreen = getQueryMap()[APP_PLATFORM_SCREEN_ID] ?: DEFAULT_SOURCE_SCREEN,
currentScreenState =
getQueryMap()[APP_PLATFORM_SCREEN_STATE_ID] ?: DEFAULT_SCREEN_STATE_ID
getQueryMap()[APP_PLATFORM_SCREEN_STATE_ID] ?: DEFAULT_SCREEN_STATE_ID,
idempotencyKey = idempotencyKey,
)
shouldPollStrategyUseCase.execute(
@@ -545,8 +551,16 @@ abstract class ApplicationPlatformVM(
bottomSheetStateHolder = bottomSheetStateHolder.value
)
_ctaResponseState.value =
_ctaResponseState.update {
when {
/**
* isPolling is true when it is a polling request. periodicTaskScheduler is null
* when polling is stopped (because we have received a response from the
* previous request in the poller)
*/
isPolling && periodicTaskScheduler.isNull() -> {
GetCtaState.Nothing
}
response.statusCode == ApiConstants.API_ERROR_NO_USER_FOUND ->
getCtaStateWithLogoutCta()
response.data?.currentScreenCta?.shouldPoll.orFalse() -> {
@@ -573,6 +587,7 @@ abstract class ApplicationPlatformVM(
}
else -> GetCtaState.Nothing
}
}
}
}
@@ -654,6 +669,7 @@ abstract class ApplicationPlatformVM(
?.initialDelay
.orVal(AP_POLL_INITIAL_DELAY)
_ctaResponseState.value = GetCtaState.Loading
val idempotencyKey = UUID.randomUUID().toString()
periodicTaskScheduler =
PeriodicTaskScheduler(
maxAttempts = numberOfRetry,
@@ -677,6 +693,7 @@ abstract class ApplicationPlatformVM(
)
},
) {
/** idempotencyKey will be same for each request in the poller */
getCta(
applicationId = queryMap[APP_PLATFORM_APPLICATION_ID].orEmpty(),
applicationType = queryMap[APP_PLATFORM_APPLICATION_TYPE].orEmpty(),
@@ -685,7 +702,9 @@ abstract class ApplicationPlatformVM(
?: queryMap[APP_PLATFORM_APPLICATION_TYPE].orEmpty(),
action = queryMap[APP_ACTION].orEmpty(),
isScreenDefinitionRequired = true,
apiAction = apiAction
apiAction = apiAction,
idempotencyKey = idempotencyKey,
isPolling = true
)
}
.apply { startTask() }

View File

@@ -1,6 +1,6 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * Copyright © 2023-2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -150,6 +150,7 @@ constructor(private val service: APRetrofitService) :
sourceScreen: String,
currentScreen: String,
currentScreenState: String,
idempotencyKey: String,
): ApRepoResult<ApGetNextCtaResponse> {
val apApiRequest =
ApApiRequest(
@@ -164,7 +165,8 @@ constructor(private val service: APRetrofitService) :
applicationId = applicationId,
request = apApiRequest,
applicationType = applicationType,
verticalType = verticalType
verticalType = verticalType,
idempotencyKey = idempotencyKey,
)
logApEvent(
Pair(APP_PLATFORM_APPLICATION_ID, applicationId),

View File

@@ -51,7 +51,8 @@ interface ApplicationPlatformRepository {
isScreenDefinitionRequired: Boolean,
sourceScreen: String,
currentScreen: String,
currentScreenState: String
currentScreenState: String,
idempotencyKey: String,
): ApRepoResult<ApGetNextCtaResponse>
suspend fun createApplicationAndGetCta(

View File

@@ -1,6 +1,6 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * Copyright © 2023-2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -24,7 +24,8 @@ class NaviApHttpClient
@Inject
constructor(
networkInfo: NetworkInfo = NetworkInfoProvider.networkInfo,
@ApplicationContext private val context: Context
@ApplicationContext private val context: Context,
private val platformInterceptor: PlatformInterceptor,
) : BaseHttpClient(networkInfo, context) {
val httpClientBuilder: OkHttpClient.Builder
@@ -42,7 +43,7 @@ constructor(
.build()
)
}
addInterceptor(PlatformInterceptor())
addInterceptor(platformInterceptor)
}
return okHttpClientBuilder
}

View File

@@ -11,10 +11,13 @@ import com.navi.ap.network.utils.RequestCache
import com.navi.ap.network.utils.isInternalServerError
import com.navi.ap.utils.constants.X_IDEMPOTENCY_KEY
import com.navi.ap.utils.helper.RequestHelper
import com.navi.base.utils.isNotNull
import com.navi.common.network.ApiConstants
import com.navi.common.network.handleException
import com.navi.common.network.logIOException
import com.navi.common.network.models.ErrorMessage
import java.util.UUID
import javax.inject.Inject
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Protocol
@@ -22,18 +25,18 @@ import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
class PlatformInterceptor(
private val requestCache: RequestCache = RequestCache(),
private val requestHelper: RequestHelper = RequestHelper(),
class PlatformInterceptor
@Inject
constructor(
private val requestCache: RequestCache,
private val requestHelper: RequestHelper,
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val stringUrl = request.url.toString()
val cachedRequest = requestCache.getCachedRequest(stringUrl)
val idempotencyKey = requestHelper.generateIdempotencyKey(request, cachedRequest)
val modifiedRequest = buildModifiedRequest(request, idempotencyKey)
val modifiedRequest = buildModifiedRequest(request, stringUrl)
return try {
val response = chain.proceed(modifiedRequest)
@@ -45,10 +48,37 @@ class PlatformInterceptor(
}
}
private fun buildModifiedRequest(request: Request, idempotencyKey: String): Request {
return request.newBuilder().addHeader(X_IDEMPOTENCY_KEY, idempotencyKey).build()
private fun buildModifiedRequest(request: Request, stringUrl: String): Request {
return request.newBuilder().addOrReplaceValidIdempotencyKey(request, stringUrl).build()
}
/**
* If the request have an empty or null idempotencyKey, then it will add a new idempotencyKey in
* the request header.
*
* If the request already have idempotencyKey but cached idempotencyKey is not null, then it
* will replace the idempotencyKey in the request header.
*/
private fun Request.Builder.addOrReplaceValidIdempotencyKey(
request: Request,
stringUrl: String
) = apply {
val cachedRequest = requestCache.getCachedRequest(stringUrl)
val cachedIdempotencyKey = requestHelper.getCachedIdempotencyKey(request, cachedRequest)
if (request.header(X_IDEMPOTENCY_KEY).isNullOrEmpty() or cachedIdempotencyKey.isNotNull()) {
val idempotencyKey = cachedIdempotencyKey ?: UUID.randomUUID().toString()
header(X_IDEMPOTENCY_KEY, idempotencyKey)
}
}
/**
* If the api request is successful, then it will remove the cached request from the request
* cache.
*
* If the api request gets internal server error, then it will cache the request in the request
* cache. And this idempotencyKey will be used in the next request if both the request are
* idempotent.
*/
private fun handleResponse(response: Response, stringUrl: String): Response {
when {
response.isSuccessful -> {

View File

@@ -1,6 +1,6 @@
/*
*
* * Copyright © 2023 by Navi Technologies Limited
* * Copyright © 2023-2024 by Navi Technologies Limited
* * All rights reserved. Strictly confidential
*
*/
@@ -67,6 +67,7 @@ interface APRetrofitService : PlLambdaService, CoinsLambdaService, GenericLambda
@Header("X-Platform") applicationHash: String = OS_ANDROID,
): Response<GenericResponse<ApCreateApplicationResponse>>
/** Idempotency key is non-null and not empty here in the case of polling */
@POST("/arc-warden/api/v3/application/{applicationId}/screen")
suspend fun getCta(
@Path("applicationId") applicationId: String,
@@ -76,6 +77,7 @@ interface APRetrofitService : PlLambdaService, CoinsLambdaService, GenericLambda
@Header("X-Target") target: String = ModuleName.AP.name,
@Header("Accept-Encoding") acceptEncoding: String = GZIP,
@Header("X-Platform") applicationHash: String = OS_ANDROID,
@Header("X-Idempotency-Key") idempotencyKey: String,
): Response<GenericResponse<ApGetNextCtaResponse>>
@GET("/application-platform/application/{applicationId}/end-routing")

View File

@@ -7,9 +7,10 @@
package com.navi.ap.network.utils
import javax.inject.Inject
import okhttp3.Request
class RequestCache {
class RequestCache @Inject constructor() {
private val cache = mutableMapOf<String, CachedRequest>()

View File

@@ -9,23 +9,22 @@ package com.navi.ap.utils.helper
import com.navi.ap.network.utils.CachedRequest
import com.navi.ap.utils.bodyToString
import java.util.UUID
import javax.inject.Inject
import okhttp3.Request
import okhttp3.RequestBody
class RequestHelper {
class RequestHelper @Inject constructor() {
fun generateIdempotencyKey(request: Request, cachedRequest: CachedRequest?): String {
return if (cachedRequest != null && areRequestsEqual(cachedRequest.request, request)) {
cachedRequest.idempotencyKey
} else {
UUID.randomUUID().toString()
fun getCachedIdempotencyKey(request: Request, cachedRequest: CachedRequest?): String? {
if (cachedRequest != null && areRequestsEqual(cachedRequest.request, request)) {
return cachedRequest.idempotencyKey
}
return null
}
private fun areRequestsEqual(cachedRequest: Request, newRequest: Request): Boolean {
if (cachedRequest.method != newRequest.method) return false
if (cachedRequest.url != newRequest.url) return false
if (cachedRequest.method != newRequest.method) return false
if (areRequestBodiesEqual(cachedRequest.body, newRequest.body).not()) return false
return true
}