diff --git a/CHANGELOG.md b/CHANGELOG.md index c67227d993..71a74f9073 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ ### Enhancements * Support for experimental K2-compilation with `kotlin.experimental.tryK2=true`. (Issue [#1483](https://github.com/realm/realm-kotlin/issues/1483)) +* [Sync] Added support for multiplexing sync connections. When enabled (the default), a single + connection is used per sync user rather than one per synchronized Realm. This + reduces resource consumption when multiple Realms are opened and will + typically improve performance. The behavior can be controlled through [AppConfiguration.Builder.enableSessionMultiplexing]. (Issue [#XXXX]()) +* [Sync] Various sync timeout options can now be configured through `AppConfiguration.Builder.syncTimeouts()`. (Issue [#971](https://github.com/realm/realm-kotlin/issues/971)). ### Fixed * None. diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 293190fe4d..f91aa76502 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -581,6 +581,12 @@ expect object RealmInterop { applicationInfo: String ) + fun realm_sync_client_config_set_connect_timeout(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) + fun realm_sync_client_config_set_connection_linger_time(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) + fun realm_sync_client_config_set_ping_keepalive_period(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) + fun realm_sync_client_config_set_pong_keepalive_timeout(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) + fun realm_sync_client_config_set_fast_reconnect_limit(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) + fun realm_sync_config_new( user: RealmUserPointer, partition: String diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 754b1dba70..82249b169a 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1301,6 +1301,26 @@ actual object RealmInterop { ) } + actual fun realm_sync_client_config_set_connect_timeout(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realmc.realm_sync_client_config_set_connect_timeout(syncClientConfig.cptr(), timeoutMs.toLong()) + } + + actual fun realm_sync_client_config_set_connection_linger_time(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realmc.realm_sync_client_config_set_connection_linger_time(syncClientConfig.cptr(), timeoutMs.toLong()) + } + + actual fun realm_sync_client_config_set_ping_keepalive_period(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realmc.realm_sync_client_config_set_ping_keepalive_period(syncClientConfig.cptr(), timeoutMs.toLong()) + } + + actual fun realm_sync_client_config_set_pong_keepalive_timeout(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realmc.realm_sync_client_config_set_pong_keepalive_timeout(syncClientConfig.cptr(), timeoutMs.toLong()) + } + + actual fun realm_sync_client_config_set_fast_reconnect_limit(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realmc.realm_sync_client_config_set_fast_reconnect_limit(syncClientConfig.cptr(), timeoutMs.toLong()) + } + actual fun realm_network_transport_new(networkTransport: NetworkTransport): RealmNetworkTransportPointer { return LongPointerWrapper(realmc.realm_network_transport_new(networkTransport)) } diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 213be632f4..18a2cfcf3b 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -2444,6 +2444,26 @@ actual object RealmInterop { ) } + actual fun realm_sync_client_config_set_connect_timeout(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realm_wrapper.realm_sync_client_config_set_connect_timeout(syncClientConfig.cptr(), timeoutMs) + } + + actual fun realm_sync_client_config_set_connection_linger_time(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realm_wrapper.realm_sync_client_config_set_connection_linger_time(syncClientConfig.cptr(), timeoutMs) + } + + actual fun realm_sync_client_config_set_ping_keepalive_period(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realm_wrapper.realm_sync_client_config_set_ping_keepalive_period(syncClientConfig.cptr(), timeoutMs) + } + + actual fun realm_sync_client_config_set_pong_keepalive_timeout(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realm_wrapper.realm_sync_client_config_set_pong_keepalive_timeout(syncClientConfig.cptr(), timeoutMs) + } + + actual fun realm_sync_client_config_set_fast_reconnect_limit(syncClientConfig: RealmSyncClientConfigurationPointer, timeoutMs: ULong) { + realm_wrapper.realm_sync_client_config_set_fast_reconnect_limit(syncClientConfig.cptr(), timeoutMs) + } + actual fun realm_sync_config_set_error_handler( syncConfig: RealmSyncConfigurationPointer, errorHandler: SyncErrorCallback diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt index 7b1f958385..e8626117ec 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/AppConfiguration.kt @@ -39,9 +39,14 @@ import io.realm.kotlin.mongodb.internal.AppConfigurationImpl import io.realm.kotlin.mongodb.internal.KtorNetworkTransport import io.realm.kotlin.mongodb.internal.LogObfuscatorImpl import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.mongodb.sync.SyncTimeoutOptions +import io.realm.kotlin.mongodb.sync.SyncTimeoutOptionsBuilder import kotlinx.coroutines.CoroutineDispatcher import org.mongodb.kbson.ExperimentalKBsonSerializerApi import org.mongodb.kbson.serialization.EJson +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds /** * An **AppConfiguration** is used to setup linkage to an Atlas App Services Application. @@ -102,6 +107,23 @@ public interface AppConfiguration { */ public val httpLogObfuscator: HttpLogObfuscator? + /** + * If enabled (the default), a single connection is used for all Realms opened + * with a single sync user. If disabled, a separate connection is used for each + * Realm. + * + * Session multiplexing reduces resources used and typically improves + * performance. When multiplexing is enabled, the connection is not immediately + * closed when the last session is closed, and instead remains open for + * [SyncTimeoutOptions.connectionLingerTime] defined in [syncTimeoutOptions]. + */ + public val enableSessionMultiplexing: Boolean + + /** + * The configured timeouts for various aspects of the sync connection from realms. + */ + public val syncTimeoutOptions: SyncTimeoutOptions + public companion object { /** * The default url for App Services applications. @@ -149,6 +171,8 @@ public interface AppConfiguration { private var httpLogObfuscator: HttpLogObfuscator? = LogObfuscatorImpl private val customRequestHeaders = mutableMapOf() private var authorizationHeaderName: String = DEFAULT_AUTHORIZATION_HEADER_NAME + private var enableSessionMultiplexing: Boolean = true + private var syncTimeoutOptions: SyncTimeoutOptions = SyncTimeoutOptionsBuilder().build() /** * Sets the encryption key used to encrypt the user metadata Realm only. Individual @@ -325,6 +349,33 @@ public interface AppConfiguration { this.ejson = ejson } + /** + * If enabled (the default), a single connection is used for all Realms opened + * with a single sync user. If disabled, a separate connection is used for each + * Realm. + * + * Session multiplexing reduces resources used and typically improves + * performance. When multiplexing is enabled, the connection is not immediately + * closed when the last session is closed, and instead remains open for + * [SyncTimeoutOptions.connectionLingerTime] as defined by [syncTimeouts] (30 seconds by + * default). + */ + public fun enableSessionMultiplexing(enabled: Boolean): Builder { + this.enableSessionMultiplexing = enabled + return this + } + + /** + * Configure the assorted types of connection timeouts for sync connections. + * See [SyncTimeoutOptionsBuilder] for a description of each option. + */ + public fun syncTimeouts(action: SyncTimeoutOptionsBuilder.()->Unit): Builder { + val builder = SyncTimeoutOptionsBuilder() + action(builder) + syncTimeoutOptions = builder.build() + return this + } + /** * Allows defining a custom network transport. It is used by some tests that require simulating * network responses. @@ -411,6 +462,8 @@ public interface AppConfiguration { httpLogObfuscator = httpLogObfuscator, customRequestHeaders = customRequestHeaders, authorizationHeaderName = authorizationHeaderName, + enableSessionMultiplexing = enableSessionMultiplexing, + syncTimeoutOptions = syncTimeoutOptions, ) } } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt index cb8fa12c7a..f209ac2796 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt @@ -35,6 +35,7 @@ import io.realm.kotlin.internal.util.DispatcherHolder import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.AppConfiguration.Companion.DEFAULT_BASE_URL import io.realm.kotlin.mongodb.HttpLogObfuscator +import io.realm.kotlin.mongodb.sync.SyncTimeoutOptions import org.mongodb.kbson.ExperimentalKBsonSerializerApi import org.mongodb.kbson.serialization.EJson @@ -56,6 +57,8 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) override val httpLogObfuscator: HttpLogObfuscator?, override val customRequestHeaders: Map, override val authorizationHeaderName: String, + override val enableSessionMultiplexing: Boolean, + override val syncTimeoutOptions: SyncTimeoutOptions, ) : AppConfiguration { /** @@ -154,9 +157,6 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) syncRootDirectory ) - // Disable multiplexing. See https://github.com/realm/realm-core/issues/6656 - RealmInterop.realm_sync_client_config_set_multiplex_sessions(syncClientConfig, false) - encryptionKey?.let { RealmInterop.realm_sync_client_config_set_metadata_encryption_key( syncClientConfig, @@ -177,6 +177,31 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) it ) } + + // Setup multiplexing + RealmInterop.realm_sync_client_config_set_multiplex_sessions(syncClientConfig, enableSessionMultiplexing) + + // Setup SyncTimeoutOptions + RealmInterop.realm_sync_client_config_set_connect_timeout( + syncClientConfig, + syncTimeoutOptions.connectTimeout.inWholeMilliseconds.toULong() + ) + RealmInterop.realm_sync_client_config_set_connection_linger_time( + syncClientConfig, + syncTimeoutOptions.connectionLingerTime.inWholeMilliseconds.toULong() + ) + RealmInterop.realm_sync_client_config_set_ping_keepalive_period( + syncClientConfig, + syncTimeoutOptions.pingKeepalivePeriod.inWholeMilliseconds.toULong() + ) + RealmInterop.realm_sync_client_config_set_pong_keepalive_timeout( + syncClientConfig, + syncTimeoutOptions.pongKeepalivePeriod.inWholeMilliseconds.toULong() + ) + RealmInterop.realm_sync_client_config_set_fast_reconnect_limit( + syncClientConfig, + syncTimeoutOptions.fastReconnectLimit.inWholeMilliseconds.toULong() + ) } internal companion object { diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncTimeoutOptions.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncTimeoutOptions.kt new file mode 100644 index 0000000000..6b46f0233c --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncTimeoutOptions.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.sync + +import kotlin.time.Duration + +/** + * The configured timeouts for various aspects of the sync connection between synchronized realms + * and App Services. + */ +public data class SyncTimeoutOptions( + + /** + * The maximum time to allow for a connection to become fully established. This includes + * the time to resolve the network address, the TCP connect operation, the SSL + * handshake, and the WebSocket handshake. + */ + val connectTimeout: Duration, + + /** + * If session multiplexing is enabled, how long to keep connections open while there are + * no active session. + */ + val connectionLingerTime: Duration, + + /** + * How long to wait between each ping message sent to the server. The client periodically + * sends ping messages to the server to check if the connection is still alive. Shorter + * periods make connection state change notifications more responsive at the cost of + * battery life (as the antenna will have to wake up more often). + */ + val pingKeepalivePeriod: Duration, + + /** + * How long to wait for the server to respond to a ping message. Shorter values make + * connection state change notifications more responsive, but increase the chance of + * spurious disconnections. + */ + val pongKeepalivePeriod: Duration, + + /** + * When a client first connects to the server, it downloads all data from the server + * before it begins to upload local changes. This typically reduces the total amount + * of merging needed and gets the local client into a useful state faster. If a + * disconnect and reconnect happens within the time span of the fast reconnect limit, + * this is skipped and the session behaves as if it were continuously + * connected. + */ + val fastReconnectLimit: Duration +) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncTimeoutOptionsBuilder.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncTimeoutOptionsBuilder.kt new file mode 100644 index 0000000000..74e1831423 --- /dev/null +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/sync/SyncTimeoutOptionsBuilder.kt @@ -0,0 +1,129 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.mongodb.sync + +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +/** + * Builder for configuring various timeouts related to the sync connection to the server. + * + * @see [io.realm.kotlin.mongodb.AppConfiguration.Builder.syncTimeouts] + */ +public class SyncTimeoutOptionsBuilder { + + /** + * The maximum time to allow for a connection to become fully established. This includes + * the time to resolve the network address, the TCP connect operation, the SSL + * handshake, and the WebSocket handshake. + * + * Only values >= 1 second is allowed. Default is 2 minutes. + * + * @throws IllegalArgumentException if the duration is outside the allowed range. + */ + public var connectTimeout: Duration = 2.minutes + set(value) { + if (value < 1.seconds) { + throw IllegalArgumentException("connectTimeout only support durations >= 1 second. This was: $value") + } + field = value + } + + /** + * If session multiplexing is enabled, how long to keep connections open while there are + * no active session. + * + * Only durations > 0 seconds are allowed. Default is 30 seconds. + * + * @throws IllegalArgumentException if the duration is outside the allowed range. + * @see io.realm.kotlin.mongodb.AppConfiguration.Builder.enableSessionMultiplexing + */ + public var connectionLingerTime: Duration = 30.seconds + set(value) { + if (value <= 0.milliseconds) { + throw IllegalArgumentException("connectionLingerTime must be a positive duration > 0. This was: $value") + } + field = value + } + + /** + * How long to wait between each ping message sent to the server. The client periodically + * sends ping messages to the server to check if the connection is still alive. Shorter + * periods make connection state change notifications more responsive at the cost of + * battery life (as the antenna will have to wake up more often). + * + * Only durations > 5 seconds are allowed. Default is 1 minute. + * + * @throws IllegalArgumentException if the duration is outside the allowed range. + */ + public var pingKeepalivePeriod: Duration = 1.minutes + set(value) { + if (value <= 5.seconds) { + throw IllegalArgumentException("pingKeepalivePeriod must be a positive duration > 5 seconds. This was: $value") + } + field = value + } + + /** + * How long to wait for the server to respond to a ping message. Shorter values make + * connection state change notifications more responsive, but increase the chance of + * spurious disconnections. + * + * Only durations > 5 seconds are allowed. Default is 2 minutes. + * + * @throws IllegalArgumentException if the duration is outside the allowed range. + */ + public var pongKeepalivePeriod: Duration = 2.minutes + set(value) { + if (value <= 5.seconds) { + throw IllegalArgumentException("pongKeepalivePeriod must be a positive duration > 5 seconds. This was: $value") + } + field = value + } + + /** + * When a client first connects to the server, it downloads all data from the server + * before it begins to upload local changes. This typically reduces the total amount + * of merging needed and gets the local client into a useful state faster. If a + * disconnect and reconnect happens within the time span of the fast reconnect limit, + * this is skipped and the session behaves as if it were continuously + * connected. + * + * Only durations > 1 second are allowed. Default is 1 minute. + * + * @throws IllegalArgumentException if the duration is outside the allowed range. + */ + public var fastReconnectLimit: Duration = 1.minutes + set(value) { + if (value <= 1.seconds) { + throw IllegalArgumentException("fastReconnectLimit must be a positive duration > 1 second. This was: $value") + } + field = value + } + + /** + * Construct the final [SyncTimeoutOptions] object. + */ + internal fun build() = SyncTimeoutOptions( + connectTimeout = this.connectTimeout, + connectionLingerTime = this.connectionLingerTime, + pingKeepalivePeriod = this.pingKeepalivePeriod, + pongKeepalivePeriod = this.pongKeepalivePeriod, + fastReconnectLimit = this.fastReconnectLimit + ) +} diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt index 3e73576d1b..8093d2a21d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppConfigurationTests.kt @@ -41,12 +41,15 @@ import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertFalse import kotlin.test.assertIs import kotlin.test.assertNotEquals import kotlin.test.assertNotNull import kotlin.test.assertNotSame import kotlin.test.assertNull import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds private const val CUSTOM_HEADER_NAME = "Foo" private const val CUSTOM_HEADER_VALUE = "bar" @@ -490,4 +493,51 @@ class AppConfigurationTests { @Ignore // TODO fun dispatcher() { } + + @Test + fun multiplexing_default() { + val config = AppConfiguration.Builder("foo").build() + assertTrue(config.enableSessionMultiplexing) + } + + @Test + fun multiplexing() { + val config = AppConfiguration.Builder("foo") + .enableSessionMultiplexing(false) + .build() + assertFalse(config.enableSessionMultiplexing) + } + + @Test + fun syncTimeOutOptions_default() { + val config = AppConfiguration.Builder("foo").build() + with(config.syncTimeoutOptions) { + assertEquals(2.minutes, connectTimeout) + assertEquals(30.seconds, connectionLingerTime) + assertEquals(1.minutes, pingKeepalivePeriod) + assertEquals(2.minutes, pongKeepalivePeriod) + assertEquals(1.minutes, fastReconnectLimit) + } + } + + @Test + fun syncTimeOutOptions() { + val config = AppConfiguration.Builder("foo") + .syncTimeouts { + connectTimeout = 1.seconds + connectionLingerTime = 1.seconds + pingKeepalivePeriod = 1.seconds + pongKeepalivePeriod = 1.seconds + fastReconnectLimit = 1.seconds + } + .build() + with(config.syncTimeoutOptions) { + assertEquals(1.seconds, connectTimeout) + assertEquals(1.seconds, connectionLingerTime) + assertEquals(1.seconds, pingKeepalivePeriod) + assertEquals(1.seconds, pongKeepalivePeriod) + assertEquals(1.seconds, fastReconnectLimit) + } + } + }