Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RDART-1081: Expose sync timeout options #1764

Merged
merged 4 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/pull/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/pull/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 {
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
/// 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()})
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
: 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);
nirinchev marked this conversation as resolved.
Show resolved Hide resolved

/// 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
Loading