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

Adding example of Client Reset #7354

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 3 additions & 3 deletions dependencies.list
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ REALM_CORE=10.3.3
MONGODB_REALM_SERVER=2021-03-09

# Common Android settings across projects
GRADLE_BUILD_TOOLS=4.0.0
GRADLE_BUILD_TOOLS=4.1.0
ANDROID_BUILD_TOOLS=29.0.3
KOTLIN=1.3.72
KOTLIN_COROUTINES=1.3.9
KOTLIN=1.4.31
KOTLIN_COROUTINES=1.4.2

# Common classpath dependencies
gradle=6.5
Expand Down
14 changes: 6 additions & 8 deletions examples/mongoDbRealmExample/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,10 @@ android {

buildTypes {
// Configure server and App Id.
// The default server is https://realm-dev.mongodb.com/ . Go to that and copy the MongoDB
// The default server is https://realm.mongodb.com/ . Go to that and copy the MongoDB
// Realm App Id.
//
// If you are running a local version of MongoDB Realm, modify endpoint accordingly. Most
// likely it is "http://localhost:9090"
def mongodbRealmUrl = "https://realm-dev.mongodb.com"
def appId = "my-app-id"
def mongodbRealmUrl = "https://realm.mongodb.com"
def appId = "counter-app-snuns"
debug {
buildConfigField "String", "MONGODB_REALM_URL", "\"${mongodbRealmUrl}\""
buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\""
Expand All @@ -65,8 +62,9 @@ realm {
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel:2.3.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
8 changes: 7 additions & 1 deletion examples/mongoDbRealmExample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,13 @@
</activity>
<activity
android:name=".LoginActivity"
android:label="Login"></activity>
android:label="Login">
</activity>
<activity
android:name=".ClientResetActivity"
android:launchMode="singleInstance"
android:label="ClientReset">
</activity>
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2021 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.mongodb.realm.example

import android.os.Bundle
import android.os.SystemClock
import android.view.View
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil
import com.mongodb.realm.example.databinding.ActivityClientresetBinding
import io.realm.*
import io.realm.mongodb.sync.*
import me.zhanghai.android.materialprogressbar.MaterialProgressBar
import java.util.*
import java.util.concurrent.ConcurrentLinkedQueue

/**
* This class is used as an example on how to implement Client Reset.
*
* This activity is launched with `singleInstance` so no matter how many times it is
* started only one instance is running. However, when finished, it will just return to whatever is
* on the top of navigation stack.
*
* Pressing back has been disabled to prevent the [clientResetHelper] from accidentally running
* while another Activity is displayed.
*/
class ClientResetActivity : AppCompatActivity() {

companion object {
// Track Client Reset errors as a queue of errors as the chance of all Realms
// connected to a single app instance experiencing Client Resets is quite high, e.g.
// in the case where Sync was terminated then restarted on the server.
var RESET_ERRORS = ConcurrentLinkedQueue<Pair<ClientResetRequiredError, SyncConfiguration>>()
}

// Run Client Reset logic on a separate helper thread to make it easier to implement timeouts.
// Note, this Runnable is not particular safe as it will continue running even if the Activity
// is excited
private val clientResetHelper = Runnable {
var errorReported = false;
ClientResetLoop@ while(true) {
val error: Pair<ClientResetRequiredError, SyncConfiguration> = RESET_ERRORS.poll() ?: break
val clientReset: ClientResetRequiredError = error.first
val config: SyncConfiguration = error.second

// The background Sync Client take about 10 seconds to fully close the connection and thus
// the background Realm. Set timeout to 20 seconds.
var maxWait = 20
while (Realm.getGlobalInstanceCount(config) > 0) {
if (maxWait == 0) {
runOnUiThread {
progressBar.visibility = View.INVISIBLE
statusView.text = "'${config.realmFileName}' did not fully close, so database could not be reset. Aborting"
}
errorReported = true
break@ClientResetLoop
} else {
maxWait--
runOnUiThread {
statusView.text = "Waiting for '${config.realmFileName}' to fully close ($maxWait): ${Realm.getGlobalInstanceCount(config)}"
}
SystemClock.sleep(1000)
}
}
clientReset.executeClientReset()
runOnUiThread {
statusView.text = ""
}
}
if (!errorReported) {
finish()
}
}

private lateinit var binding: ActivityClientresetBinding

private lateinit var statusView: TextView
private lateinit var progressBar: MaterialProgressBar

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_clientreset)
statusView = binding.status
progressBar = binding.progressbar
}

override fun onResume() {
super.onResume()
Thread(clientResetHelper).start();
}

override fun onBackPressed() {
Toast.makeText(
this,
"Pressing 'Back' is disabled while Client Reset is running.",
Toast.LENGTH_LONG
).show()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,7 @@ import io.realm.kotlin.syncSession
import io.realm.kotlin.where
import io.realm.log.RealmLog
import io.realm.mongodb.User
import io.realm.mongodb.sync.ProgressListener
import io.realm.mongodb.sync.ProgressMode
import io.realm.mongodb.sync.SyncConfiguration
import io.realm.mongodb.sync.SyncSession
import io.realm.mongodb.sync.*
import me.zhanghai.android.materialprogressbar.MaterialProgressBar
import java.util.*
import java.util.concurrent.atomic.AtomicBoolean
Expand Down Expand Up @@ -97,43 +94,37 @@ class CounterActivity : AppCompatActivity() {
user = loggedInUser
val user = user
if (user != null) {
// Create a RealmConfiguration for our user
// Use user id as partition value, so each user gets an unique view.
// FIXME Right now we are using waitForInitialRemoteData and a more advanced
// initialData block due to Sync only supporting ObjectId keys. This should
// be changed once natural keys are supported.
// Create a RealmConfiguration for our user. Use user id as partition value, so each
// user gets an unique view.
val config = SyncConfiguration.Builder(user, user.id)
.initialData {
if (it.isEmpty) {
it.insert(CRDTCounter())
}
it.insert(CRDTCounter(user.id))
}
.clientResetHandler { session, error ->
ClientResetActivity.RESET_ERRORS.add(Pair(error, session.configuration))
val intent = Intent(this, ClientResetActivity::class.java)
startActivity(intent)
}
.waitForInitialRemoteData()
.build()

// This will automatically sync all changes in the background for as long as the Realm is open
Realm.getInstanceAsync(config, object: Realm.Callback() {
override fun onSuccess(realm: Realm) {
[email protected] = realm

counter = realm.where<CRDTCounter>().findFirstAsync()
counter.addChangeListener<CRDTCounter> { obj, _ ->
if (obj.isValid) {
counterView.text = String.format(Locale.US, "%d", counter.count)
} else {
counterView.text = "-"
}
realm = Realm.getInstance(config).also {
counter = it.where<CRDTCounter>().findFirstAsync()
counter.addChangeListener<CRDTCounter> { obj, _ ->
if (obj.isValid) {
counterView.text = String.format(Locale.US, "%d", counter.count)
} else {
counterView.text = "-"
}
}

// Setup progress listeners for indeterminate progress bars
session = realm.syncSession
session.run {
addDownloadProgressListener(ProgressMode.INDEFINITELY, downloadListener)
addUploadProgressListener(ProgressMode.INDEFINITELY, uploadListener)
}
// Setup progress listeners for indeterminate progress bars
session = it.syncSession
session.run {
addDownloadProgressListener(ProgressMode.INDEFINITELY, downloadListener)
addUploadProgressListener(ProgressMode.INDEFINITELY, uploadListener)
}
})
counterView.text = "-"
counterView.text = "-"
}
}
}

Expand All @@ -144,6 +135,7 @@ class CounterActivity : AppCompatActivity() {
removeProgressListener(downloadListener)
removeProgressListener(uploadListener)
}
// Close Realm here to make sure it is closed when navigating to other Activities.
realm?.close()
}
}
Expand All @@ -156,15 +148,16 @@ class CounterActivity : AppCompatActivity() {
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_logout -> {
val user = user
user?.logOutAsync {
if (it.isSuccess) {
realm?.close()
this.user = loggedInUser
} else {
RealmLog.error(it.error.toString())
}
}
// val user = user
// user?.logOutAsync {
// if (it.isSuccess) {
// realm?.close()
// this.user = loggedInUser
// } else {
// RealmLog.error(it.error.toString())
// }
// }
ResetHelper.triggerClientReset(user!!.app.sync, realm!!.syncSession)
true
}
else -> super.onOptionsItemSelected(item)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ import io.realm.annotations.RealmField
import io.realm.annotations.Required
import org.bson.types.ObjectId

open class CRDTCounter : RealmObject() {
open class CRDTCounter(userId: String) : RealmObject() {

// Required by Realm
constructor(): this("")

@PrimaryKey
@RealmField("_id")
var id: ObjectId = ObjectId.get()
var id: String = userId

@Required
private val counter: MutableRealmInteger = MutableRealmInteger.valueOf(0L)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.realm.mongodb.sync

class ResetHelper {
companion object {
fun triggerClientReset(sync: Sync, session: SyncSession) {
sync.simulateClientReset(session)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">

<data />

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:padding="32dp"
android:layout_centerInParent="true"
android:orientation="vertical">

<me.zhanghai.android.materialprogressbar.MaterialProgressBar
android:id="@+id/progressbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="64dp"
android:layout_gravity="center_horizontal"
android:indeterminate="true"
style="@style/Widget.MaterialProgressBar.ProgressBar" />

<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
app:fontFamily="sans-serif" />
</LinearLayout>

</RelativeLayout>
</layout>