Docs Menu
Docs Home
/ /
Atlas Device SDKs
/ /

Manual Client Reset Data Recovery - Java SDK

On this page

  • Example
  • Track Updates to Objects
  • Track Successful Syncs
  • Manual Recovery with Last Updated Time and Last Synced Time
  • Alternative Implementations

Important

Manual Recovery is Manual

Manual recovery requires significant amounts of code, schema concessions, and custom conflict resolution logic. If your application can accommodate losing unsynced data during a client reset, try the discard unsynced changes client reset strategy instead.

Warning

Avoid Making Breaking Schema Changes in Production

Do not expect to recover all unsynced data after a breaking schema change. The best way to preserve user data is to never make a breaking - also called destructive - schema change at all.

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 manually recover unsynced changes client reset strategy gives developers the opportunity to recover data already written to the client realm file but not yet synced to the backend. The following steps demonstrate the process at a high level:

  1. Client reset error: Your application receives a client reset error code from the backend.

  2. Strategy implementation: The SDK calls your strategy implementation.

  3. Close all instances of the realm: Close all open instances of the realm experiencing the client reset. If your application architecture makes this difficult (for instance, if your app uses many realm instances simultaneously in listeners throughout the application), it may be easier to restart the application. You can do this programmatically or through a direct request to the user in a dialog.

  4. Move the realm to a backup file: Call the executeClientReset() method of the provided ClientResetRequiredError. This method moves the current copy of the client realm file to a backup file.

  5. Open new instance of the realm: Open a new instance of the realm using your typical sync configuration. If your application uses multiple realms, you can identify the realm experiencing a client reset from the backup file name.

  6. Download all realm data from the backend: Download the entire set of data in the realm before you proceed. If your sync configuration doesn't specify the waitForInitialRemoteData() option, you can call SyncSession.downloadAllServerChanges() after opening the realm.

  7. Open the realm backup: Use the getBackupRealmConfiguration() method of the provided ClientResetRequiredError to open an instance of the client realm file from the backup file. You must open this instance as a DynamicRealm, a type of realm that uses text field lookups for all data access.

  8. Migrate unsynced changes: Query the backup realm for data to recover. Insert, delete or update data in the new realm accordingly.

To handle client resets with the "manually recover unsynced changes" strategy, pass an instance of ManuallyRecoverUnsyncedChangesStrategy to the defaultSyncClientResetStrategy() builder method when you instantiate your App. Your ManuallyRecoverUnsyncedChangesStrategy instance must implement the following methods:

  • onClientReset(): called when the SDK receives a client reset error from the backend.

The following example implements this strategy:

String appID = YOUR_APP_ID; // replace this with your App ID
final App app = new App(new AppConfiguration.Builder(appID)
.defaultSyncClientResetStrategy(new ManuallyRecoverUnsyncedChangesStrategy() {
@Override
public void onClientReset(SyncSession session, ClientResetRequiredError error) {
Log.v("EXAMPLE", "Executing manual client reset handler");
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 { session, error ->
Log.v("EXAMPLE", "Executing manual client reset handler")
handleManualReset(session.user.app, session, error)
}
.build())

Note

handleManualReset() Implementation

This client reset example calls a separate method that handles the specific logic of the client reset. Continue reading the sections below for an example implementation.

The specifics of manual recovery depend heavily upon your application and your schema. However, there are a few techniques that can help with most manual recoveries. The following example implementation demonstrates one method of recovering unsynced changes from a backup realm.

This example adds a "Last Updated Time" to each object model to track when each object last changed. We'll watch the realm for the "Last Synced Time" to determine when the realm last uploaded its state to the backend. Then, we can find objects that were deleted, created, or updated since the last sync with the backend, and copy that data from the backup realm to the new realm.

Ordinarily, there is no way to detect when a Realm object was last modified. This makes it difficult to determine which changes were synced to the backend. By adding a timestamp to your Realm object classes and updating that timestamp to the current time whenever a change occurs, you can keep track of when objects were changed:

Potato.java
import org.bson.types.ObjectId;
import io.realm.DynamicRealmObject;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Potato extends RealmObject {
@PrimaryKey
private ObjectId _id;
private Long lastUpdated;
private String species;
public Potato(ObjectId id, String species) {
this._id = id;
this.lastUpdated = System.currentTimeMillis();
this.species = species;
}
public Potato() { this.lastUpdated = System.currentTimeMillis(); }
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
public Potato(DynamicRealmObject obj) {
this._id = obj.getObjectId("_id");
this.species = obj.getString("species");
this.lastUpdated = obj.getLong("lastUpdated");
}
public ObjectId getId() { return _id; }
public String getSpecies() { return species; }
public void setSpecies(String species) {
this.species = species;
this.lastUpdated = System.currentTimeMillis();
}
public Long getLastUpdated() { return lastUpdated; }
public void setLastUpdated(Long lastUpdated) {
this.lastUpdated = lastUpdated;
}
}
Potato.kt
import io.realm.DynamicRealmObject
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId
open class Potato : RealmObject {
@PrimaryKey
var _id: ObjectId? = null
var lastUpdated: Long
var species: String? = null
set(species: String?) {
field = species
lastUpdated = System.currentTimeMillis()
}
constructor(id: ObjectId?, species: String?) {
this._id = id
lastUpdated = System.currentTimeMillis()
this.species = species
}
constructor() {
lastUpdated = System.currentTimeMillis()
}
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
constructor(obj: DynamicRealmObject) {
_id = obj.getObjectId("_id")
species = obj.getString("species")
lastUpdated = obj.getLong("lastUpdated")
}
}
Onion.java
import org.bson.types.ObjectId;
import io.realm.DynamicRealmObject;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Onion extends RealmObject {
@PrimaryKey
public ObjectId _id;
public Long lastUpdated;
public String varietal;
public Onion(ObjectId id, String varietal) {
this._id = id;
this.lastUpdated = System.currentTimeMillis();
this.varietal = varietal;
}
public Onion() { this.lastUpdated = System.currentTimeMillis(); }
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
public Onion(DynamicRealmObject obj) {
this._id = obj.getObjectId("_id");
this.varietal = obj.getString("varietal");
this.lastUpdated = obj.getLong("lastUpdated");
}
public ObjectId getId() { return _id; }
public String getVarietal() { return varietal; }
public void setVarietal(String varietal) {
this.varietal = varietal;
this.lastUpdated = System.currentTimeMillis();
}
public Long getLastUpdated() { return lastUpdated; }
public void setLastUpdated(Long lastUpdated) {
this.lastUpdated = lastUpdated;
}
}
Onion.kt
import io.realm.DynamicRealmObject
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId
open class Onion : RealmObject {
@PrimaryKey
var _id: ObjectId? = null
var lastUpdated: Long
var varietal: String? = null
set(varietal: String?) {
lastUpdated = System.currentTimeMillis()
field = varietal
}
constructor(id: ObjectId?, varietal: String?) {
this._id = id
lastUpdated = System.currentTimeMillis()
this.varietal = varietal
}
constructor() {
lastUpdated = System.currentTimeMillis()
}
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
constructor(obj: DynamicRealmObject) {
_id = obj.getObjectId("_id")
varietal = obj.getString("varietal")
lastUpdated = obj.getLong("lastUpdated")
}
}
Rice.java
import org.bson.types.ObjectId;
import io.realm.DynamicRealmObject;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class Rice extends RealmObject {
@PrimaryKey
protected ObjectId _id;
protected Long lastUpdated;
protected String style;
public Rice(ObjectId id, String style) {
this._id = id;
this.lastUpdated = System.currentTimeMillis();
this.style = style;
}
public Rice() { this.lastUpdated = System.currentTimeMillis(); }
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
public Rice(DynamicRealmObject obj) {
this._id = obj.getObjectId("_id");
this.style = obj.getString("style");
this.lastUpdated = obj.getLong("lastUpdated");
}
public ObjectId getId() { return _id; }
public String getStyle() { return style; }
public void setStyle(String style) {
this.style = style;
this.lastUpdated = System.currentTimeMillis();
}
public Long getLastUpdated() { return lastUpdated; }
public void setLastUpdated(Long lastUpdated) {
this.lastUpdated = lastUpdated;
}
}
Rice.kt
import io.realm.DynamicRealmObject
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId
open class Rice : RealmObject {
@PrimaryKey
var _id: ObjectId? = null
var lastUpdated: Long
var style: String? = null
set(style: String?) {
field = style
lastUpdated = System.currentTimeMillis()
}
constructor(id: ObjectId?, style: String?) {
this._id = id
lastUpdated = System.currentTimeMillis()
this.style = style
}
constructor() {
lastUpdated = System.currentTimeMillis()
}
// convenience constructor that allows us to convert DynamicRealmObjects in a backup realm
// into full object instances
constructor(obj: DynamicRealmObject) {
_id = obj.getObjectId("_id")
style = obj.getString("style")
lastUpdated = obj.getLong("lastUpdated")
}
}

Just knowing when objects were changed isn't enough to recover data during a client reset. You also need to know when the realm last completed a sync successfully. This example implementation uses a singleton object called LastSynced in the realm, paired with an upload progress listener, to record whenever a realm finishes syncing successfully.

LastSynced.java
import org.bson.types.ObjectId;
import java.util.Date;
import io.realm.RealmObject;
import io.realm.annotations.PrimaryKey;
public class LastSynced extends RealmObject {
protected Long timestamp;
@PrimaryKey
protected ObjectId _id = null;
// only one instance per realm -- enforce by forcing a single objectid value on all instances
public LastSynced(Long timestamp) {
this.timestamp = timestamp;
}
public LastSynced() {}
public Long getTimestamp() {
return timestamp;
}
public void setTimestamp(Long timestamp) {
this.timestamp = timestamp;
}
public ObjectId get_id() {
return _id;
}
}
LastSynced.kt
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
import org.bson.types.ObjectId
open class LastSynced : RealmObject {
var timestamp: Long? = null
@PrimaryKey
var _id: ObjectId? = null
protected set(id: ObjectId?) {}
// only one instance per realm -- enforce by forcing a single objectid value on all instances
constructor(timestamp: Long?) {
this.timestamp = timestamp
}
constructor() {}
}

You can use SyncSession.addUploadProgressListener() to listen for upload progress events in your App. Implement onChange() to handle these events. Call Progress.isTransferComplete() to check if the upload has completed. When isTransferComplete() returns true, all clientside updates, inserts, and deletes in the realm have successfully synced to the backend, and you can update the LastSynced time to the current time. To prevent LastSynced from looping on updates to the LastSynced time, don't update the LastSynced time if it's been less than, say, 10ms since you last updated the time.

Register your progress listener with ProgressMode.INDEFINITELY to subscribe your listener to all future upload progress events, instead of just the current upload's progress events.

// use a "last synced" singleton in the realm to keep track of when the
// realm last successfully completed a sync
app.getSync().getSession(config)
.addUploadProgressListener(ProgressMode.INDEFINITELY, progress -> {
// get the last synced time. Create an instance if it does not already exist.
Realm notificationRealm = Realm.getInstance(config);
LastSynced lastSynced =
notificationRealm.where(LastSynced.class).findFirst();
if (lastSynced == null) {
notificationRealm.executeTransaction(transactionRealm ->
transactionRealm.createObject(LastSynced.class,
new ObjectId()).setTimestamp(System.currentTimeMillis()));
}
// only update the "last synced" time when ALL client data has uploaded
// avoid repeatedly setting "last synced" every time we update "last synced"
// by checking if the current "last synced" time was within the last 10ms
if(progress.isTransferComplete() &&
System.currentTimeMillis() > lastSynced.getTimestamp() + 10) {
notificationRealm.executeTransaction(transactionRealm -> {
transactionRealm.where(LastSynced.class)
.findFirst()
.setTimestamp(System.currentTimeMillis());
Log.v("EXAMPLE", "Updating last synced time to: "
+ System.currentTimeMillis());
});
Log.v("EXAMPLE", "Updated last synced time to: " +
lastSynced.getTimestamp());
}
notificationRealm.close();
});
Upload Progress Listener
// use a "last synced" singleton in the realm to keep track of when the
// realm last successfully completed a sync
app.sync.getSession(config)
.addUploadProgressListener(ProgressMode.INDEFINITELY) { progress: Progress ->
// get the last synced time. Create an instance if it does not already exist.
val notificationRealm = Realm.getInstance(config)
val lastSynced =
notificationRealm.where(LastSynced::class.java).findFirst()
if (lastSynced == null) {
notificationRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.createObject(
LastSynced::class.java,
ObjectId()
).timestamp = System.currentTimeMillis()
}
}
// only update the "last synced" time when ALL client data has uploaded
// avoid repeatedly setting "last synced" every time we update "last synced"
// by checking if the current "last synced" time was within the last 10ms
if (progress.isTransferComplete &&
System.currentTimeMillis() > lastSynced?.timestamp?.plus(10) ?: 0
) {
notificationRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.where(LastSynced::class.java)
.findFirst()
?.timestamp = System.currentTimeMillis()
Log.v(
"EXAMPLE", "Updating last synced time to: "
+ System.currentTimeMillis()
)
}
Log.v(
"EXAMPLE", "Updated last synced time to: " +
lastSynced!!.timestamp
)
}
notificationRealm.close()
}

Now that you've recorded update times for all objects in your application as well as the last time your application completed a sync, it's time to implement the manual recovery process. This example handles two main recovery operations:

  • restoring unsynced inserts and updates from the backup realm

  • deleting objects from the new realm that were previously deleted from the backup realm

You can follow along with the implementation of these operations in the code samples below.

public void handleManualReset(App app, SyncSession session, ClientResetRequiredError error) {
Log.w("EXAMPLE", "Beginning manual reset recovery.");
// Close all instances of the realm -- this application only uses one
globalRealm.close();
try {
Log.w("EXAMPLE", "About to execute the client reset.");
// Move the realm to a backup file -- execute the client reset
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 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);
// Download all realm data from the backend -- ensure that the backend state is
// fully downloaded before proceeding
try {
app.getSync().getSession(globalConfig).downloadAllServerChanges(10000,
TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.w("EXAMPLE", "Opened a fresh instance of the realm.");
// Open the the realm backup -- as a dynamic realm
// (no formal schema; access all data through field lookups)
DynamicRealm backupRealm = DynamicRealm.getInstance(error.getBackupRealmConfiguration());
Log.w("EXAMPLE", "Opened the backup realm.");
// To only migrate unsynced data,
// you'll need to know the last time the realm synced.
// you can keep track of successful sync connections
// locally in an object in the realm
DynamicRealmObject lastSuccessfulSynced =
backupRealm.where("LastSynced").findFirst();
Long lastSuccessfulSyncTime =
lastSuccessfulSynced.getLong("timestamp");
// Migrate unsynced changes: move data from the backup
// instance of the realm to the new "fresh" instance fetched from the backend.
// This includes:
// - copying any objects that updated, but didn't sync from the
// backup realm to the new realm.
// - re-deleting any objects that were deleted locally while we were offline
// Insert any unsynced updated objects to the new realm
// NOTE: this will overwrite any changes made by other clients
// to those objects since the last sync.
// Applications that require finer-grained conflict resolution
// should use custom logic instead.
// This example keeps track of when the object last updated by also writing
// to a "lastUpdated" field on write operations.
RealmQuery<DynamicRealmObject> potatoQuery =
backupRealm.where("Potato")
.greaterThan("lastUpdated", lastSuccessfulSyncTime);
RealmQuery<DynamicRealmObject> onionQuery =
backupRealm.where("Onion")
.greaterThan("lastUpdated", lastSuccessfulSyncTime);
RealmQuery<DynamicRealmObject> riceQuery =
backupRealm.where("Rice")
.greaterThan("lastUpdated", lastSuccessfulSyncTime);
// insert the backup version of all unsynced object updates + creates into the new realm
// NOTE: this process will overwrite writes from other clients, potentially overwriting
// data in fields not modified in the backup realm. Use with caution. If this does not
// meet your application's needs, consider keeping track of the last write for each
// field individually (and recovering them individually, per-field).
for(DynamicRealmObject potato : potatoQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + potato.getString("species"));
newRealm.executeTransaction(transactionRealm ->
transactionRealm.insertOrUpdate(new Potato(potato)));
}
for(DynamicRealmObject onion : onionQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + onion.getString("varietal"));
newRealm.executeTransaction(transactionRealm ->
transactionRealm.insertOrUpdate(new Onion(onion)));
}
for(DynamicRealmObject rice : riceQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + rice.getString("style"));
newRealm.executeTransaction(transactionRealm ->
transactionRealm.insertOrUpdate(new Rice(rice)));
}
// re-delete unsynced deletions from the new realm
// caveat: if an object has been updated SINCE the last update from this client,
// (from another client) this does not delete that object. This doesn't match
// realm's usual "deletes always win" behavior but it isn't possible to
// distinguish between:
// - objects that were deleted from this client after the last sync
// - objects that were created by another client after the last sync
// So instead of deleting innocent objects created by other clients, we let
// other client updates "win" in this case.
// This means that previously deleted (but unsynced) objects could reappear on this
// client after the client reset event.
// get all the ids of objects that haven't been updated since the last client sync
// (anything that's been updated since the last sync should not be deleted)
// -- could be new object, or an object this client deleted but another client modified
Set<ObjectId> allNewPotatoIds = newRealm.where(Potato.class)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream().map(Potato::getId).collect(Collectors.toSet());
Set<ObjectId> allNewOnionIds = newRealm.where(Onion.class)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream().map(Onion::getId).collect(Collectors.toSet());
Set<ObjectId> allNewRiceIds = newRealm.where(Rice.class)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream().map(Rice::getId).collect(Collectors.toSet());
Log.v("EXAMPLE", "number of potatoes in fresh realm" +
"that have not been updated since last sync: " + allNewPotatoIds.size());
Log.v("EXAMPLE", "number of onions in fresh realm" +
"that have not been updated since last sync: " + allNewOnionIds.size());
Log.v("EXAMPLE", "number of rices in fresh realm" +
"that have not been updated since last sync: " + allNewRiceIds.size());
// get all the ids of objects in the backup realm
Set<ObjectId> allOldPotatoIds = backupRealm.where("Potato")
.findAll().stream().map(obj -> obj.getObjectId("_id"))
.collect(Collectors.toSet());
Set<ObjectId> allOldOnionIds = backupRealm.where("Onion")
.findAll().stream().map(obj -> obj.getObjectId("_id"))
.collect(Collectors.toSet());
Set<ObjectId> allOldRiceIds = backupRealm.where("Rice")
.findAll().stream().map(obj -> obj.getObjectId("_id"))
.collect(Collectors.toSet());
Log.v("EXAMPLE", "number of potatoes in the old realm: " +
allOldPotatoIds.size());
Log.v("EXAMPLE", "number of onions in the old realm: " +
allOldOnionIds.size());
Log.v("EXAMPLE", "number of rices in the old realm: " +
allOldRiceIds.size());
// Get the set of:
// all objects in the new realm
// - that have not been updated since last sync
// - that are not in the backup realm
// Those objects were deleted from the backup realm sometime after the last sync.
Set<ObjectId> unsyncedPotatoDeletions = allNewPotatoIds.stream()
.filter(((Predicate<ObjectId>)(allOldPotatoIds::contains)).negate())
.collect(Collectors.toSet());
Set<ObjectId> unsyncedOnionDeletions = allNewOnionIds.stream()
.filter(((Predicate<ObjectId>)(allOldOnionIds::contains)).negate())
.collect(Collectors.toSet());
Set<ObjectId> unsyncedRiceDeletions = allNewRiceIds.stream()
.filter(((Predicate<ObjectId>)(allOldRiceIds::contains)).negate())
.collect(Collectors.toSet());
Log.v("EXAMPLE", "Number of potatos to re-delete: "
+ unsyncedPotatoDeletions.size());
Log.v("EXAMPLE", "Number of onions to re-delete: "
+ unsyncedOnionDeletions.size());
Log.v("EXAMPLE", "Number of rices to re-delete: "
+ unsyncedRiceDeletions.size());
// perform "re-deletions"
for(ObjectId id: unsyncedPotatoDeletions) {
Log.w("EXAMPLE", "Deleting " + unsyncedPotatoDeletions.size()
+ " potato objects.");
newRealm.executeTransaction(transactionRealm -> {
transactionRealm.where(Potato.class).equalTo("_id", id)
.findAll().deleteAllFromRealm();
});
}
for(ObjectId id: unsyncedOnionDeletions) {
Log.w("EXAMPLE", "Deleting " + unsyncedOnionDeletions.size()
+ " onion objects.");
newRealm.executeTransaction(transactionRealm -> {
transactionRealm.where(Onion.class).equalTo("_id", id)
.findAll().deleteAllFromRealm();
});
}
for(ObjectId id: unsyncedRiceDeletions) {
Log.w("EXAMPLE", "Deleting " + unsyncedRiceDeletions.size()
+ " rice objects.");
newRealm.executeTransaction(transactionRealm -> {
transactionRealm.where(Rice.class).equalTo("_id", id)
.findAll().deleteAllFromRealm();
});
}
// Output the state of the freshly downloaded realm, after recovering local data.
Log.v("EXAMPLE", "Number of potato objects in the new realm: "
+ newRealm.where(Potato.class).findAll().size());
Log.v("EXAMPLE", "Number of onion objects in the new realm: "
+ newRealm.where(Onion.class).findAll().size());
Log.v("EXAMPLE", "Number of rice objects in the new realm: "
+ newRealm.where(Rice.class).findAll().size());
// close the realms
backupRealm.close();
newRealm.close();
});
// execute the recovery logic on a background thread
try {
executor.awaitTermination(20000, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
fun handleManualReset(app: App, session: SyncSession?, error: ClientResetRequiredError) {
Log.w("EXAMPLE", "Beginning manual reset recovery.")
// Close all instances of the realm -- this application only uses one
globalRealm!!.close()
try {
Log.w("EXAMPLE", "About to execute the client reset.")
// Move the realm to a backup file: execute the client reset
error.executeClientReset()
Log.w("EXAMPLE", "Executed the client reset.")
} catch (e: 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 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)
// Download all realm data from the backend -- 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", "Opened a fresh instance of the realm.")
// Open the the realm backup -- as a dynamic realm
// (no formal schema; access all data through field lookups)
val backupRealm =
DynamicRealm.getInstance(error.backupRealmConfiguration)
Log.w("EXAMPLE", "Opened the backup realm.")
// To only migrate unsynced data,
// you'll need to know the last time the realm synced.
// you can keep track of successful sync connections
// locally in an object in the realm
val lastSuccessfulSynced =
backupRealm.where("LastSynced").findFirst()
val lastSuccessfulSyncTime = lastSuccessfulSynced!!.getLong("timestamp")
// Migrate unsynced changes: move data from the backup
// instance of the realm to the new "fresh" instance fetched from the backend.
// This includes:
// - copying any objects that updated, but didn't sync from the
// backup realm to the new realm.
// - re-deleting any objects that were deleted locally while we were offline
// Insert any unsynced updated objects to the new realm
// NOTE: this will overwrite any changes made by other clients
// to those objects since the last sync.
// Applications that require finer-grained conflict resolution
// should use custom logic instead.
// This example keeps track of when the object last updated by also writing
// to a "lastUpdated" field on write operations.
val potatoQuery = backupRealm.where("Potato")
.greaterThan("lastUpdated", lastSuccessfulSyncTime)
val onionQuery = backupRealm.where("Onion")
.greaterThan("lastUpdated", lastSuccessfulSyncTime)
val riceQuery = backupRealm.where("Rice")
.greaterThan("lastUpdated", lastSuccessfulSyncTime)
// insert the backup version of all unsynced object updates + creates into the new realm
// NOTE: this process will overwrite writes from other clients, potentially overwriting
// data in fields not modified in the backup realm. Use with caution. If this does not
// meet your application's needs, consider keeping track of the last write for each
// field individually (and recovering them individually, per-field).
for (potato in potatoQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + potato.getString("species"))
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.insertOrUpdate(
Potato(potato)
)
}
}
for (onion in onionQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + onion.getString("varietal"))
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.insertOrUpdate(
Onion(onion)
)
}
}
for (rice in riceQuery.findAll()) {
Log.w("EXAMPLE", "Inserting: " + rice.getString("style"))
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.insertOrUpdate(
Rice(rice)
)
}
}
// re-delete unsynced deletions from the new realm
// caveat: if an object has been updated SINCE the last update from this client,
// (from another client) this does not delete that object. This doesn't match
// realm's usual "deletes always win" behavior but it isn't possible to
// distinguish between:
// - objects that were deleted from this client after the last sync
// - objects that were created by another client after the last sync
// So instead of deleting innocent objects created by other clients, we let
// other client updates "win" in this case.
// This means that previously deleted (but unsynced) objects could reappear on this
// client after the client reset event.
// get all the ids of objects that haven't been updated since the last client sync
// (anything that's been updated since the last sync should not be deleted)
// -- could be new object, or an object this client deleted but another client modified
val allNewPotatoIds =
newRealm.where(
Potato::class.java
)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream()
.map { obj: Potato -> obj._id }
.collect(Collectors.toSet())
val allNewOnionIds =
newRealm.where(
Onion::class.java
)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream()
.map { obj: Onion -> obj._id }
.collect(Collectors.toSet())
val allNewRiceIds =
newRealm.where(
Rice::class.java
)
.lessThan("lastUpdated", lastSuccessfulSyncTime)
.findAll().stream()
.map { obj: Rice -> obj._id }
.collect(Collectors.toSet())
Log.v(
"EXAMPLE", "number of potatoes in fresh realm" +
"that have not been updated since last sync: " + allNewPotatoIds.size
)
Log.v(
"EXAMPLE", "number of onions in fresh realm" +
"that have not been updated since last sync: " + allNewOnionIds.size
)
Log.v(
"EXAMPLE", "number of rices in fresh realm" +
"that have not been updated since last sync: " + allNewRiceIds.size
)
// get all the ids of objects in the backup realm
val allOldPotatoIds =
backupRealm.where("Potato")
.findAll().stream()
.map { obj: DynamicRealmObject ->
obj.getObjectId(
"_id"
)
}
.collect(Collectors.toSet())
val allOldOnionIds =
backupRealm.where("Onion")
.findAll().stream()
.map { obj: DynamicRealmObject ->
obj.getObjectId(
"_id"
)
}
.collect(Collectors.toSet())
val allOldRiceIds =
backupRealm.where("Rice")
.findAll().stream()
.map { obj: DynamicRealmObject ->
obj.getObjectId(
"_id"
)
}
.collect(Collectors.toSet())
Log.v("EXAMPLE", "number of potatoes in the backup realm: " +
allOldPotatoIds.size)
Log.v("EXAMPLE", "number of onions in the backup realm: " +
allOldOnionIds.size)
Log.v("EXAMPLE", "number of rices in the backup realm: " +
allOldRiceIds.size)
// Get the set of:
// all objects in the new realm
// - that have not been updated since last sync
// - that are not in the backup realm
// Those objects were deleted from the backup realm sometime after the last sync.
val unsyncedPotatoDeletions =
allNewPotatoIds.stream()
.filter(Predicate { o: ObjectId ->
allOldPotatoIds.contains(o)
}.negate())
.collect(Collectors.toSet())
val unsyncedOnionDeletions =
allNewOnionIds.stream()
.filter(Predicate { o: ObjectId ->
allOldOnionIds.contains(o)
}.negate())
.collect(Collectors.toSet())
val unsyncedRiceDeletions =
allNewRiceIds.stream()
.filter(Predicate { o: ObjectId ->
allOldRiceIds.contains(o)
}.negate())
.collect(Collectors.toSet())
Log.v("EXAMPLE", "Number of potatos to re-delete: "
+ unsyncedPotatoDeletions.size)
Log.v("EXAMPLE", "Number of onions to re-delete: "
+ unsyncedOnionDeletions.size)
Log.v("EXAMPLE", "Number of rices to re-delete: "
+ unsyncedRiceDeletions.size)
// perform "re-deletions"
for (id in unsyncedPotatoDeletions) {
Log.w(
"EXAMPLE",
"Deleting " + unsyncedPotatoDeletions.size + " potato objects."
)
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.where(
Potato::class.java
).equalTo("_id", id).findAll().deleteAllFromRealm()
}
}
for (id in unsyncedOnionDeletions) {
Log.w(
"EXAMPLE",
"Deleting " + unsyncedOnionDeletions.size + " onion objects."
)
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.where(
Onion::class.java
).equalTo("_id", id).findAll().deleteAllFromRealm()
}
}
for (id in unsyncedRiceDeletions) {
Log.w(
"EXAMPLE",
"Deleting " + unsyncedRiceDeletions.size + " rice objects."
)
newRealm.executeTransaction { transactionRealm: Realm ->
transactionRealm.where(
Rice::class.java
).equalTo("_id", id).findAll().deleteAllFromRealm()
}
}
// Output the state of the freshly downloaded realm, after recovering local data.
Log.v(
"EXAMPLE", "Number of potato objects in the new realm: "
+ newRealm.where(
Potato::class.java
).findAll().size
)
Log.v(
"EXAMPLE", "Number of onion objects in the new realm: "
+ newRealm.where(
Onion::class.java
).findAll().size
)
Log.v(
"EXAMPLE", "Number of rice objects in the new realm: "
+ newRealm.where(
Rice::class.java
).findAll().size
)
// close the realms
backupRealm.close()
newRealm.close()
}
// execute the recovery logic on a background thread
try {
executor.awaitTermination(20000, TimeUnit.MILLISECONDS)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}

Note

This Example is Simplified

This example keeps track of the last time each object was updated. As a result, the recovery operation overwrites the entire object in the new realm if any field was updated after the last successful sync of the backup realm. This could overwrite fields updated by other clients with old data from this client. If your realm objects contain multiple fields containing important data, consider keeping track of the last updated time of each field instead, and recovering each field individually.

Other possible implementations include:

  • Overwrite the entire backend with the backup state: with no "last updated time" or "last synced time", insertOrUpdate() all objects from the backup realm into the new realm. There is no way to recovered unsynced deletions with this approach. This approach overwrites all data written to the backend by other clients since the last sync. Recommended for applications where only one user writes to each realm.

  • Track changes by field: Instead of tracking a "last updated time" for every object, track the "last updated time" for every field. Update fields individually using this logic to avoid overwriting field writes from other clients with old data. Recommended for applications with many fields per-object where conflicts must be resolved at the field level.

  • Track updates separately from objects: Instead of tracking a "last updated time" in the schema of each object, create another model in your schema called Updates. Every time any field in any object (besides Updates) updates, record the primary key, field, and time of the update. During a client reset, "re-write" all of the Update events that occurred after the "last synced time" using the latest value of that field in the backup realm. This approach should replicate all unsynced local changes in the new realm without overwriting any fields with stale data. However, storing the collection of updates could become expensive if your application writes frequently. Recommended for applications where adding "lastUpdated" fields to object models is undesirable.

Back

Client Reset