Skip to content

Commit

Permalink
Expose sync timeout options
Browse files Browse the repository at this point in the history
  • Loading branch information
nirinchev committed Jul 30, 2024
1 parent 28e0e5a commit 5d7bd59
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 26 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
95 changes: 85 additions & 10 deletions packages/realm_dart/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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 == '') {
Expand Down
32 changes: 20 additions & 12 deletions packages/realm_dart/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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, {
Expand All @@ -362,6 +369,7 @@ class FlexibleSyncConfiguration extends Configuration {
super.maxNumberOfActiveVersions,
this.shouldCompactCallback,
this.schemaVersion = 0,
this.cancelAsyncOperationsOnNonFatalErrors = false,
}) : super._();

@override
Expand Down
7 changes: 7 additions & 0 deletions packages/realm_dart/lib/src/handles/native/app_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
}
Expand Down
2 changes: 2 additions & 0 deletions packages/realm_dart/lib/src/handles/native/config_handle.dart
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ class ConfigHandle extends HandleBase<realm_config> {
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<Void Function(Handle, Pointer<realm_sync_session_t>, realm_sync_error_t)>(_syncErrorHandlerCallback);
final errorHandlerUserdata = realmLib.realm_dart_userdata_async_new(config, errorHandlerCallback.cast(), schedulerHandle.pointer);
Expand Down
3 changes: 1 addition & 2 deletions packages/realm_dart/lib/src/realm_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -179,7 +179,6 @@ class Realm {
return await CancellableFuture.value(realm, cancellationToken);
}


final asyncOpenHandle = AsyncOpenTaskHandle.from(config);
return await CancellableFuture.from<Realm>(() async {
if (cancellationToken != null && cancellationToken.isCancelled) {
Expand Down
29 changes: 29 additions & 0 deletions packages/realm_dart/test/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,35 @@ void main() {
test('AppConfiguration(empty-id) throws', () {
expect(() => AppConfiguration(''), throwsA(isA<RealmException>()));
});

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 {
Expand Down
1 change: 0 additions & 1 deletion packages/realm_dart/test/baas_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ class BaasHelper {
app.clientAppId,
baseUrl: Uri.parse(customBaseUrl ?? baseUrl),
baseFilePath: temporaryPath,
maxConnectionTimeout: Duration(minutes: 10),
defaultRequestTimeout: Duration(minutes: 7),
);
}
Expand Down

0 comments on commit 5d7bd59

Please sign in to comment.