Jak wywołać żądanie asynchroniczne synchronicznie z wartością limitu czasu?

Jan 07 2021

Muszę synchronicznie wywołać asynchroniczne żądanie API. Ponieważ odpowiedź na to żądanie interfejsu API zajmuje dużo czasu, chcę również ustawić limit czasu na niepowodzenie żądania interfejsu API i kontynuować z wartością null. Oto mój kod wywołujący ten interfejs 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()
        }
    }
}

Nie mogę zmienić tego, jak External3rdPartyApidziała.

Myślę, że powyższy kod wygląda źle. Przeczytałem też w innej odpowiedzi :

withTimeout { ... }jest przeznaczony do anulowania trwającej operacji po przekroczeniu limitu czasu, co jest możliwe tylko wtedy, gdy daną operację można anulować .

Więc powinienem używać suspendCancellableCoroutinezamiast suspendCoroutine?

Jak mogę to lepiej napisać?

Odpowiedzi

1 ChristianB Jan 07 2021 at 19:42

Używanie suspendCoroutinejest w porządku, jeśli nie możesz (lub nie chcesz) obsługiwać anulowania Coroutine. Ale ponieważ masz limit czasu, powinieneś rozważyć użycie suspendCancellableCoroutine i obsłużyć zdarzenie anulowania, aby zatrzymać pracę (w funkcji strony trzeciej - jeśli możesz).

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

Gdy funkcja innej firmy zgłasza wyjątek, możesz spróbować go przechwycić i zakończyć kontynuację, używając wznawiania exceptionlub zwracania nullwartości (w zależności od przypadku użycia):

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)
  }   
}

Połączmy wszystko razem

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
      }
    }
  }
}