While attempting to adopt Query Batching using the Apollo Kotlin version 3.6.2
I’m experiencing a NullPointerException with the following Stacktrace.
pool-21-thread-1
at com.somrandomapp.android.fd1.w.y0(_Collections.kt:3)
at com.apollographql.apollo3.network.http.BatchingHttpInterceptor.executePendingRequests(BatchingHttpInterceptor.kt:121)
at com.apollographql.apollo3.network.http.BatchingHttpInterceptor.access$executePendingRequests(BatchingHttpInterceptor:62)
at com.apollographql.apollo3.network.http.BatchingHttpInterceptor$1.invokeSuspend(BatchingHttpInterceptor.kt:80)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
My understanding is that this exception is being thrown when attempting to convert the pendingRequests property to an immutable list in line:121 of the BatchingHttpInterceptor.
The property pendingRequests is a non-null property which is initialized during the Initialization of the BatchingHttpInterceptor and it throwing a NPE is bizarre.
So far the only explanation for this behavior could be race condition where in the pendingRequests is being accessed before it is initialized.
Any suggestions that could help explain this behavior would be extremely useful.
Hi @mbonnin
I was able to retrace the obfuscated bits from the stacktrace.
at kotlin.collections.CollectionsKt___CollectionsKt.y0(_Collections.kt:3)
at com.apollographql.apollo3.network.http.BatchingHttpInterceptor.executePendingRequests(BatchingHttpInterceptor.kt:121)
at com.apollographql.apollo3.network.http.BatchingHttpInterceptor.access$executePendingRequests(BatchingHttpInterceptor:62)
at com.apollographql.apollo3.network.http.BatchingHttpInterceptor$1.invokeSuspend(BatchingHttpInterceptor.kt:80)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:919)
it appears com.somrandomapp.android.fd1.w.y0 is referring to the kotlin collections package.
Also, the y0 refers to the following:-
Hi @mbonnin
I continue to recreate the exception, in vain, on my Android App.
But I was able to recreate the exception using a sample code I wrote which resembles the BatchingHttpInterceptor.
Here’s the code I wrote:-
fun main(args: Array<String>) {
Test()
}
class Test{
private val dispatcher = CloseableSingleThreadDispatcher()
val scope = CoroutineScope(dispatcher.coroutineDispatcher)
init {
val job = scope.launch {
while(true) {
println("In loop!!")
delay(10)
doSomethingRandom()
}
}
runBlocking {
job.join()
}
println("Exiting code!!!")
}
class BogusInnerClass
val bogusInnerClasses = mutableListOf<BogusInnerClass>().also {
println("bogusInnerClassesInitialized!!")
}
fun doSomethingRandom(): Int {
println("# of bogus classes: ${bogusInnerClasses.toList()}") //This is where the exception is thrown
return -1
}
}
internal class CloseableSingleThreadDispatcher constructor() : Closeable {
private var closed = false
private val _dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
val coroutineDispatcher: CoroutineDispatcher
get() = _dispatcher
override fun close() {
if (!closed) {
_dispatcher.close()
closed = true
}
}
}
This may be a bit of a stretch but shouldn’t the initialization of the bogusInnerClasses take place before the init block?
The thing that confuses me if this were indeed an issue, almost everyone using Query Batching would/should be facing this issue.
But this was the only way of recreating the exception.
In the Test case, bogusInnerClasses is initialized (here) after launch {} (here). Especially, since the job is joined, the constructor will pause at this point until bogusInnerClasses is accessed (which crashes because it wasn’t initialized).
In the BatchingHttpInterceptor case, the scenario is different. pendingRequests is initialized in the constructor (here). I can’t figure out how launch {} would be called concurrently with the constructor. Nothing in the constructor calls into intercept()
So I’m not sure what’s going on there. If you’re using R8, might be worth disassembling the dex bytecode to see if it matches the java bytecode. You can do so with apktool