Skip to content

Commit 98e77a5

Browse files
authored
RDART-1081: Expose sync timeout options (#1764)
* Expose sync timeout options * CR comments * Format
1 parent 25deb78 commit 98e77a5

File tree

8 files changed

+131
-5
lines changed

8 files changed

+131
-5
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
## vNext (TBD)
22

33
### Enhancements
4-
* None
4+
* 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))
5+
* 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/pull/1764))
56

67
### Fixed
78
* 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))
9+
* `AppConfiguration.maxConnectionTimeout` never had any effect and has been deprecated in favor of `SyncTimeoutOptions.connectTimeout`. (PR [#1764](https://github.com/realm/realm-dart/pull/1764))
810
* Pure dart apps, when compiled to an exe and run from outside the project directory would fail to load the native shared library. (Issue [#1765](https://github.com/realm/realm-dart/issues/1765))
911

1012
### Compatibility

packages/realm_dart/lib/src/app.dart

+78-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,78 @@ import 'handles/realm_core.dart';
1515
import 'logging.dart';
1616
import 'user.dart';
1717

18+
/// Options for configuring timeouts and intervals used by the sync client.
19+
@immutable
20+
final class SyncTimeoutOptions {
21+
/// Controls the maximum amount of time to allow for a connection to
22+
/// become fully established.
23+
///
24+
/// This includes the time to resolve the
25+
/// network address, the TCP connect operation, the SSL handshake, and
26+
/// the WebSocket handshake.
27+
///
28+
/// Defaults to 2 minutes.
29+
final Duration connectTimeout; // TimeSpan.FromMinutes(2);
30+
31+
/// Controls the amount of time to keep a connection open after all
32+
/// sessions have been abandoned.
33+
///
34+
/// After all synchronized Realms have been closed for a given server, the
35+
/// connection is kept open until the linger time has expired to avoid the
36+
/// overhead of reestablishing the connection when Realms are being closed and
37+
/// reopened.
38+
///
39+
/// Defaults to 30 seconds.
40+
final Duration connectionLingerTime;
41+
42+
/// Controls how long to wait between each heartbeat ping message.
43+
///
44+
/// The client periodically sends ping messages to the server to check if the
45+
/// connection is still alive. Shorter periods make connection state change
46+
/// notifications more responsive at the cost of battery life (as the antenna
47+
/// will have to wake up more often).
48+
///
49+
/// Defaults to 1 minute.
50+
final Duration pingKeepAlivePeriod;
51+
52+
/// Controls how long to wait for a reponse to a heartbeat ping before
53+
/// concluding that the connection has dropped.
54+
///
55+
/// Shorter values will make connection state change notifications more
56+
/// responsive as it will only change to `disconnected` after this much time has
57+
/// elapsed, but overly short values may result in spurious disconnection
58+
/// notifications when the server is simply taking a long time to respond.
59+
///
60+
/// Defaults to 2 minutes.
61+
final Duration pongKeepAliveTimeout;
62+
63+
/// Controls the maximum amount of time since the loss of a
64+
/// prior connection, for a new connection to be considered a "fast
65+
/// reconnect".
66+
///
67+
/// When a client first connects to the server, it defers uploading any local
68+
/// changes until it has downloaded all changesets from the server. This
69+
/// typically reduces the total amount of merging that has to be done, and is
70+
/// particularly beneficial the first time that a specific client ever connects
71+
/// to the server.
72+
///
73+
/// When an existing client disconnects and then reconnects within the "fact
74+
/// reconnect" time this is skipped and any local changes are uploaded
75+
/// immediately without waiting for downloads, just as if the client was online
76+
/// the whole time.
77+
///
78+
/// Defaults to 1 minute.
79+
final Duration fastReconnectLimit;
80+
81+
const SyncTimeoutOptions({
82+
this.connectTimeout = const Duration(minutes: 2),
83+
this.connectionLingerTime = const Duration(seconds: 30),
84+
this.pingKeepAlivePeriod = const Duration(minutes: 1),
85+
this.pongKeepAliveTimeout = const Duration(minutes: 2),
86+
this.fastReconnectLimit = const Duration(minutes: 1),
87+
});
88+
}
89+
1890
/// A class exposing configuration options for an [App]
1991
/// {@category Application}
2092
@immutable
@@ -41,6 +113,7 @@ class AppConfiguration {
41113
/// become fully established. This includes the time to resolve the
42114
/// network address, the TCP connect operation, the SSL handshake, and
43115
/// the WebSocket handshake. Defaults to 2 minutes.
116+
@Deprecated('Use SyncTimeoutOptions.connectTimeout')
44117
final Duration maxConnectionTimeout;
45118

46119
/// Enumeration that specifies how and if logged-in User objects are persisted across application launches.
@@ -61,6 +134,9 @@ class AppConfiguration {
61134
/// a more complex networking setup.
62135
final Client httpClient;
63136

137+
/// Options for the assorted types of connection timeouts for sync connections opened for this app.
138+
final SyncTimeoutOptions syncTimeoutOptions;
139+
64140
/// Instantiates a new [AppConfiguration] with the specified appId.
65141
AppConfiguration(
66142
this.appId, {
@@ -69,8 +145,9 @@ class AppConfiguration {
69145
this.defaultRequestTimeout = const Duration(seconds: 60),
70146
this.metadataEncryptionKey,
71147
this.metadataPersistenceMode = MetadataPersistenceMode.plaintext,
72-
this.maxConnectionTimeout = const Duration(minutes: 2),
148+
@Deprecated('Use SyncTimeoutOptions.connectTimeout') this.maxConnectionTimeout = const Duration(minutes: 2),
73149
Client? httpClient,
150+
this.syncTimeoutOptions = const SyncTimeoutOptions(),
74151
}) : baseUrl = baseUrl ?? Uri.parse(realmCore.getDefaultBaseUrl()),
75152
baseFilePath = baseFilePath ?? path.dirname(Configuration.defaultRealmPath),
76153
httpClient = httpClient ?? defaultClient {

packages/realm_dart/lib/src/configuration.dart

+11
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ abstract class Configuration {
187187
int? maxNumberOfActiveVersions,
188188
ShouldCompactCallback? shouldCompactCallback,
189189
int schemaVersion = 0,
190+
bool cancelAsyncOperationsOnNonFatalErrors = false,
190191
}) =>
191192
FlexibleSyncConfiguration._(
192193
user,
@@ -199,6 +200,7 @@ abstract class Configuration {
199200
maxNumberOfActiveVersions: maxNumberOfActiveVersions,
200201
shouldCompactCallback: shouldCompactCallback,
201202
schemaVersion: schemaVersion,
203+
cancelAsyncOperationsOnNonFatalErrors: cancelAsyncOperationsOnNonFatalErrors,
202204
);
203205

204206
/// Constructs a [DisconnectedSyncConfiguration]
@@ -351,6 +353,14 @@ class FlexibleSyncConfiguration extends Configuration {
351353
/// all subscriptions will be reset since they may not conform to the new schema.
352354
final int schemaVersion;
353355

356+
/// Controls whether async operations such as [Realm.open], [Session.waitForUpload], and [Session.waitForDownload]
357+
/// should throw an error whenever a non-fatal error, such as timeout occurs.
358+
///
359+
/// If set to `false`, non-fatal session errors will be ignored and sync will continue retrying the
360+
/// connection under in the background. This means that in cases where the devie is offline, these operations
361+
/// may take an indeterminate time to complete.
362+
final bool cancelAsyncOperationsOnNonFatalErrors;
363+
354364
FlexibleSyncConfiguration._(
355365
this.user,
356366
super.schemaObjects, {
@@ -362,6 +372,7 @@ class FlexibleSyncConfiguration extends Configuration {
362372
super.maxNumberOfActiveVersions,
363373
this.shouldCompactCallback,
364374
this.schemaVersion = 0,
375+
this.cancelAsyncOperationsOnNonFatalErrors = false,
365376
}) : super._();
366377

367378
@override

packages/realm_dart/lib/src/handles/native/app_handle.dart

+7
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,13 @@ _AppConfigHandle _createAppConfig(AppConfiguration configuration, HttpTransportH
438438
realmLib.realm_app_config_set_metadata_encryption_key(handle.pointer, configuration.metadataEncryptionKey!.toUint8Ptr(arena));
439439
}
440440

441+
final syncClientConfig = realmLib.realm_app_config_get_sync_client_config(handle.pointer);
442+
realmLib.realm_sync_client_config_set_connect_timeout(syncClientConfig, configuration.syncTimeoutOptions.connectTimeout.inMilliseconds);
443+
realmLib.realm_sync_client_config_set_connection_linger_time(syncClientConfig, configuration.syncTimeoutOptions.connectionLingerTime.inMilliseconds);
444+
realmLib.realm_sync_client_config_set_ping_keepalive_period(syncClientConfig, configuration.syncTimeoutOptions.pingKeepAlivePeriod.inMilliseconds);
445+
realmLib.realm_sync_client_config_set_pong_keepalive_timeout(syncClientConfig, configuration.syncTimeoutOptions.pongKeepAliveTimeout.inMilliseconds);
446+
realmLib.realm_sync_client_config_set_fast_reconnect_limit(syncClientConfig, configuration.syncTimeoutOptions.fastReconnectLimit.inMilliseconds);
447+
441448
return handle;
442449
});
443450
}

packages/realm_dart/lib/src/handles/native/config_handle.dart

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ class ConfigHandle extends HandleBase<realm_config> {
9191
try {
9292
realmLib.realm_sync_config_set_session_stop_policy(syncConfigPtr, config.sessionStopPolicy.index);
9393
realmLib.realm_sync_config_set_resync_mode(syncConfigPtr, config.clientResetHandler.clientResyncMode.index);
94+
realmLib.realm_sync_config_set_cancel_waits_on_nonfatal_error(syncConfigPtr, config.cancelAsyncOperationsOnNonFatalErrors);
95+
9496
final errorHandlerCallback =
9597
Pointer.fromFunction<Void Function(Handle, Pointer<realm_sync_session_t>, realm_sync_error_t)>(_syncErrorHandlerCallback);
9698
final errorHandlerUserdata = realmLib.realm_dart_userdata_async_new(config, errorHandlerCallback.cast(), schedulerHandle.pointer);

packages/realm_dart/lib/src/realm_class.dart

+1-2
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export 'package:realm_common/realm_common.dart'
5959
Uuid;
6060

6161
// always expose with `show` to explicitly control the public API surface
62-
export 'app.dart' show AppException, App, MetadataPersistenceMode, AppConfiguration;
62+
export 'app.dart' show AppException, App, MetadataPersistenceMode, AppConfiguration, SyncTimeoutOptions;
6363
export 'collections.dart' show Move;
6464
export "configuration.dart"
6565
show
@@ -179,7 +179,6 @@ class Realm {
179179
return await CancellableFuture.value(realm, cancellationToken);
180180
}
181181

182-
183182
final asyncOpenHandle = AsyncOpenTaskHandle.from(config);
184183
return await CancellableFuture.from<Realm>(() async {
185184
if (cancellationToken != null && cancellationToken.isCancelled) {

packages/realm_dart/test/app_test.dart

+29
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,35 @@ void main() {
382382
test('AppConfiguration(empty-id) throws', () {
383383
expect(() => AppConfiguration(''), throwsA(isA<RealmException>()));
384384
});
385+
386+
baasTest('AppConfiguration.syncTimeouts are passed correctly to Core', (appConfig) async {
387+
Realm.logger.setLogLevel(LogLevel.debug);
388+
final buffer = StringBuffer();
389+
final sub = Realm.logger.onRecord.listen((r) => buffer.writeln('[${r.category}] ${r.level}: ${r.message}'));
390+
391+
final customConfig = AppConfiguration(appConfig.appId,
392+
baseUrl: appConfig.baseUrl,
393+
baseFilePath: appConfig.baseFilePath,
394+
defaultRequestTimeout: appConfig.defaultRequestTimeout,
395+
syncTimeoutOptions: SyncTimeoutOptions(
396+
connectTimeout: Duration(milliseconds: 1234),
397+
connectionLingerTime: Duration(milliseconds: 3456),
398+
pingKeepAlivePeriod: Duration(milliseconds: 5678),
399+
pongKeepAliveTimeout: Duration(milliseconds: 7890),
400+
fastReconnectLimit: Duration(milliseconds: 9012)));
401+
402+
final realm = await getIntegrationRealm(appConfig: customConfig);
403+
await realm.syncSession.waitForDownload();
404+
405+
final log = buffer.toString();
406+
expect(log, contains('Config param: connect_timeout = 1234 ms'));
407+
expect(log, contains('Config param: connection_linger_time = 3456 ms'));
408+
expect(log, contains('Config param: ping_keepalive_period = 5678 ms'));
409+
expect(log, contains('Config param: pong_keepalive_timeout = 7890 ms'));
410+
expect(log, contains('Config param: fast_reconnect_limit = 9012 ms'));
411+
412+
await sub.cancel();
413+
});
385414
}
386415

387416
extension PersonExt on Person {

packages/realm_dart/test/baas_helper.dart

-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ class BaasHelper {
191191
app.clientAppId,
192192
baseUrl: Uri.parse(customBaseUrl ?? baseUrl),
193193
baseFilePath: temporaryPath,
194-
maxConnectionTimeout: Duration(minutes: 10),
195194
defaultRequestTimeout: Duration(minutes: 7),
196195
);
197196
}

0 commit comments

Comments
 (0)