Skip to content

Commit

Permalink
Merge branch 'main' into rl.maven.update.api
Browse files Browse the repository at this point in the history
  • Loading branch information
rlazo authored Nov 11, 2024
2 parents 2a13767 + f20340a commit df99435
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 116 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ import com.google.firebase.app
import com.google.firebase.initialize
import com.google.firebase.util.nextAlphanumericString
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
* A JUnit test rule that creates instances of [FirebaseApp] for use during testing, and closes them
Expand All @@ -37,6 +43,12 @@ class TestFirebaseAppFactory : FactoryTestRule<FirebaseApp, Nothing>() {
)

override fun destroyInstance(instance: FirebaseApp) {
instance.delete()
// Work around app crash due to IllegalStateException from FirebaseAuth if `delete()` is called
// very quickly after `FirebaseApp.getInstance()`. See b/378116261 for details.
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(Dispatchers.IO) {
delay(1.seconds)
instance.delete()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import com.google.android.gms.tasks.Tasks
import com.google.firebase.appcheck.AppCheckProvider
import com.google.firebase.appcheck.AppCheckProviderFactory
import com.google.firebase.appcheck.FirebaseAppCheck
import com.google.firebase.dataconnect.core.FirebaseDataConnectInternal
import com.google.firebase.dataconnect.generated.GeneratedConnector
import com.google.firebase.dataconnect.generated.GeneratedMutation
import com.google.firebase.dataconnect.generated.GeneratedQuery
Expand Down Expand Up @@ -137,6 +138,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
fun executeQueryShouldNotSendAuthMetadataWhenNotLoggedIn() = runTest {
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAuthReady()
val queryRef = dataConnect.query("qryfyk7yfppfe", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob = async { grpcServer.metadatas.first() }

Expand All @@ -149,6 +151,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
fun executeMutationShouldNotSendAuthMetadataWhenNotLoggedIn() = runTest {
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAuthReady()
val mutationRef =
dataConnect.mutation("mutckjpte9v9j", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob = async { grpcServer.metadatas.first() }
Expand All @@ -162,6 +165,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
fun executeQueryShouldSendAuthMetadataWhenLoggedIn() = runTest {
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAuthReady()
val queryRef = dataConnect.query("qryyarwrxe2fv", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob = async { grpcServer.metadatas.first() }
firebaseAuthSignIn(dataConnect)
Expand All @@ -175,6 +179,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
fun executeMutationShouldSendAuthMetadataWhenLoggedIn() = runTest {
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAuthReady()
val mutationRef =
dataConnect.mutation("mutayn7as5k7d", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob = async { grpcServer.metadatas.first() }
Expand All @@ -189,6 +194,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
fun executeQueryShouldNotSendAuthMetadataAfterLogout() = runTest {
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAuthReady()
val queryRef = dataConnect.query("qryyarwrxe2fv", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob1 = async { grpcServer.metadatas.first() }
val metadatasJob2 = async { grpcServer.metadatas.take(2).last() }
Expand All @@ -206,6 +212,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
fun executeMutationShouldNotSendAuthMetadataAfterLogout() = runTest {
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAuthReady()
val mutationRef =
dataConnect.mutation("mutvw945ag3vv", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob1 = async { grpcServer.metadatas.first() }
Expand All @@ -226,6 +233,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
// appcheck token is sent at all.
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAppCheckReady()
val queryRef = dataConnect.query("qrybbeekpkkck", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob = async { grpcServer.metadatas.first() }

Expand All @@ -240,6 +248,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
// appcheck token is sent at all.
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAppCheckReady()
val mutationRef =
dataConnect.mutation("mutbs7hhxk39c", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob = async { grpcServer.metadatas.first() }
Expand All @@ -253,6 +262,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
fun executeQueryShouldSendAppCheckMetadataWhenAppCheckIsEnabled() = runTest {
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAppCheckReady()
val queryRef = dataConnect.query("qryyarwrxe2fv", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob = async { grpcServer.metadatas.first() }
val appCheck = FirebaseAppCheck.getInstance(dataConnect.app)
Expand All @@ -267,6 +277,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() {
fun executeMutationShouldSendAppCheckMetadataWhenAppCheckIsEnabled() = runTest {
val grpcServer = inProcessDataConnectGrpcServer.newInstance()
val dataConnect = dataConnectFactory.newInstance(grpcServer)
(dataConnect as FirebaseDataConnectInternal).awaitAppCheckReady()
val mutationRef =
dataConnect.mutation("mutz4hzqzpgb4", Unit, serializer<Unit>(), serializer<Unit>())
val metadatasJob = async { grpcServer.metadatas.first() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield

Expand All @@ -58,6 +61,9 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(
val instanceId: String
get() = logger.nameWithId

private val _providerAvailable = MutableStateFlow(false)
val providerAvailable: StateFlow<Boolean> = _providerAvailable.asStateFlow()

@Suppress("LeakingThis") private val weakThis = WeakReference(this)

private val coroutineScope =
Expand Down Expand Up @@ -230,9 +236,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(

if (state.compareAndSet(oldState, State.Closed)) {
providerListenerPair?.run {
provider?.let { provider ->
runIgnoringFirebaseAppDeleted { removeTokenListener(provider, tokenListener) }
}
provider?.let { provider -> removeTokenListener(provider, tokenListener) }
}
return
}
Expand Down Expand Up @@ -416,7 +420,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(
@DeferredApi
private fun onProviderAvailable(newProvider: T, tokenListener: L) {
logger.debug { "onProviderAvailable(newProvider=$newProvider)" }
runIgnoringFirebaseAppDeleted { addTokenListener(newProvider, tokenListener) }
addTokenListener(newProvider, tokenListener)

while (true) {
val oldState = state.get()
Expand All @@ -431,7 +435,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(
"onProviderAvailable(newProvider=$newProvider)" +
" unregistering token listener that was just added"
}
runIgnoringFirebaseAppDeleted { removeTokenListener(newProvider, tokenListener) }
removeTokenListener(newProvider, tokenListener)
break
}
is State.Ready ->
Expand All @@ -448,6 +452,8 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(
break
}
}

_providerAvailable.value = true
}

/**
Expand Down Expand Up @@ -478,20 +484,6 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any, L : Any>(
private class GetTokenCancelledException(cause: Throwable) :
DataConnectException("getToken() was cancelled, likely by close()", cause)

// Work around a race condition where addIdTokenListener() and removeIdTokenListener() throw if
// the FirebaseApp is deleted during or before its invocation.
private fun runIgnoringFirebaseAppDeleted(block: () -> Unit) {
try {
block()
} catch (e: IllegalStateException) {
if (e.message == "FirebaseApp was deleted") {
logger.warn(e) { "ignoring exception: $e" }
} else {
throw e
}
}
}

protected data class GetTokenResult(val token: String?)

private companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
Expand All @@ -72,6 +74,9 @@ internal interface FirebaseDataConnectInternal : FirebaseDataConnect {

val lazyGrpcClient: SuspendingLazy<DataConnectGrpcClient>
val lazyQueryManager: SuspendingLazy<QueryManager>

suspend fun awaitAuthReady()
suspend fun awaitAppCheckReady()
}

internal class FirebaseDataConnectImpl(
Expand Down Expand Up @@ -107,11 +112,18 @@ internal class FirebaseDataConnectImpl(
SupervisorJob() +
nonBlockingDispatcher +
CoroutineName(instanceId) +
CoroutineExceptionHandler { _, throwable ->
logger.warn(throwable) { "uncaught exception from a coroutine" }
CoroutineExceptionHandler { context, throwable ->
logger.warn(throwable) {
val coroutineName = context[CoroutineName]?.name
"WARNING: uncaught exception from coroutine named \"$coroutineName\" " +
"(error code jszxcbe37k)"
}
}
)

private val authProviderAvailable = MutableStateFlow(false)
private val appCheckProviderAvailable = MutableStateFlow(false)

// Protects `closed`, `grpcClient`, `emulatorSettings`, and `queryManager`.
private val mutex = Mutex()

Expand All @@ -121,29 +133,49 @@ internal class FirebaseDataConnectImpl(
// All accesses to this variable _must_ have locked `mutex`.
private var closed = false

private val lazyDataConnectAuth =
SuspendingLazy(mutex) {
if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed")
DataConnectAuth(
deferredAuthProvider = deferredAuthProvider,
parentCoroutineScope = coroutineScope,
blockingDispatcher = blockingDispatcher,
logger = Logger("DataConnectAuth").apply { debug { "created by $instanceId" } },
)
.apply { initialize() }
private val dataConnectAuth: DataConnectAuth =
DataConnectAuth(
deferredAuthProvider = deferredAuthProvider,
parentCoroutineScope = coroutineScope,
blockingDispatcher = blockingDispatcher,
logger = Logger("DataConnectAuth").apply { debug { "created by $instanceId" } },
)

override suspend fun awaitAuthReady() {
authProviderAvailable.first { it }
}

init {
coroutineScope.launch(CoroutineName("DataConnectAuth initializer for $instanceId")) {
dataConnectAuth.initialize()
dataConnectAuth.providerAvailable.collect { isProviderAvailable ->
logger.debug { "authProviderAvailable=$isProviderAvailable" }
authProviderAvailable.value = isProviderAvailable
}
}
}

private val lazyDataConnectAppCheck =
SuspendingLazy(mutex) {
if (closed) throw IllegalStateException("FirebaseDataConnect instance has been closed")
DataConnectAppCheck(
deferredAppCheckTokenProvider = deferredAppCheckProvider,
parentCoroutineScope = coroutineScope,
blockingDispatcher = blockingDispatcher,
logger = Logger("DataConnectAppCheck").apply { debug { "created by $instanceId" } },
)
.apply { initialize() }
private val dataConnectAppCheck: DataConnectAppCheck =
DataConnectAppCheck(
deferredAppCheckTokenProvider = deferredAppCheckProvider,
parentCoroutineScope = coroutineScope,
blockingDispatcher = blockingDispatcher,
logger = Logger("DataConnectAppCheck").apply { debug { "created by $instanceId" } },
)

override suspend fun awaitAppCheckReady() {
appCheckProviderAvailable.first { it }
}

init {
coroutineScope.launch(CoroutineName("DataConnectAppCheck initializer for $instanceId")) {
dataConnectAppCheck.initialize()
dataConnectAppCheck.providerAvailable.collect { isProviderAvailable ->
logger.debug { "appCheckProviderAvailable=$isProviderAvailable" }
appCheckProviderAvailable.value = isProviderAvailable
}
}
}

private val lazyGrpcRPCs =
SuspendingLazy(mutex) {
Expand Down Expand Up @@ -181,8 +213,8 @@ internal class FirebaseDataConnectImpl(
val grpcMetadata =
DataConnectGrpcMetadata.forSystemVersions(
firebaseApp = app,
dataConnectAuth = lazyDataConnectAuth.getLocked(),
dataConnectAppCheck = lazyDataConnectAppCheck.getLocked(),
dataConnectAuth = dataConnectAuth,
dataConnectAppCheck = dataConnectAppCheck,
connectorLocation = config.location,
parentLogger = logger,
)
Expand Down Expand Up @@ -210,8 +242,8 @@ internal class FirebaseDataConnectImpl(
projectId = projectId,
connector = config,
grpcRPCs = lazyGrpcRPCs.getLocked(),
dataConnectAuth = lazyDataConnectAuth.getLocked(),
dataConnectAppCheck = lazyDataConnectAppCheck.getLocked(),
dataConnectAuth = dataConnectAuth,
dataConnectAppCheck = dataConnectAppCheck,
logger = Logger("DataConnectGrpcClient").apply { debug { "created by $instanceId" } },
)
}
Expand Down Expand Up @@ -397,8 +429,8 @@ internal class FirebaseDataConnectImpl(

// Close Auth and AppCheck synchronously to avoid race conditions with auth callbacks.
// Since close() is re-entrant, this is safe even if they have already been closed.
lazyDataConnectAuth.initializedValueOrNull?.close()
lazyDataConnectAppCheck.initializedValueOrNull?.close()
dataConnectAuth.close()
dataConnectAppCheck.close()

// Start the job to asynchronously close the gRPC client.
while (true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,43 +552,6 @@ class DataConnectAuthUnitTest {
)
}

@Test
fun `addIdTokenListener() throwing IllegalStateException due to FirebaseApp deleted should be ignored`() =
runTest {
every { mockInternalAuthProvider.addIdTokenListener(any()) } throws
firebaseAppDeletedException
coEvery { mockInternalAuthProvider.getAccessToken(any()) } returns taskForToken(accessToken)
val dataConnectAuth = newDataConnectAuth()
dataConnectAuth.initialize()
advanceUntilIdle()

eventually(`check every 100 milliseconds for 2 seconds`) {
mockLogger.shouldHaveLoggedExactlyOneMessageContaining(
"ignoring exception: $firebaseAppDeletedException"
)
}
val result = dataConnectAuth.getToken(requestId)
withClue("result=$result") { result shouldBe accessToken }
}

@Test
fun `removeIdTokenListener() throwing IllegalStateException due to FirebaseApp deleted should be ignored`() =
runTest {
every { mockInternalAuthProvider.removeIdTokenListener(any()) } throws
firebaseAppDeletedException
val dataConnectAuth = newDataConnectAuth()
dataConnectAuth.initialize()
advanceUntilIdle()

dataConnectAuth.close()

eventually(`check every 100 milliseconds for 2 seconds`) {
mockLogger.shouldHaveLoggedExactlyOneMessageContaining(
"ignoring exception: $firebaseAppDeletedException"
)
}
}

private fun TestScope.newDataConnectAuth(
deferredInternalAuthProvider: DeferredInternalAuthProvider =
ImmediateDeferred(mockInternalAuthProvider),
Expand Down
Loading

0 comments on commit df99435

Please sign in to comment.