From 5d7bd59e536c22b77042a52d0f6c2729493ee6a3 Mon Sep 17 00:00:00 2001 From: Nikola Irinchev Date: Tue, 30 Jul 2024 12:20:35 +0200 Subject: [PATCH] Expose sync timeout options --- CHANGELOG.md | 4 +- packages/realm_dart/lib/src/app.dart | 95 +++++++++++++++++-- .../realm_dart/lib/src/configuration.dart | 32 ++++--- .../lib/src/handles/native/app_handle.dart | 7 ++ .../lib/src/handles/native/config_handle.dart | 2 + packages/realm_dart/lib/src/realm_class.dart | 3 +- packages/realm_dart/test/app_test.dart | 29 ++++++ packages/realm_dart/test/baas_helper.dart | 1 - 8 files changed, 147 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5f839c75..b0bff2f24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ ## vNext (TBD) ### Enhancements -* None +* Added a new parameter of type `SyncTimeoutOptions` to `AppConfiguration`. It allows users to control sync timings, such as ping/pong intervals as well various connection timeouts. (Issue [#1763](https://github.com/realm/realm-dart/issues/1763)) +* Added a new parameter `cancelAsyncOperationsOnNonFatalErrors` on `Configuration.flexibleSync` that allows users to control whether non-fatal errors such as connection timeouts should be surfaced in the form of errors or if sync should try and reconnect in the background. (PR [#1764](https://github.com/realm/realm-dart/pulls/1764)) ### Fixed * Fixed an issue where creating a flexible sync configuration with an embedded object not referenced by any top-level object would throw a "No such table" exception with no meaningful information about the issue. Now a `RealmException` will be thrown that includes the offending object name, as well as more precise text for what the root cause of the error is. (PR [#1748](https://github.com/realm/realm-dart/pull/1748)) +* `AppConfiguration.maxConnectionTimeout` never had any effect and has been deprecated in favor of `SyncTimeoutOptions.connectTimeout`. (PR [#1764](https://github.com/realm/realm-dart/pulls/1764)) ### Compatibility * Realm Studio: 15.0.0 or later. diff --git a/packages/realm_dart/lib/src/app.dart b/packages/realm_dart/lib/src/app.dart index 91dc6c734..a8878e8d4 100644 --- a/packages/realm_dart/lib/src/app.dart +++ b/packages/realm_dart/lib/src/app.dart @@ -15,6 +15,77 @@ import 'handles/realm_core.dart'; import 'logging.dart'; import 'user.dart'; +/// Options for configuring timeouts and intervals used by the sync client. +@immutable +class SyncTimeoutOptions { + /// Controls the maximum amount of 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. + /// + /// Defaults to 2 minutes. + final Duration connectTimeout; // TimeSpan.FromMinutes(2); + + /// Controls the amount of time to keep a connection open after all + /// sessions have been abandoned. + /// + /// After all synchronized Realms have been closed for a given server, the + /// connection is kept open until the linger time has expired to avoid the + /// overhead of reestablishing the connection when Realms are being closed and + /// reopened. + /// + /// Defaults to 30 seconds. + final Duration connectionLingerTime; + + /// Controls how long to wait between each heartbeat ping message. + /// + /// 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). + /// + /// Defaults to 1 minute. + final Duration pingKeepAlivePeriod; + + /// Controls how long to wait for a reponse to a heartbeat ping before + /// concluding that the connection has dropped. + /// + /// Shorter values will make connection state change notifications more + /// responsive as it will only change to `disconnected` after this much time has + /// elapsed, but overly short values may result in spurious disconnection + /// notifications when the server is simply taking a long time to respond. + /// + /// Defaults to 2 minutes. + final Duration pongKeepAliveTimeout; + + /// Controls the maximum amount of time since the loss of a + /// prior connection, for a new connection to be considered a "fast + /// reconnect". + /// + /// When a client first connects to the server, it defers uploading any local + /// changes until it has downloaded all changesets from the server. This + /// typically reduces the total amount of merging that has to be done, and is + /// particularly beneficial the first time that a specific client ever connects + /// to the server. + /// + /// When an existing client disconnects and then reconnects within the "fact + /// reconnect" time this is skipped and any local changes are uploaded + /// immediately without waiting for downloads, just as if the client was online + /// the whole time. + /// + /// Defaults to 1 minute. + final Duration fastReconnectLimit; + + const SyncTimeoutOptions( + {this.connectTimeout = const Duration(minutes: 2), + this.connectionLingerTime = const Duration(seconds: 30), + this.pingKeepAlivePeriod = const Duration(minutes: 1), + this.pongKeepAliveTimeout = const Duration(minutes: 2), + this.fastReconnectLimit = const Duration(minutes: 1)}); +} + /// A class exposing configuration options for an [App] /// {@category Application} @immutable @@ -41,6 +112,7 @@ class AppConfiguration { /// become fully established. This includes the time to resolve the /// network address, the TCP connect operation, the SSL handshake, and /// the WebSocket handshake. Defaults to 2 minutes. + @Deprecated('Use SyncTimeoutOptions.connectTimeout') final Duration maxConnectionTimeout; /// Enumeration that specifies how and if logged-in User objects are persisted across application launches. @@ -61,17 +133,20 @@ class AppConfiguration { /// a more complex networking setup. final Client httpClient; + /// Options for the assorted types of connection timeouts for sync connections opened for this app. + final SyncTimeoutOptions syncTimeoutOptions; + /// Instantiates a new [AppConfiguration] with the specified appId. - AppConfiguration( - this.appId, { - Uri? baseUrl, - String? baseFilePath, - this.defaultRequestTimeout = const Duration(seconds: 60), - this.metadataEncryptionKey, - this.metadataPersistenceMode = MetadataPersistenceMode.plaintext, - this.maxConnectionTimeout = const Duration(minutes: 2), - Client? httpClient, - }) : baseUrl = baseUrl ?? Uri.parse(realmCore.getDefaultBaseUrl()), + AppConfiguration(this.appId, + {Uri? baseUrl, + String? baseFilePath, + this.defaultRequestTimeout = const Duration(seconds: 60), + this.metadataEncryptionKey, + this.metadataPersistenceMode = MetadataPersistenceMode.plaintext, + @Deprecated('Use SyncTimeoutOptions.connectTimeout') this.maxConnectionTimeout = const Duration(minutes: 2), + Client? httpClient, + this.syncTimeoutOptions = const SyncTimeoutOptions()}) + : baseUrl = baseUrl ?? Uri.parse(realmCore.getDefaultBaseUrl()), baseFilePath = baseFilePath ?? path.dirname(Configuration.defaultRealmPath), httpClient = httpClient ?? defaultClient { if (appId == '') { diff --git a/packages/realm_dart/lib/src/configuration.dart b/packages/realm_dart/lib/src/configuration.dart index ffd299b70..e6aea1261 100644 --- a/packages/realm_dart/lib/src/configuration.dart +++ b/packages/realm_dart/lib/src/configuration.dart @@ -187,19 +187,18 @@ abstract class Configuration { int? maxNumberOfActiveVersions, ShouldCompactCallback? shouldCompactCallback, int schemaVersion = 0, + bool cancelAsyncOperationsOnNonFatalErrors = false, }) => - FlexibleSyncConfiguration._( - user, - schemaObjects, - fifoFilesFallbackPath: fifoFilesFallbackPath, - path: path, - encryptionKey: encryptionKey, - syncErrorHandler: syncErrorHandler, - clientResetHandler: clientResetHandler, - maxNumberOfActiveVersions: maxNumberOfActiveVersions, - shouldCompactCallback: shouldCompactCallback, - schemaVersion: schemaVersion, - ); + FlexibleSyncConfiguration._(user, schemaObjects, + fifoFilesFallbackPath: fifoFilesFallbackPath, + path: path, + encryptionKey: encryptionKey, + syncErrorHandler: syncErrorHandler, + clientResetHandler: clientResetHandler, + maxNumberOfActiveVersions: maxNumberOfActiveVersions, + shouldCompactCallback: shouldCompactCallback, + schemaVersion: schemaVersion, + cancelAsyncOperationsOnNonFatalErrors: cancelAsyncOperationsOnNonFatalErrors); /// Constructs a [DisconnectedSyncConfiguration] static DisconnectedSyncConfiguration disconnectedSync( @@ -351,6 +350,14 @@ class FlexibleSyncConfiguration extends Configuration { /// all subscriptions will be reset since they may not conform to the new schema. final int schemaVersion; + /// Controls whether async operations such as [Realm.open], [Session.waitForUpload], and [Session.waitForDownload] + /// should throw an error whenever a non-fatal error, such as timeout occurs. + /// + /// If set to `false`, non-fatal session errors will be ignored and sync will continue retrying the + /// connection under in the background. This means that in cases where the devie is offline, these operations + /// may take an indeterminate time to complete. + final bool cancelAsyncOperationsOnNonFatalErrors; + FlexibleSyncConfiguration._( this.user, super.schemaObjects, { @@ -362,6 +369,7 @@ class FlexibleSyncConfiguration extends Configuration { super.maxNumberOfActiveVersions, this.shouldCompactCallback, this.schemaVersion = 0, + this.cancelAsyncOperationsOnNonFatalErrors = false, }) : super._(); @override diff --git a/packages/realm_dart/lib/src/handles/native/app_handle.dart b/packages/realm_dart/lib/src/handles/native/app_handle.dart index 010022eeb..28c5c9e4e 100644 --- a/packages/realm_dart/lib/src/handles/native/app_handle.dart +++ b/packages/realm_dart/lib/src/handles/native/app_handle.dart @@ -438,6 +438,13 @@ _AppConfigHandle _createAppConfig(AppConfiguration configuration, HttpTransportH realmLib.realm_app_config_set_metadata_encryption_key(handle.pointer, configuration.metadataEncryptionKey!.toUint8Ptr(arena)); } + final syncClientConfig = realmLib.realm_app_config_get_sync_client_config(handle.pointer); + realmLib.realm_sync_client_config_set_connect_timeout(syncClientConfig, configuration.syncTimeoutOptions.connectTimeout.inMilliseconds); + realmLib.realm_sync_client_config_set_connection_linger_time(syncClientConfig, configuration.syncTimeoutOptions.connectionLingerTime.inMilliseconds); + realmLib.realm_sync_client_config_set_ping_keepalive_period(syncClientConfig, configuration.syncTimeoutOptions.pingKeepAlivePeriod.inMilliseconds); + realmLib.realm_sync_client_config_set_pong_keepalive_timeout(syncClientConfig, configuration.syncTimeoutOptions.pongKeepAliveTimeout.inMilliseconds); + realmLib.realm_sync_client_config_set_fast_reconnect_limit(syncClientConfig, configuration.syncTimeoutOptions.fastReconnectLimit.inMilliseconds); + return handle; }); } diff --git a/packages/realm_dart/lib/src/handles/native/config_handle.dart b/packages/realm_dart/lib/src/handles/native/config_handle.dart index 6d7b41ff7..b166d70b6 100644 --- a/packages/realm_dart/lib/src/handles/native/config_handle.dart +++ b/packages/realm_dart/lib/src/handles/native/config_handle.dart @@ -91,6 +91,8 @@ class ConfigHandle extends HandleBase { try { realmLib.realm_sync_config_set_session_stop_policy(syncConfigPtr, config.sessionStopPolicy.index); realmLib.realm_sync_config_set_resync_mode(syncConfigPtr, config.clientResetHandler.clientResyncMode.index); + realmLib.realm_sync_config_set_cancel_waits_on_nonfatal_error(syncConfigPtr, config.cancelAsyncOperationsOnNonFatalErrors); + final errorHandlerCallback = Pointer.fromFunction, realm_sync_error_t)>(_syncErrorHandlerCallback); final errorHandlerUserdata = realmLib.realm_dart_userdata_async_new(config, errorHandlerCallback.cast(), schedulerHandle.pointer); diff --git a/packages/realm_dart/lib/src/realm_class.dart b/packages/realm_dart/lib/src/realm_class.dart index 846f9e43b..aa2c6b787 100644 --- a/packages/realm_dart/lib/src/realm_class.dart +++ b/packages/realm_dart/lib/src/realm_class.dart @@ -59,7 +59,7 @@ export 'package:realm_common/realm_common.dart' Uuid; // always expose with `show` to explicitly control the public API surface -export 'app.dart' show AppException, App, MetadataPersistenceMode, AppConfiguration; +export 'app.dart' show AppException, App, MetadataPersistenceMode, AppConfiguration, SyncTimeoutOptions; export 'collections.dart' show Move; export "configuration.dart" show @@ -179,7 +179,6 @@ class Realm { return await CancellableFuture.value(realm, cancellationToken); } - final asyncOpenHandle = AsyncOpenTaskHandle.from(config); return await CancellableFuture.from(() async { if (cancellationToken != null && cancellationToken.isCancelled) { diff --git a/packages/realm_dart/test/app_test.dart b/packages/realm_dart/test/app_test.dart index 13f758250..684659e02 100644 --- a/packages/realm_dart/test/app_test.dart +++ b/packages/realm_dart/test/app_test.dart @@ -382,6 +382,35 @@ void main() { test('AppConfiguration(empty-id) throws', () { expect(() => AppConfiguration(''), throwsA(isA())); }); + + baasTest('AppConfiguration.syncTimeouts are passed correctly to Core', (appConfig) async { + Realm.logger.setLogLevel(LogLevel.debug); + final buffer = StringBuffer(); + final sub = Realm.logger.onRecord.listen((r) => buffer.writeln('[${r.category}] ${r.level}: ${r.message}')); + + final customConfig = AppConfiguration(appConfig.appId, + baseUrl: appConfig.baseUrl, + baseFilePath: appConfig.baseFilePath, + defaultRequestTimeout: appConfig.defaultRequestTimeout, + syncTimeoutOptions: SyncTimeoutOptions( + connectTimeout: Duration(milliseconds: 1234), + connectionLingerTime: Duration(milliseconds: 3456), + pingKeepAlivePeriod: Duration(milliseconds: 5678), + pongKeepAliveTimeout: Duration(milliseconds: 7890), + fastReconnectLimit: Duration(milliseconds: 9012))); + + final realm = await getIntegrationRealm(appConfig: customConfig); + await realm.syncSession.waitForDownload(); + + final log = buffer.toString(); + expect(log, contains('Config param: connect_timeout = 1234 ms')); + expect(log, contains('Config param: connection_linger_time = 3456 ms')); + expect(log, contains('Config param: ping_keepalive_period = 5678 ms')); + expect(log, contains('Config param: pong_keepalive_timeout = 7890 ms')); + expect(log, contains('Config param: fast_reconnect_limit = 9012 ms')); + + await sub.cancel(); + }); } extension PersonExt on Person { diff --git a/packages/realm_dart/test/baas_helper.dart b/packages/realm_dart/test/baas_helper.dart index a8810ef99..66154823e 100644 --- a/packages/realm_dart/test/baas_helper.dart +++ b/packages/realm_dart/test/baas_helper.dart @@ -191,7 +191,6 @@ class BaasHelper { app.clientAppId, baseUrl: Uri.parse(customBaseUrl ?? baseUrl), baseFilePath: temporaryPath, - maxConnectionTimeout: Duration(minutes: 10), defaultRequestTimeout: Duration(minutes: 7), ); }