diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index 5695a672a32..6d203c6c1da 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -11,6 +11,9 @@ to enable metrics collection in cloud monitoring. ([#8486](https://github.com/firebase/firebase-android-sdk/pull/8486), [#8495](https://github.com/firebase/firebase-android-sdk/pull/8495)) +- [changed] Add grpc request header for SQL Connect server affinity, to improve + server resource usage efficiency and performance. + ([#8499](https://github.com/firebase/firebase-android-sdk/pull/8499)) - [changed] Wait for 15 seconds before closing realtime streaming connection with backend after last subscriber unsubscribes (instead of closing the connection immediately). diff --git a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/GrpcMetadataIntegrationTest.kt b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/GrpcMetadataIntegrationTest.kt index 5b2854db6f3..eb82b6e574e 100644 --- a/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/GrpcMetadataIntegrationTest.kt +++ b/firebase-dataconnect/src/androidTest/kotlin/com/google/firebase/dataconnect/GrpcMetadataIntegrationTest.kt @@ -329,6 +329,7 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { googRequestParamsHeader.name(), googApiClientHeader.name(), clientVersionHeader.name(), + sqlConnectAffinityHeader.name(), ) assertSoftly { // Do not verify "x-firebase-auth-token" here since that header is effectively tested by @@ -338,6 +339,8 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { metadata.get(googApiClientHeader) shouldBe expectedGoogApiClientHeader(isFromGeneratedSdk) metadata.get(gmpAppIdHeader) shouldBe expectedAppId metadata.get(clientVersionHeader) shouldBe "android/${BuildConfig.VERSION_NAME}" + metadata.get(sqlConnectAffinityHeader) shouldBe + "${dataConnect.app.options.projectId}${dataConnect.config.serviceId}" } } } @@ -479,6 +482,10 @@ class GrpcMetadataIntegrationTest : DataConnectIntegrationTestBase() { val clientVersionHeader: Metadata.Key = Metadata.Key.of("x-client-version", Metadata.ASCII_STRING_MARSHALLER) + @Suppress("SpellCheckingInspection") + val sqlConnectAffinityHeader: Metadata.Key = + Metadata.Key.of("x-firebase-sqlconnect-affinity", Metadata.ASCII_STRING_MARSHALLER) + private val gmpAppIdHeader: Metadata.Key = Metadata.Key.of("x-firebase-gmpid", Metadata.ASCII_STRING_MARSHALLER) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt index 7360a4f435b..452b533daa6 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt @@ -31,11 +31,13 @@ import io.grpc.Metadata internal class DataConnectGrpcMetadata( val connectorLocation: String, + val connectorServiceId: String, val kotlinVersion: String, val androidVersion: Int, val dataConnectSdkVersion: String, val grpcVersion: String, val appId: String, + val projectId: String, val parentLogger: Logger, ) { private val logger = @@ -43,11 +45,13 @@ internal class DataConnectGrpcMetadata( debug { "created by ${parentLogger.nameWithId} with" + " connectorLocation=$connectorLocation" + + " connectorServiceId=$connectorServiceId" + " kotlinVersion=$kotlinVersion" + " androidVersion=$androidVersion" + " dataConnectSdkVersion=$dataConnectSdkVersion" + " grpcVersion=$grpcVersion" + - " appId=$appId" + " appId=$appId" + + " projectId=$projectId" } } val instanceId: String @@ -84,6 +88,7 @@ internal class DataConnectGrpcMetadata( it.put(googRequestParamsHeader, googRequestParamsHeaderValue) it.put(googApiClientHeader, googApiClientHeaderValue(callerSdkType)) it.put(clientVersionHeader, "android/$dataConnectSdkVersion") + it.put(sqlConnectAffinityHeader, "$projectId$connectorServiceId") if (appId.isNotBlank()) { it.put(gmpAppIdHeader, appId) } @@ -190,18 +195,26 @@ internal class DataConnectGrpcMetadata( private val gmpAppIdHeader: Metadata.Key = Metadata.Key.of("x-firebase-gmpid", Metadata.ASCII_STRING_MARSHALLER) + @Suppress("SpellCheckingInspection") + private val sqlConnectAffinityHeader: Metadata.Key = + Metadata.Key.of("x-firebase-sqlconnect-affinity", Metadata.ASCII_STRING_MARSHALLER) + fun forSystemVersions( firebaseApp: FirebaseApp, + projectId: String, connectorLocation: String, + connectorServiceId: String, parentLogger: Logger, ): DataConnectGrpcMetadata = DataConnectGrpcMetadata( connectorLocation = connectorLocation, + connectorServiceId = connectorServiceId, kotlinVersion = "${KotlinVersion.CURRENT}", androidVersion = Build.VERSION.SDK_INT, dataConnectSdkVersion = BuildConfig.VERSION_NAME, grpcVersion = "", // no way to get the grpc version at runtime, appId = firebaseApp.options.applicationId, + projectId = projectId, parentLogger = parentLogger, ) } diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt index d13d97ba35c..bcda1282984 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/FirebaseDataConnectImpl.kt @@ -299,7 +299,9 @@ internal class FirebaseDataConnectImpl( val grpcMetadata = DataConnectGrpcMetadata.forSystemVersions( firebaseApp = app, + projectId = projectId, connectorLocation = config.location, + connectorServiceId = config.serviceId, parentLogger = logger, ) val dataConnectGrpcRPCs = diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt index 0e097d05fbf..195cb379030 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt @@ -154,6 +154,18 @@ class DataConnectGrpcMetadataUnitTest { getExpectedHeaderValue = { "tvsxjeb745.appId" }, ) + @Test + fun `should include x-firebase-sqlconnect-affinity`() = + testMetadataIncludesHeader( + dataConnectGrpcMetadataArb = + Arb.dataConnect.dataConnectGrpcMetadata( + projectId = Arb.constant("tvsxjeb745.projectId"), + connectorServiceId = Arb.constant("q8mgtztcz2"), + ), + headerName = "x-firebase-sqlconnect-affinity", + getExpectedHeaderValue = { "tvsxjeb745.projectIdq8mgtztcz2" }, + ) + private fun testMetadataIncludesHeader( dataConnectGrpcMetadataArb: Arb = Arb.dataConnect.dataConnectGrpcMetadata(), @@ -359,16 +371,22 @@ class DataConnectGrpcMetadataUnitTest { @Test fun `forSystemVersions() should return correct values`() = runTest { val connectorLocation = Arb.dataConnect.connectorLocation().next() + val connectorServiceId = Arb.dataConnect.connectorServiceId().next() + val projectId = Arb.dataConnect.projectId().next() val dataConnectGrpcMetadata = DataConnectGrpcMetadata.forSystemVersions( firebaseApp = firebaseAppFactory.newInstance(), + projectId = projectId, connectorLocation = connectorLocation, + connectorServiceId = connectorServiceId, parentLogger = mockk(relaxed = true), ) dataConnectGrpcMetadata.asClue { it.connectorLocation shouldBeSameInstanceAs connectorLocation + it.connectorServiceId shouldBeSameInstanceAs connectorServiceId + it.projectId shouldBeSameInstanceAs projectId it.kotlinVersion shouldBe "${KotlinVersion.CURRENT}" it.androidVersion shouldBe Build.VERSION.SDK_INT it.dataConnectSdkVersion shouldBe BuildConfig.VERSION_NAME diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCsUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCsUnitTest.kt index aa03817c231..d3af81a364e 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCsUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcRPCsUnitTest.kt @@ -850,6 +850,33 @@ class DataConnectGrpcRPCsUnitTest { getExpectedHeaderValue = { "android/${it.grpcMetadata.dataConnectSdkVersion}" }, ) + @Test + fun `executeQuery sends x-firebase-sqlconnect-affinity header`() = + testExecuteQuerySendsHeader( + headerName = "x-firebase-sqlconnect-affinity", + getExpectedHeaderValue = { + "${it.grpcMetadata.projectId}${it.grpcMetadata.connectorServiceId}" + }, + ) + + @Test + fun `executeMutation sends x-firebase-sqlconnect-affinity header`() = + testExecuteMutationSendsHeader( + headerName = "x-firebase-sqlconnect-affinity", + getExpectedHeaderValue = { + "${it.grpcMetadata.projectId}${it.grpcMetadata.connectorServiceId}" + }, + ) + + @Test + fun `connect sends x-firebase-sqlconnect-affinity header`() = + testConnectSendsHeader( + headerName = "x-firebase-sqlconnect-affinity", + getExpectedHeaderValue = { + "${it.grpcMetadata.projectId}${it.grpcMetadata.connectorServiceId}" + }, + ) + private fun testExecuteQuerySendsHeader( headerName: String, getExpectedHeaderValue: (DataConnectGrpcRPCs) -> String, diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt index e9f78da26bd..73385909c26 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt @@ -71,19 +71,23 @@ import kotlinx.serialization.modules.SerializersModule internal fun DataConnectArb.dataConnectGrpcMetadata( connectorLocation: Arb = connectorLocation(), + connectorServiceId: Arb = connectorServiceId(), kotlinVersion: Arb = Arb.string(size = 8, Codepoint.alphanumeric()), androidVersion: Arb = Arb.int(0..100), dataConnectSdkVersion: Arb = Arb.string(size = 8, Codepoint.alphanumeric()), grpcVersion: Arb = Arb.string(size = 8, Codepoint.alphanumeric()), appId: Arb = Arb.string(size = 8, Codepoint.alphanumeric()), + projectId: Arb = Arb.string(size = 8, Codepoint.alphanumeric()), ): Arb = arbitrary { DataConnectGrpcMetadata( connectorLocation = connectorLocation.bind(), + connectorServiceId = connectorServiceId.bind(), kotlinVersion = kotlinVersion.bind(), androidVersion = androidVersion.bind(), dataConnectSdkVersion = dataConnectSdkVersion.bind(), grpcVersion = grpcVersion.bind(), appId = appId.bind(), + projectId = projectId.bind(), parentLogger = mockk(relaxed = true), ) }