Client Reset - Java SDK
On this page
Tip
See also: Learn More About Client Resets
To learn about the causes of and strategies for handling client resets, check out the Sync Client Resets page.
The SDK reads and writes to a realm file on the device. When you use Atlas Device Sync, this local realm syncs with the application backend. Some conditions can cause the realm to be unable to sync with the backend. When this occurs, you get a client reset error.
This error means you must reset the realm file in the client application. Clients in this state may continue to run and save data locally. Until you perform the client reset, the realm does not sync with the backend.
Choose a client reset strategy to handle client reset errors. These strategies restore realm to a syncable state, but have tradeoffs:
Discard Unsynced Changes. Restore Sync by discarding local changes since the last sync. Maintains change listeners.
Manually Recover Unsynced Changes:. Move the unsyncable realm and download a new copy. Invalidates change listeners.
Both options let you write custom logic to recover local changes. Neither option can recover local changes for you.
Discard unsynced changes is a less complex alternative to manual recovery. However, this strategy cannot handle every client reset error. You must maintain a manual client reset handler as a fallback.
Discard Unsynced Changes
New in version 10.10.0.
Discard unsynced changes is a client reset strategy provided by the SDK. This strategy requires minimal code. This strategy performs a reset without closing the realm or missing notifications.
It does delete all local changes made since the last successful sync. This includes any data already written to the realm but not yet synced to the application backend. Do not use this strategy if your application cannot lose unsynced data.
Discard unsynced changes cannot handle breaking or destructive schema changes. When breaking changes occur, the SDK falls back to manual recovery mode.
To use this strategy, pass an instance of
DiscardUnsyncedChangesStrategy to the
defaultSyncClientResetStrategy()
builder method when you instantiate your App
. Your
DiscardUnsyncedChangesStrategy
instance must implement the following
methods:
onBeforeReset()
. The SDK calls this block when it receives a client reset error from the backend. This occurs before the SDK executes the client reset strategy.onAfterReset()
. The SDK calls this block after successfully executing this strategy. This block provides a frozen copy of the original realm. It also returns a live instance of the realm in a syncable state.onError()
. The SDK calls this method during a breaking schema change. Behaves similarly to defaultClientResetStrategy().
The following example implements this strategy:
String appID = YOUR_APP_ID; // replace this with your App ID App app = new App(new AppConfiguration.Builder(appID) .defaultSyncClientResetStrategy(new DiscardUnsyncedChangesStrategy() { public void onBeforeReset(Realm realm) { Log.w("EXAMPLE", "Beginning client reset for " + realm.getPath()); } public void onAfterReset(Realm before, Realm after) { Log.w("EXAMPLE", "Finished client reset for " + before.getPath()); } public void onError(SyncSession session, ClientResetRequiredError error) { Log.e("EXAMPLE", "Couldn't handle the client reset automatically." + " Falling back to manual recovery: " + error.getErrorMessage()); handleManualReset(session.getUser().getApp(), session, error); } }) .build());
val appID: String = YOUR_APP_ID // replace this with your App ID val app = App( AppConfiguration.Builder(appID) .defaultSyncClientResetStrategy(object : DiscardUnsyncedChangesStrategy { override fun onBeforeReset(realm: Realm) { Log.w("EXAMPLE", "Beginning client reset for " + realm.path) } override fun onAfterReset(before: Realm, after: Realm) { Log.w("EXAMPLE", "Finished client reset for " + before.path) } override fun onError(session: SyncSession, error: ClientResetRequiredError) { Log.e( "EXAMPLE", "Couldn't handle the client reset automatically." + " Falling back to manual recovery: " + error.errorMessage ) handleManualReset(session.user.app, session, error) } }) .build() )
Discard Unsynced Changes after Breaking Schema Changes
Important
Breaking Schema Changes Require an App Schema Update
After a breaking schema change:
All clients must perform a client reset.
You must update client models affected by the breaking schema change.
The discard unsynced changes strategy cannot handle breaking changes. You
must manually handle the client reset in the onError()
method. This
example manually discards unsynced changes to handle the client reset:
String appID = YOUR_APP_ID; // replace this with your App ID App app = null; AtomicReference<App> globalApp = new AtomicReference<>(app); // accessing the app from within the lambda below requires an effectively final object app = new App(new AppConfiguration.Builder(appID) .defaultSyncClientResetStrategy(new DiscardUnsyncedChangesStrategy() { public void onBeforeReset(Realm realm) { Log.w("EXAMPLE", "Beginning client reset for " + realm.getPath()); } public void onAfterReset(Realm before, Realm after) { Log.w("EXAMPLE", "Finished client reset for " + before.getPath()); } public void onError(SyncSession session, ClientResetRequiredError error) { Log.e("EXAMPLE", "Couldn't handle the client reset automatically." + " Falling back to manual client reset execution: " + error.getErrorMessage()); // close all instances of your realm -- this application only uses one globalRealm.close(); try { Log.w("EXAMPLE", "About to execute the client reset."); // execute the client reset, moving the current realm to a backup file error.executeClientReset(); Log.w("EXAMPLE", "Executed the client reset."); } catch (IllegalStateException e) { Log.e("EXAMPLE", "Failed to execute the client reset: " + e.getMessage()); // The client reset can only proceed if there are no open realms. // if execution failed, ask the user to restart the app, and we'll client reset // when we first open the app connection. AlertDialog restartDialog = new AlertDialog.Builder(activity) .setMessage("Sync error. Restart the application to resume sync.") .setTitle("Restart to Continue") .create(); restartDialog.show(); } // open a new instance of the realm. This initializes a new file for the new realm // and downloads the backend state. Do this in a background thread so we can wait // for server changes to fully download. ExecutorService executor = Executors.newSingleThreadExecutor(); executor.execute(() -> { Realm newRealm = Realm.getInstance(globalConfig); // ensure that the backend state is fully downloaded before proceeding try { globalApp.get().getSync().getSession(globalConfig).downloadAllServerChanges(10000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } Log.w("EXAMPLE", "Downloaded server changes for a fresh instance of the realm."); newRealm.close(); }); // execute the recovery logic on a background thread try { executor.awaitTermination(20000, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { e.printStackTrace(); } } }) .build()); globalApp.set(app);
val appID = YOUR_APP_ID // replace this with your App ID var app: App? = null app = App( AppConfiguration.Builder(appID) .defaultSyncClientResetStrategy(object : DiscardUnsyncedChangesStrategy { override fun onBeforeReset(realm: Realm) { Log.w("EXAMPLE", "Beginning client reset for " + realm.path) } override fun onAfterReset(before: Realm, after: Realm) { Log.w("EXAMPLE", "Finished client reset for " + before.path) } override fun onError(session: SyncSession, error: ClientResetRequiredError) { Log.e( "EXAMPLE", "Couldn't handle the client reset automatically." + " Falling back to manual client reset execution: " + error.errorMessage ) // close all instances of your realm -- this application only uses one globalRealm!!.close() try { Log.w("EXAMPLE", "About to execute the client reset.") // execute the client reset, moving the current realm to a backup file error.executeClientReset() Log.w("EXAMPLE", "Executed the client reset.") } catch (e: java.lang.IllegalStateException) { Log.e("EXAMPLE", "Failed to execute the client reset: " + e.message) // The client reset can only proceed if there are no open realms. // if execution failed, ask the user to restart the app, and we'll client reset // when we first open the app connection. val restartDialog = AlertDialog.Builder(activity) .setMessage("Sync error. Restart the application to resume sync.") .setTitle("Restart to Continue") .create() restartDialog.show() } // open a new instance of the realm. This initializes a new file for the new realm // and downloads the backend state. Do this in a background thread so we can wait // for server changes to fully download. val executor = Executors.newSingleThreadExecutor() executor.execute { val newRealm = Realm.getInstance(globalConfig) // ensure that the backend state is fully downloaded before proceeding try { app!!.sync.getSession(globalConfig) .downloadAllServerChanges( 10000, TimeUnit.MILLISECONDS ) } catch (e: InterruptedException) { e.printStackTrace() } Log.w( "EXAMPLE", "Downloaded server changes for a fresh instance of the realm." ) newRealm.close() } // execute the recovery logic on a background thread try { executor.awaitTermination(20000, TimeUnit.MILLISECONDS) } catch (e: InterruptedException) { e.printStackTrace() } } }) .build() )
Manually Recover Unsynced Changes
Tip
Manual recovery replaces the deprecated
SyncSession.ClientResetHandler
.
Clients using the deprecated handler can update to manual recovery
with no logic changes.
We do not recommend manual client reset recovery. It requires:
Substantial amounts of code
Schema concessions
Complex conflict resolution logic.
To learn more, see the Advanced Guide to Manual Client Reset Data Recovery.
Test Client Reset Handling
You can manually test your application's client reset handling by terminating and re-enabling Device Sync.
When you terminate and re-enable Sync, clients that have previously connected with Sync are unable to connect until after they perform a client reset. Terminating Sync deletes the metadata from the server that allows the client to synchronize. The client must download a new copy of the realm from the server. The server sends a client reset error to these clients. So, when you terminate Sync, you trigger the client reset condition.
To test client reset handling:
Write data from a client application and wait for it to synchronize.
Terminate and re-enable Device Sync.
Run the client app again. The app should get a client reset error when it tries to connect to the server.
Warning
While you iterate on client reset handling in your client application, you may need to terminate and re-enable Sync repeatedly. Terminating and re-enabling Sync renders all existing clients unable to sync until after completing a client reset. To avoid this in production, test client reset handling in a development environment.