https://kotlinlang.org logo
Title
n

Neal Sanche

06/01/2021, 5:48 PM
Today’s the day I'm going to make another attempt to get a GraphQL subscription working in iOS. Wish me luck. KMM memory model woes.
🤞 3
So far more of the same.
Uncaught Kotlin exception: kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen com.apollographql.apollo.network.ws.ApolloWebSocketNetworkTransport@6f8e68
I am going to try to refactor things so that instead of using a kotlin
object
as the source of the ApolloClient instances, I will use
koin
instead. Not sure if that's going to change the outcome, here, but it's worth a try I suppose.
❤️ 1
r

rocketraman

06/01/2021, 8:13 PM
What context are you running your iOS code in?
The approach I took was to run all my graphql api calls in the
MainScope()
CoroutineScope
. That worked perfectly on both Android and iOS.
n

Neal Sanche

06/01/2021, 8:54 PM
I'm generally not changing the coroutine scope, it is using multithreaded coroutines and the Dispatcher.Main for much of this. Regular GraphQL queries were working just fine. But a subscription test I'm making wasn't working at all. I have just finished implementing Koin to build up my dependencies in a more controlled way, and data came across in the Flow! However there is still a crash that I'm looking into.
👍 1
Current exception is:
Uncaught Kotlin exception: kotlin.native.concurrent.FreezingException: freezing of Continuation @ $onUnsubscribed$<anonymous>_6COROUTINE$16 has failed, first blocker is ArrayChannel@147048{ReceiveQueued}(buffer:capacity=64,size=0)
Trying to figure out which line crashes in
private suspend fun onUnsubscribed() {
      mutex.withLock {
        if (--activeSubscriptionCount == 0 && idleTimeoutMs > 0) {
          /*
          idleTimeoutJob?.cancel()
          connectionKeepAliveTimeoutJob?.cancel()

          idleTimeoutJob = coroutineScope.launch {
            delay(idleTimeoutMs)
            close()
          }

           */
        }
      }
    }
With all of those commented, the iOS app works.
So, after spending a bit of time with this block of code (above) in
ApolloWebSocketNetworkTransport.kt
I find that the iOS app crashes if there is any code in this lambda:
idleTimeoutJob = coroutineScope.launch {
    delay(idleTimeoutMs)
    close()
}
If I leave the
delay
or the
close()
in there, we have the same crash as above, a FreezingException. There is only two places in this code where coroutines are launched. I'm going to do some experimentation.
Reading through the code to find the source of the
coroutineScope
being used here, I found the execute() method can take a dispatcher, and I was able to get this subscription working using that. Testing some more to ensure it wasn't a fluke.
Definitely not a fluke, it seems. So, what have I done. I used Koin to inject ApolloClient to the rest of the code I am running. The subscription itself is probably not important, pretty standard. I made. The addition of the
executionContext
parameter to the
execute
method here is what stopped the crashes that I was seeing. I absolutely need the
flowOn
as well.
Found another crash in here, however, if I take my server down and try to connect, the iOS app crashes with:
Uncaught Kotlin exception: com.apollographql.apollo.ApolloWebSocketException: Failed to establish GraphQL websocket connection with the server, timeout.
kfun:com.apollographql.apollo.network.ws.ApolloWebSocketNetworkTransport.$openServerConnectionCOROUTINE$22.invokeSuspend#internal + 1269
Not sure where this exception is going because I don't seem to be able to catch it the same way I had for other classes.
This morning, I'm trying to figure out why the connection timeout exceptions aren't being handled and bubbled up for the websocket connections. I've discovered the root cause of this in the
getServerConnection
method of ApolloWebSocketNetworkTransport which creates a flow that internally calls
openServerConnection
. That method can throw ApolloWebSocketException. Those exceptions crash the app unless they are caught. Naively I added the following code to suppress them:
private fun getServerConnection(dispatcherContext: ApolloCoroutineDispatcherContext): Flow<GraphQLWebsocketConnection> {
    return flow {
      val connection = mutex.withLock {
        if (graphQLWebsocketConnection?.isClosedForReceive() != false) {
          graphQLWebsocketConnection = openServerConnection(dispatcherContext)
        }
        graphQLWebsocketConnection
      }
      emit(connection)
    }
        .catch { emit(null)}
        .filterNotNull()
  }
the
catch
at the bottom emits null which is immediately filtered. But this ultimately fixes the crash I'm currently seeing.
Clearly it'd be better to pass that exception up, and handle it as an error. I'll see if I can figure out a nice way to have this happen. the
flow {}
builder doesn't seem to have an emit error functionality though.
r

rocketraman

06/02/2021, 3:05 PM
Emit
Result
?
n

Neal Sanche

06/02/2021, 3:12 PM
Sorry Raman, are you saying modify openServerConnection to return a Result<GraphQLWebsocketConnection> instead? That way the error can be contained?
r

rocketraman

06/02/2021, 3:13 PM
I guess
Flow<Result<GraphQLWebsocketConnection>>
, yeah
n

Neal Sanche

06/02/2021, 3:22 PM
So, the above might become:
private fun getServerConnection(dispatcherContext: ApolloCoroutineDispatcherContext): Flow<Result<GraphQLWebsocketConnection?>> {
    return flow {
      emit(kotlin.runCatching {
        mutex.withLock {
          if (graphQLWebsocketConnection?.isClosedForReceive() != false) {
            graphQLWebsocketConnection = openServerConnection(dispatcherContext)
          }
          graphQLWebsocketConnection
        }
      })
    }
  }
But that leads to problems upstream of this, need to rewrite how this is consumed. Not sure why I have to handle nullable GraphQLWebsocketConnection here.
r

rocketraman

06/02/2021, 3:26 PM
Make it non-nullable and emit an error Result instead if it is null
n

Neal Sanche

06/02/2021, 3:26 PM
Doesn't runCatching do that?
r

rocketraman

06/02/2021, 3:27 PM
Yeah you could just throw with an elvis operator.
n

Neal Sanche

06/02/2021, 3:29 PM
Okay, like this then?
So the code that was consuming the serverConnection needs to be changed to handle a Result now. Working through that.
Not much luck so far. Getting hung up on exactly what to do with the error part of the Result.
Okay, I was able to get this working in a way. I'm still not sure why the exceptions that are getting thrown don't get passed up to iOS the way I expect they should. Using runCatching and then if there is an error, sending that error to the flow that execute produces in the errors array of the response works. But in this case the error is a timeout while trying to connect to the server, not an actual graphql error. It does work, though.
Okay, I've rolled out the changes I made, and have figured out how to do the error handling in a better way. The Apollo code as written works, and I've figured out workarounds for getting it running on iOS.
I was able to get this sort of code to work on iOS to consume a subscription. The difference here being the use of the
catch
operator if errors in connection need to be sent along with the rest of the flow. If it doesn't matter, the flow will just finish if the server can't be contacted, so nothing needs to be emitted.