Como chamar a solicitação assíncrona de forma síncrona com um valor de tempo limite?

Jan 07 2021

Eu tenho que chamar de forma síncrona uma solicitação de API assíncrona. Como essa solicitação de api leva muito tempo para responder, também quero definir um tempo limite para falhar a solicitação de api e continuar com nulo. Este é meu código para chamar esta api:

private suspend fun call(
    accessStage: AccessStage,
): Response? = withContext<Response?>(Dispatchers.IO) {

    return@withContext withTimeoutOrNull(1000) {

        suspendCoroutine<Response?> { continuation ->

            val request = External3rdPartyApi.newRequest(
                accessStage
            ) { response, throwable ->

                continuation.resume(response)
            }

            request.parameters = hashMapOf<String, String>().apply {
                put["token"] = External3rdPartyApi.TOKEN
                put["salt"] = External3rdPartyApi.calculateSalt(accessStage)
            }
            request.executeAsync()
        }
    }
}

Eu não posso mudar como External3rdPartyApifunciona.

Acho que o código acima parece mal. Além disso, li em outra resposta :

withTimeout { ... }foi projetado para cancelar a operação em andamento no tempo limite, o que só é possível se a operação em questão for cancelável .

Então, devo usar em suspendCancellableCoroutinevez de suspendCoroutine?

Como posso escrever de uma maneira melhor?

Respostas

1 ChristianB Jan 07 2021 at 19:42

Não há problema em usar suspendCoroutinese você não puder (ou não quiser) o cancelamento da co-rotina. Mas como você tem um tempo limite, você deve considerar o uso de suspendCancellableCoroutine e manipular o evento de cancelamento para interromper o trabalho (na função de terceiros - se possível).

suspendCancellableCoroutine<T> { continuation ->
  continuation.invokeOnCancellation { throwable ->
    // now you could stop your (third party) work  
  }        
}

Quando sua função de terceiros lança uma exceção, você pode tentar capturá-la e terminar sua continuação usando retomar com exceptionou retornar um nullvalor (depende do seu caso de uso):

suspendCancellableCoroutine<T?> { continuation ->
  try {
    continuation.resume(thirdParty.call())
  } catch (e: Exception) {
    // resume with exception
    continuation.resumeWithException(e)
    // or just return null and swallow the exception
    continuation.resume(null)
  }   
}

Vamos colocar tudo junto

suspend fun call(): Response? = withContext(Dispatchers.IO) {
  return@withContext withTimeoutOrNull(1000L) {
    return@withTimeoutOrNull suspendCancellableCoroutine { continuation ->
      try {
        continuation.resume(External3rdPartyApi.newRequest(accessStage))
      } catch (e: Exception) {
        // resume with exception
        continuation.resumeWithException(e)
        
        // or just return null and swallow the exception
        continuation.resume(null)
      }
         
      // in case the coroutine gets cancelled - because of timeout or something else
      continuation.invokeOnCancellation {
        // stop all the work
      }
    }
  }
}