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

Add support for automatic resetting incremental backoff when network status changes #1491

Merged
merged 20 commits into from
Sep 1, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public data class RealmInstantImpl(override val seconds: Long, override val nano
}
}

internal fun RealmInstant.toDuration(): Duration {
public fun RealmInstant.toDuration(): Duration {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for this PR, but we could consider to make this a public-public extension.

return epochSeconds.seconds + nanosecondsOfSecond.nanoseconds
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
Expand Down Expand Up @@ -58,56 +59,64 @@ class RealmSyncInitializer : Initializer<Context> {
}
}

private var connectivityManager: ConnectivityManager? = null

@Suppress("invisible_member", "invisible_reference")
override fun create(context: Context): Context {
connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
// There has been a fair amount of changes and deprecations with regard to how to listen
// to the network status. ConnectivityManager#CONNECTIVITY_ACTION was deprecated in API 28
// but ConnectivityManager.NetworkCallback became available a lot sooner in API 21, so
// we default to this as soon as possible.
//
// On later versions of Android (need reference), these callbacks will also only trigger
// if the app is in the foreground.
//
// The current implementation is a best-effort in detecting when the network is available
// again.
//
// See https://developer.android.com/training/basics/network-ops/reading-network-state
// See https://developer.android.com/reference/android/net/ConnectivityManager#CONNECTIVITY_ACTION
// See https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /* 21 */) {
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /* 23 */) {
request.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
@Suppress("invisible_member", "invisible_reference")
RealmLog.info("Register ConnectivityManager network callbacks")
connectivityManager?.registerNetworkCallback(
request.build(),
object : NetworkCallback() {
override fun onAvailable(network: Network) {
NetworkStateObserver.notifyConnectionChange(true)
}
override fun onUnavailable() {
NetworkStateObserver.notifyConnectionChange(false)
val result: Int = context.checkCallingOrSelfPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
if (result == PackageManager.PERMISSION_GRANTED) {
try {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
// There has been a fair amount of changes and deprecations with regard to how to listen
// to the network status. ConnectivityManager#CONNECTIVITY_ACTION was deprecated in API 28
// but ConnectivityManager.NetworkCallback became available a lot sooner in API 21, so
// we default to this as soon as possible.
//
// On later versions of Android (need reference), these callbacks will also only trigger
// if the app is in the foreground.
//
// The current implementation is a best-effort in detecting when the network is available
// again.
//
// See https://developer.android.com/training/basics/network-ops/reading-network-state
// See https://developer.android.com/reference/android/net/ConnectivityManager#CONNECTIVITY_ACTION
// See https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /* 21 */) {
val request = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP /* 23 */) {
request.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
RealmLog.info("Register ConnectivityManager network callbacks")
connectivityManager?.registerNetworkCallback(
request.build(),
object : NetworkCallback() {
override fun onAvailable(network: Network) {
NetworkStateObserver.notifyConnectionChange(true)
}

override fun onUnavailable() {
NetworkStateObserver.notifyConnectionChange(false)
}
}
)
} else {
RealmLog.info("Register BroadcastReceiver connectivity callbacks")
@Suppress("DEPRECATION")
context.registerReceiver(
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val isConnected: Boolean = isConnected(connectivityManager)
NetworkStateObserver.notifyConnectionChange(isConnected)
}
},
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
)
}
)
} catch (ex: Exception) {
RealmLog.warn("Something went wrong trying to register a network state listener: $ex")
}
} else {
@Suppress("invisible_member", "invisible_reference")
RealmLog.info("Register BroadcastReceiver connectivity callbacks")
@Suppress("DEPRECATION")
context.registerReceiver(
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val isConnected: Boolean = isConnected(connectivityManager)
NetworkStateObserver.notifyConnectionChange(isConnected)
}
},
IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
)
RealmLog.warn("It was not possible to register a network state listener. " +
"ACCESS_NETWORK_STATE was not granted.")
}
return context
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.realm.kotlin.internal.interop.RealmAppPointer
import io.realm.kotlin.internal.interop.RealmInterop
import io.realm.kotlin.internal.interop.RealmUserPointer
import io.realm.kotlin.internal.interop.sync.NetworkTransport
import io.realm.kotlin.internal.toDuration
import io.realm.kotlin.internal.util.DispatcherHolder
import io.realm.kotlin.internal.util.Validation
import io.realm.kotlin.internal.util.use
Expand All @@ -36,6 +37,8 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

internal typealias AppResources = Triple<DispatcherHolder, NetworkTransport, RealmAppPointer>

Expand All @@ -48,10 +51,10 @@ public class AppImpl(
internal val appNetworkDispatcher: DispatcherHolder
private val networkTransport: NetworkTransport

private var lastOnlineStateReportedMs: Long? = null
private var lastOnlineStateReported: Duration? = null
private var lastConnectedState: Boolean? = null // null = unknown, true = connected, false = disconnected
@Suppress("MagicNumber")
private val reconnectThresholdMs = 5_000 // 5 seconds
private val reconnectThreshold = 5.seconds

@Suppress("invisible_member", "invisible_reference", "MagicNumber")
private val connectionListener = NetworkStateObserver.ConnectionListener { connectionAvailable ->
Expand All @@ -65,18 +68,16 @@ public class AppImpl(
// "isOnline" messages in short order. So in order to prevent resetting the network
// too often we throttle messages, so a reconnect can only happen ever 5 seconds.
RealmLog.debug("Network state change detected. ConnectionAvailable = $connectionAvailable")
val nowMs: Long = RealmInstant.now().let { timestamp ->
timestamp.epochSeconds * 1000L + timestamp.nanosecondsOfSecond / 1_000_000L
}
if (connectionAvailable && (lastOnlineStateReportedMs == null || nowMs - lastOnlineStateReportedMs!! > reconnectThresholdMs)
val now: Duration = RealmInstant.now().toDuration()
if (connectionAvailable && (lastOnlineStateReported == null || now.minus(lastOnlineStateReported!!) > reconnectThreshold)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 ... I guess you could make it more Kotlin idiomatic with something like

if (connectionAvailable && (lastOnlineStateReported?.let { now - it > reconnectThreshold } != false))

but don't mind keeping it as is.

) {
RealmLog.info("Trigger network reconnect.")
try {
sync.reconnect()
} catch (ex: Exception) {
RealmLog.error(ex.toString())
}
lastOnlineStateReportedMs = nowMs
lastOnlineStateReported = now
}
lastConnectedState = connectionAvailable
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import io.realm.kotlin.mongodb.App
import io.realm.kotlin.mongodb.syncSession

/**
* A <i>Device Sync</i> manager responsible for controlling all sync sessions across all realms
* A _Device Sync_ manager responsible for controlling all sync sessions across all realms
* associated with a given [App] instance. For session functionality associated with a single
rorbech marked this conversation as resolved.
Show resolved Hide resolved
* realm, see [syncSession].
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package io.realm.kotlin.test.mongodb

import io.realm.kotlin.annotations.ExperimentalRealmSerializerApi
import io.realm.kotlin.internal.interop.RealmInterop
import io.realm.kotlin.internal.interop.SynchronizableObject
import io.realm.kotlin.internal.interop.sync.NetworkTransport
import io.realm.kotlin.internal.platform.runBlocking
import io.realm.kotlin.internal.platform.singleThreadDispatcher
Expand Down Expand Up @@ -72,6 +73,7 @@ open class TestApp private constructor(
pairAdminApp: Pair<App, AppAdmin>
) : App by pairAdminApp.first, AppAdmin by pairAdminApp.second {

var mutex = SynchronizableObject()
var isClosed: Boolean = false
val app: App = pairAdminApp.first

Expand Down Expand Up @@ -123,39 +125,41 @@ open class TestApp private constructor(
}

override fun close() {
if (isClosed) {
return
}
// This is needed to "properly reset" all sessions across tests since deleting users
// directly using the REST API doesn't do the trick
runBlocking {
try {
while (currentUser != null) {
(currentUser as User).logOut()
mutex.withLock {
if (isClosed) {
return
}
// This is needed to "properly reset" all sessions across tests since deleting users
// directly using the REST API doesn't do the trick
runBlocking {
try {
while (currentUser != null) {
(currentUser as User).logOut()
}
deleteAllUsers()
} catch (ex: Exception) {
// Some tests might render the server inaccessible, preventing us from
// deleting users. Assume those tests know what they are doing and
// ignore errors here.
RealmLog.warn("Server side users could not be deleted: $ex")
}
deleteAllUsers()
} catch (ex: Exception) {
// Some tests might render the server inaccessible, preventing us from
// deleting users. Assume those tests know what they are doing and
// ignore errors here.
RealmLog.warn("Server side users could not be deleted: $ex")
}
}

if (dispatcher is CloseableCoroutineDispatcher) {
dispatcher.close()
}
app.close()
if (dispatcher is CloseableCoroutineDispatcher) {
dispatcher.close()
}
app.close()

// Close network client resources
closeClient()
// Close network client resources
closeClient()

// Make sure to clear cached apps before deleting files
RealmInterop.realm_clear_cached_apps()
// Make sure to clear cached apps before deleting files
RealmInterop.realm_clear_cached_apps()

// Delete metadata Realm files
PlatformUtils.deleteTempDir("${configuration.syncRootDirectory}/mongodb-realm")
isClosed = true
// Delete metadata Realm files
PlatformUtils.deleteTempDir("${configuration.syncRootDirectory}/mongodb-realm")
isClosed = true
}
}

companion object {
Expand Down