Cancellation in Coroutines and Okhttp

Viewed 2746

So I'm playing around using Coroutines and Okhttp to connect a websocket.

What I've done

// initialise okhttp
fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(RetryInterceptor())
            .build()
}

// RetryInterceptor.kt
class RetryInterceptor : Interceptor {

    companion object {
        private const val RETRIES_LIMIT = 4
    }

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        var retries = 0

        var response: Response?
        response = sendRequest(chain, request)

        while (response == null && retries <= RETRIES_LIMIT) {
            retries++
            val sleepTimer = 2.toDouble().pow(retries.toDouble())
            Log.d("OkhttpClient", "Connection failed, retry in ${sleepTimer}s")
            Thread.sleep(sleepTimer.toLong() * 1000)
            response = sendRequest(chain, request)
        }

        return response ?: Response.Builder()
            .request(request)
            .code(400)
            .build()
    }

    private fun sendRequest(chain: Interceptor.Chain, request: Request): Response? {
        val response: Response
        return try {
            response = chain.proceed(request)
            if (!response.isSuccessful) null else response
        } catch (e: IOException) {
            null
        }
    }
}

// define a exception handler
val handler = CoroutineExceptionHandler { _, throwable ->
        when (throwable) {
            is CancellationException -> {
                // cancel the socket connection here
                Log.d("CancellationException", "cancelled")
            }
            else -> onRegisterError(
                throwable.localizedMessage ?: "Coroutine Error"
            )

        }
    }

// Then inside ViewModel, fire up the okhttp client
val viewModelScopeJob = viewModelScope.launch(context = handler) {

            val someOtherJob = otherContext.launch {
                // launch suspend fun connectSocket()
            }

        }
// Then call cancel inside ViewModel like this:
viewModelScopeJob.cancel()

Problem

viewModelScopeJob is a parent job, when the cancel() is being called, it should cancel its child-jobs and invoke the CancellationException, however it doesn't.

Question

So the coroutine job will not be cancelled because of Thread.sleep() inside interceptor is not cooperative.

My questions is: given RetryInterceptor is located in a separate class, I'm not be able to use methods like delay(), how should I change my code in order to cancel the retry when viewModelScopeJob.cancel() is called?

1 Answers

You need to make two fixes.

First, register a coroutine cancelation listener that cancels the OkHttp call. You can see an example of this in Retrofit’s coroutine integration.

continuation.invokeOnCancellation {
  cancel()
}

Next, you need to interrupt the thread sleep when the call is canceled. One way to handle this is with an EventListener. Override cancel to interrupt the OkHttp thread. You can save a reference to that thread with Thread.currentThread() in callStart(). You should also override callEnd() and callFailed() to clear that saved reference.

The Events page has more information on how to register an event listener factory so that each call gets its own EventListener instance.

Related