Docs Menu
Docs Home
/ /
Atlas Device SDKs
/ /

Client Reset - Java SDK

On this page

  • Discard Unsynced Changes
  • Discard Unsynced Changes after Breaking Schema Changes
  • Manually Recover Unsynced Changes
  • Test Client Reset Handling

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:

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.

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() {
@Override
public void onBeforeReset(Realm realm) {
Log.w("EXAMPLE", "Beginning client reset for " + realm.getPath());
}
@Override
public void onAfterReset(Realm before, Realm after) {
Log.w("EXAMPLE", "Finished client reset for " + before.getPath());
}
@Override
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()
)

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() {
@Override
public void onBeforeReset(Realm realm) {
Log.w("EXAMPLE", "Beginning client reset for " + realm.getPath());
}
@Override
public void onAfterReset(Realm before, Realm after) {
Log.w("EXAMPLE", "Finished client reset for " + before.getPath());
}
@Override
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()
)

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.

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:

  1. Write data from a client application and wait for it to synchronize.

  2. Terminate and re-enable Device Sync.

  3. 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.

Back

Handle Sync Errors

Next

Manual Client Reset Data Recovery