kotlinretrofit2kotlin-coroutines

Kotlin' coroutines and retrofit2: Program blocks for one minute after getting http response


I need to use Kotlin' coroutines and the retrofit2 library to make http requests.
When I run the following code, the http request works as expected (posts is printed), but after this, the program hangs (blocks) and only exits after one minute.

My questions are:

import retrofit2.http.GET
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import kotlinx.coroutines.*

data class Post(
    val userId: Int,
    val id: Int,
    val title: String
)

interface ApiService {
    @GET("posts")
    suspend fun getPosts(): List<Post>
}

object RetrofitClient {
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    val instance: ApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(ApiService::class.java)
    }
}

fun main() = runBlocking {
    val posts = RetrofitClient.instance.getPosts()
    posts.forEach { println(it) }
}

Solution

  • Why the application blocks after getting the data via http?

    The application doesn't exit because some non-daemon threads are still running.

    Retrofit uses OkHttpClient, which creates threads that stay alive even after requests finish.
    These threads are typically kept alive for connection reuse.

    You can see these threads by adding this code at the end of the main function:

    Thread.getAllStackTraces()
        .keys
        .filter { !it.isDaemon }
        .forEach { println(it.name) }
    

    How to avoid the application blocking?

    I would suggest 2 options:

    1) Use an OkHttpClient that doesn’t keep non-daemon threads running

    You need to build such OkHttpClient and pass it when building Retrofit.
    In your original code, you don’t call client(httpClient), so Retrofit uses the default OkHttpClient.

    object RetrofitClient {
        private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
    
        val httpClient by lazy {
            val dispatcher = Dispatcher(
                Executors.newCachedThreadPool {
                    Thread(it).apply { isDaemon = true }
                }
            )
            OkHttpClient.Builder()
                .dispatcher(dispatcher)
                .connectionPool(ConnectionPool(0, 1, TimeUnit.MILLISECONDS))
                .build()
        }
        val instance: ApiService by lazy {
            Retrofit.Builder()
                .client(httpClient)
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(ApiService::class.java)
        }
    }
    
    fun main() = runBlocking {
        val posts = RetrofitClient.instance.getPosts()
        posts.forEach { println(it) }
    }
    
    2) Explicitly shut down OkHttpClient's internal resources

    Write a closeHttpClient() function that closes OkHttpClient,
    and call it at the end of the main function.

    The implementation of closeHttpClient() follows the guidance from "Shutdown isn't necessary" of OkHttpClient documentation.

    object RetrofitClient {
        private const val BASE_URL = "https://jsonplaceholder.typicode.com/"
    
        private val httpClient by lazy { OkHttpClient.Builder().build() }
    
        val instance: ApiService by lazy {
            Retrofit.Builder()
                .client(httpClient)
                .baseUrl(BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
                .create(ApiService::class.java)
        }
    
        fun closeHttpClient() {
            httpClient.dispatcher().executorService().shutdown()
            httpClient.connectionPool().evictAll()
            httpClient.cache()?.close()
        }
    }
    
    fun main() = runBlocking {
        val posts = RetrofitClient.instance.getPosts()
        posts.forEach { println(it) }
        RetrofitClient.closeHttpClient()
    }