From fcefa8a1c8c460fff182203145124fb64e0150e8 Mon Sep 17 00:00:00 2001 From: clementetb Date: Mon, 15 Jan 2024 09:51:14 +0100 Subject: [PATCH] Propagate user-level exceptions during client reset (#1581) --- CHANGELOG.md | 2 + dependencies.list | 6 +- .../kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/internal/interop/sync/SyncError.kt | 14 ++-- .../kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/internal/interop/RealmInterop.kt | 52 ++++++------- packages/external/core | 2 +- .../src/main/jni/realm_api_helpers.cpp | 74 ++++++++----------- .../ClientResetRequiredException.kt | 7 +- .../kotlin/test/mongodb/util/AppAdmin.kt | 10 +++ .../test/mongodb/util/AppServicesClient.kt | 3 +- .../mongodb/common/AsymmetricSyncTests.kt | 17 ++++- .../common/SyncClientResetIntegrationTests.kt | 11 +++ 14 files changed, 112 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ebee13f301..74ef8c00d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements * [Sync] Added option to use managed WebSockets via OkHttp instead of Realm's built-in WebSocket client for Sync traffic (Only Android and JVM targets for now). Managed WebSockets offer improved support for proxies and firewalls that require authentication. This feature is currently opt-in and can be enabled by using `AppConfiguration.usePlatformNetworking()`. Managed WebSockets will become the default in a future version. (PR [#1528](https://github.com/realm/realm-kotlin/pull/1528)). +* `AutoClientResetFailed` exception now reports as the throwable cause any user exceptions that might occur during a client reset. (Issue [#1580](https://github.com/realm/realm-kotlin/issues/1580)) ### Fixed * Cache notification callback JNI references at startup to ensure that symbols can be resolved in core callbacks. (Issue [#1577](https://github.com/realm/realm-kotlin/issues/1577)) @@ -28,6 +29,7 @@ ### Internal * Update to Ktor 2.3.4. * Updated to CMake 3.27.7 +* Updated to Realm Core 13.25.0, commit 71f94d75e25bfc8913fcd93ae8de550b57577a4a. ## 1.13.1-SNAPSHOT (YYYY-MM-DD) diff --git a/dependencies.list b/dependencies.list index 15db1c3303..97dfbc8b5e 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,11 +1,11 @@ # Version of MongoDB Realm used by integration tests # See https://github.com/realm/ci/packages/147854 for available versions -MONGODB_REALM_SERVER=2023-11-07 +MONGODB_REALM_SERVER=2023-12-15 # `BAAS` and `BAAS-UI` projects commit hashes matching MONGODB_REALM_SERVER image version # note that the MONGODB_REALM_SERVER image is a nightly build, find the matching commits # for that date within the following repositories: # https://github.com/10gen/baas/ # https://github.com/10gen/baas-ui/ -REALM_BAAS_GIT_HASH=41fa6cdbca47826c20a64f756e21b2c184393e90 -REALM_BAAS_UI_GIT_HASH=b97a27ac858e0e8126aeb63f6ff9734d11029a91 +REALM_BAAS_GIT_HASH=47d9f6170ab1ac2aa64e7b5046e85247f3ac6d30 +REALM_BAAS_UI_GIT_HASH=49157ef4a6af1c1de4dfbad5d7d02543776b25eb diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 31db69c4c8..6dc9c8cb8c 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -67,6 +67,7 @@ expect enum class ErrorCode : CodeDescription { RLM_ERR_TLS_HANDSHAKE_FAILED, RLM_ERR_WRONG_SYNC_TYPE, RLM_ERR_SYNC_WRITE_NOT_ALLOWED, + RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH, RLM_ERR_SYSTEM_ERROR, RLM_ERR_LOGIC, RLM_ERR_NOT_SUPPORTED, diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncError.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncError.kt index dfe69d2ea3..f1d27b8b48 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncError.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/SyncError.kt @@ -29,13 +29,14 @@ data class SyncError constructor( val isFatal: Boolean, val isUnrecognizedByClient: Boolean, val isClientResetRequested: Boolean, - val compensatingWrites: Array + val compensatingWrites: Array, + val userError: Throwable?, ) { // Constructs an SyncError out from a simple code. There are some situations (SyncSessionTransferCompletionCallback) // where we receive an error code rather than a full SyncErrorCode, wrapping the code // simplifies the error handling logic. constructor( - error: CoreError + error: CoreError, ) : this( errorCode = error, originalFilePath = null, @@ -43,7 +44,8 @@ data class SyncError constructor( isFatal = false, isUnrecognizedByClient = false, isClientResetRequested = false, - compensatingWrites = emptyArray() + compensatingWrites = emptyArray(), + userError = null, ) // Constructor used by JNI so we avoid creating too many objects on the JNI side. @@ -56,7 +58,8 @@ data class SyncError constructor( isFatal: Boolean, isUnrecognizedByClient: Boolean, isClientResetRequested: Boolean, - compensatingWrites: Array + compensatingWrites: Array, + userError: Throwable?, ) : this( CoreError(categoryFlags, value, message), originalFilePath, @@ -64,6 +67,7 @@ data class SyncError constructor( isFatal, isUnrecognizedByClient, isClientResetRequested, - compensatingWrites + compensatingWrites, + userError, ) } diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 28bdbb8cb0..19c7a0c29a 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -64,6 +64,7 @@ actual enum class ErrorCode(override val description: String, override val nativ RLM_ERR_TLS_HANDSHAKE_FAILED("TLSHandshakeFailed", realm_errno_e.RLM_ERR_TLS_HANDSHAKE_FAILED), RLM_ERR_WRONG_SYNC_TYPE("WrongSyncType", realm_errno_e.RLM_ERR_WRONG_SYNC_TYPE), RLM_ERR_SYNC_WRITE_NOT_ALLOWED("SyncWriteNotAllowed", realm_errno_e.RLM_ERR_SYNC_WRITE_NOT_ALLOWED), + RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH("SyncLocalClockBeforeEpoch", realm_errno_e.RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH), RLM_ERR_SYSTEM_ERROR("SystemError", realm_errno_e.RLM_ERR_SYSTEM_ERROR), RLM_ERR_LOGIC("Logic", realm_errno_e.RLM_ERR_LOGIC), RLM_ERR_NOT_SUPPORTED("NotSupported", realm_errno_e.RLM_ERR_NOT_SUPPORTED), diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt index 07d2f18a88..9851808195 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt @@ -68,6 +68,7 @@ actual enum class ErrorCode( RLM_ERR_TLS_HANDSHAKE_FAILED("TLSHandshakeFailed", realm_errno.RLM_ERR_TLS_HANDSHAKE_FAILED), RLM_ERR_WRONG_SYNC_TYPE("WrongSyncType", realm_errno.RLM_ERR_WRONG_SYNC_TYPE), RLM_ERR_SYNC_WRITE_NOT_ALLOWED("SyncWriteNotAllowed", realm_errno.RLM_ERR_SYNC_WRITE_NOT_ALLOWED), + RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH("SyncLocalClockBeforeEpoch", realm_errno.RLM_ERR_SYNC_LOCAL_CLOCK_BEFORE_EPOCH), RLM_ERR_SYSTEM_ERROR("SystemError", realm_errno.RLM_ERR_SYSTEM_ERROR), RLM_ERR_LOGIC("Logic", realm_errno.RLM_ERR_LOGIC), RLM_ERR_NOT_SUPPORTED("NotSupported", realm_errno.RLM_ERR_NOT_SUPPORTED), diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index 70d626ddd9..3b0faf5100 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -158,9 +158,9 @@ private fun throwOnError() { errorCodeNativeValue = error.error.value.toInt(), messageNativeValue = error.message?.toKString(), path = error.path?.toKString(), - userError = error.usercode_error?.asStableRef()?.get() + userError = error.user_code_error?.asStableRef()?.get() ).also { - error.usercode_error?.let { disposeUserData(it) } + error.user_code_error?.let { disposeUserData(it) } realm_clear_last_error() } } @@ -638,9 +638,9 @@ actual object RealmInterop { errorCodeNativeValue = err.error.value.toInt(), messageNativeValue = err.message?.toKString(), path = err.path?.toKString(), - userError = err.usercode_error?.asStableRef()?.get() + userError = err.user_code_error?.asStableRef()?.get() ) - err.usercode_error?.let { disposeUserData(it) } + err.user_code_error?.let { disposeUserData(it) } } else { realm_release(realm) } @@ -2534,8 +2534,11 @@ actual object RealmInterop { isFatal = is_fatal, isUnrecognizedByClient = is_unrecognized_by_client, isClientResetRequested = is_client_reset_requested, - compensatingWrites = compensatingWrites - ) + compensatingWrites = compensatingWrites, + userError = user_code_error?.asStableRef()?.get() + ).also { + user_code_error?.let { disposeUserData(it) } + } } val errorCallback = safeUserData(userData) val session = CPointerWrapper(realm_clone(syncSession)) @@ -2565,16 +2568,10 @@ actual object RealmInterop { realm_wrapper.realm_sync_config_set_before_client_reset_handler( syncConfig.cptr(), staticCFunction { userData, beforeRealm -> - val beforeCallback = safeUserData(userData) - val beforeDb = CPointerWrapper(beforeRealm, false) - - // Check if exceptions have been thrown, return true if all went as it should - try { - beforeCallback.onBeforeReset(beforeDb) + stableUserDataWithErrorPropagation(userData) { + val beforeDb = CPointerWrapper(beforeRealm, false) + onBeforeReset(beforeDb) true - } catch (e: Throwable) { - println(e.message) - false } }, StableRef.create(beforeHandler).asCPointer(), @@ -2591,22 +2588,19 @@ actual object RealmInterop { realm_wrapper.realm_sync_config_set_after_client_reset_handler( syncConfig.cptr(), staticCFunction { userData, beforeRealm, afterRealm, didRecover -> - val afterCallback = safeUserData(userData) - val beforeDb = CPointerWrapper(beforeRealm, false) + stableUserDataWithErrorPropagation(userData) { + val beforeDb = CPointerWrapper(beforeRealm, false) - // afterRealm is wrapped inside a ThreadSafeReference so the pointer needs to be resolved - val afterRealmPtr = realm_wrapper.realm_from_thread_safe_reference(afterRealm, null) - val afterDb = CPointerWrapper(afterRealmPtr, false) + // afterRealm is wrapped inside a ThreadSafeReference so the pointer needs to be resolved + val afterRealmPtr = realm_wrapper.realm_from_thread_safe_reference(afterRealm, null) + val afterDb = CPointerWrapper(afterRealmPtr, false) - // Check if exceptions have been thrown, return true if all went as it should - try { - afterCallback.onAfterReset(beforeDb, afterDb, didRecover) - true - } catch (e: Throwable) { - println(e.message) - false - } finally { - realm_wrapper.realm_close(afterRealmPtr) + try { + onAfterReset(beforeDb, afterDb, didRecover) + true + } finally { + realm_wrapper.realm_close(afterRealmPtr) + } } }, StableRef.create(afterHandler).asCPointer(), diff --git a/packages/external/core b/packages/external/core index e593a5f19d..71f94d75e2 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit e593a5f19d0dc205db931ec5618a8c10c95cac90 +Subproject commit 71f94d75e25bfc8913fcd93ae8de550b57577a4a diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp index 9c123c91db..54522828ba 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp @@ -34,20 +34,26 @@ jobject wrap_pointer(JNIEnv* jenv, jlong pointer, jboolean managed = false) { managed); } -inline jboolean jni_check_exception(JNIEnv *jenv = get_env(), bool registrable_callback_error = false) { +inline jboolean jni_check_exception(JNIEnv *jenv = get_env()) { // FIXME https://github.com/realm/realm-kotlin/issues/665 This function is catching and swallowing // the exception. This behavior could leave the SDK in an illegal state. if (jenv->ExceptionCheck()) { - if (registrable_callback_error) { - // setting the user code error is only propagated on certain callbacks. - jthrowable exception = jenv->ExceptionOccurred(); - jenv->ExceptionClear(); - realm_register_user_code_callback_error(jenv->NewGlobalRef(exception)); - } else { - // Print the exception stacktrace in stderr. - jenv->ExceptionDescribe(); - jenv->ExceptionClear(); - } + // Print the exception stacktrace in stderr. + jenv->ExceptionDescribe(); + jenv->ExceptionClear(); + return false; + } + return true; +} + +inline jboolean jni_check_exception_for_callback(JNIEnv *jenv = get_env()) { + if (jenv->ExceptionCheck()) { + // setting the user code error is only propagated on certain callbacks. + jthrowable exception = jenv->ExceptionOccurred(); + jenv->ExceptionClear(); + // This global ref would be released once core has propagated the exception back + // see: create_java_exception and convert_to_jvm_sync_error + realm_register_user_code_callback_error(jenv->NewGlobalRef(exception)); return false; } return true; @@ -79,10 +85,10 @@ inline jobject create_java_exception(JNIEnv *jenv, realm_error_t error) { jint(error.error), error_message, error_path, - static_cast(error.usercode_error) + static_cast(error.user_code_error) ); - if (error.usercode_error) { - (jenv)->DeleteGlobalRef(static_cast(error.usercode_error)); + if (error.user_code_error) { + (jenv)->DeleteGlobalRef(static_cast(error.user_code_error)); } jni_check_exception(jenv); return jenv->PopLocalFrame(exception); @@ -111,15 +117,6 @@ inline std::string get_exception_message(JNIEnv *env) { return env->GetStringUTFChars(message, NULL); } -inline void system_out_println(JNIEnv *env, std::string message) { - jclass system_class = env->FindClass("java/lang/System"); - jfieldID field_id = env->GetStaticFieldID(system_class, "out", "Ljava/io/PrintStream;"); - jobject system_out = env->GetStaticObjectField(system_class, field_id); - jclass print_stream_class = env->FindClass("java/io/PrintStream"); - jmethodID method_id = env->GetMethodID(print_stream_class, "println", "(Ljava/lang/String;)V"); - env->CallVoidMethod(system_out, method_id, to_jstring(env, message)); -} - void realm_changed_callback(void* userdata) { auto env = get_env(true); @@ -161,7 +158,7 @@ bool migration_callback(void *userdata, realm_t *old_realm, realm_t *new_realm, wrap_pointer(env, reinterpret_cast(new_realm), false), wrap_pointer(env, reinterpret_cast(schema)) ); - bool failed = jni_check_exception(env, true); + bool failed = jni_check_exception_for_callback(env); env->PopLocalFrame(NULL); return failed; } @@ -559,7 +556,7 @@ bool realm_should_compact_callback(void* userdata, uint64_t total_bytes, uint64_ jobject callback = static_cast(userdata); jboolean result = env->CallBooleanMethod(callback, java_should_compact_method, jlong(total_bytes), jlong(used_bytes)); - return jni_check_exception(env, true) && result; + return jni_check_exception_for_callback(env) && result; } bool realm_data_initialization_callback(void* userdata, realm_t*) { @@ -569,7 +566,7 @@ bool realm_data_initialization_callback(void* userdata, realm_t*) { jobject callback = static_cast(userdata); env->CallVoidMethod(callback, java_data_init_method); - return jni_check_exception(env, true); + return jni_check_exception_for_callback(env); } static void send_request_via_jvm_transport(JNIEnv *jenv, jobject network_transport, const realm_http_request_t request, jobject j_response_callback) { @@ -1001,7 +998,7 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) static JavaMethod sync_error_constructor(jenv, JavaClassGlobalDef::sync_error(), "", - "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZ[Lio/realm/kotlin/internal/interop/sync/CoreCompensatingWriteInfo;)V"); + "(IILjava/lang/String;Ljava/lang/String;Ljava/lang/String;ZZZ[Lio/realm/kotlin/internal/interop/sync/CoreCompensatingWriteInfo;Ljava/lang/Throwable;)V"); jint category = static_cast(error.status.categories); jint value = static_cast(error.status.error); @@ -1089,10 +1086,14 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) is_fatal, is_unrecognized_by_client, is_client_reset_requested, - j_compensating_write_info_array + j_compensating_write_info_array, + static_cast(error.user_code_error) ); jni_check_exception(jenv); + if(error.user_code_error) { + jenv->DeleteGlobalRef(static_cast(error.user_code_error)); + } return jenv->PopLocalFrame(result); } @@ -1192,14 +1193,7 @@ before_client_reset(void* userdata, realm_t* before_realm) { env->PushLocalFrame(1); jobject before_pointer = wrap_pointer(env, reinterpret_cast(before_realm), false); env->CallVoidMethod(static_cast(userdata), java_before_callback_function, before_pointer); - - bool result = true; - if (env->ExceptionCheck()) { - std::string exception_message = get_exception_message(env); - std::string message_template = "An error has occurred in the 'onBefore' callback: "; - system_out_println(env, message_template.append(exception_message)); - result = false; - } + bool result = jni_check_exception_for_callback(env); env->PopLocalFrame(NULL); return result; } @@ -1222,13 +1216,7 @@ after_client_reset(void* userdata, realm_t* before_realm, jobject after_pointer = wrap_pointer(env, reinterpret_cast(after_realm_ptr), false); env->CallVoidMethod(static_cast(userdata), java_after_callback_function, before_pointer, after_pointer, did_recover); realm_close(after_realm_ptr); - bool result = true; - if (env->ExceptionCheck()) { - std::string exception_message = get_exception_message(env); - std::string message_template = "An error has occurred in the 'onAfter' callback: "; - system_out_println(env, message_template.append(exception_message)); - result = false; - } + bool result = jni_check_exception_for_callback(env); env->PopLocalFrame(NULL); return result; } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt index 26b2daa2e4..48a7fcfb72 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/ClientResetRequiredException.kt @@ -30,8 +30,11 @@ import io.realm.kotlin.mongodb.internal.createMessageFromSyncError */ public class ClientResetRequiredException constructor( private val appPointer: RealmAppPointer, - error: SyncError -) : Throwable(message = createMessageFromSyncError(error.errorCode)) { + error: SyncError, +) : Throwable( + message = createMessageFromSyncError(error.errorCode), + cause = error.userError, +) { /** * Path to the original (local) copy of the realm when the Client Reset event was triggered. diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppAdmin.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppAdmin.kt index 8e79a606df..e15939520e 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppAdmin.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppAdmin.kt @@ -96,6 +96,11 @@ interface AppAdmin { */ suspend fun countDocuments(clazz: String): Int + /** + * Delete documents of a given type. + */ + suspend fun deleteDocuments(database: String, clazz: String, query: String): JsonObject? + fun closeClient() } @@ -191,6 +196,11 @@ class AppAdminImpl( app.countDocuments(clazz) } + override suspend fun deleteDocuments(database: String, clazz: String, query: String): JsonObject? = + baasClient.run { + app.deleteDocument(database, clazz, query) + } + override fun closeClient() { baasClient.closeClient() } diff --git a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt index 8cd6982fd1..4757b3bb45 100644 --- a/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt +++ b/packages/test-sync/src/commonMain/kotlin/io/realm/kotlin/test/mongodb/util/AppServicesClient.kt @@ -47,7 +47,6 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement @@ -623,7 +622,7 @@ class AppServicesClient( } ?: throw IllegalStateException("Unexpected result: $result") } - private suspend fun BaasApp.deleteDocument( + suspend fun BaasApp.deleteDocument( db: String, clazz: String, query: String diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt index 261a5b7845..272c2be08b 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt @@ -42,6 +42,7 @@ import io.realm.kotlin.types.RealmList import io.realm.kotlin.types.RealmObject import io.realm.kotlin.types.annotations.PersistedName import io.realm.kotlin.types.annotations.PrimaryKey +import kotlinx.atomicfu.atomic import kotlinx.coroutines.delay import org.mongodb.kbson.ObjectId import kotlin.test.AfterTest @@ -114,6 +115,9 @@ class AsymmetricSyncTests { @AfterTest fun tearDown() { realm.close() + runBlocking { + app.deleteDocuments(app.clientAppId, Measurement::class.simpleName!!, "{}") + } if (this::app.isInitialized) { app.close() } @@ -133,6 +137,9 @@ class AsymmetricSyncTests { ) } } + + realm.syncSession.uploadAllLocalChanges() + verifyDocuments(clazz = "Measurement", expectedCount = newDocuments, initialCount = initialServerDocuments) } @@ -311,21 +318,23 @@ class AsymmetricSyncTests { } private suspend fun verifyDocuments(clazz: String, expectedCount: Int, initialCount: Int) { + // Variable shared across while must be atomic + // https://youtrack.jetbrains.com/issue/KT-64139/Native-Bug-with-while-loop-coroutine-which-is-started-and-stopped-on-the-same-thread + var documents = atomic(0) var found = false - var documents = 0 var attempt = 30 // The translator might be slow to incorporate changes into MongoDB, so we retry for a bit // before giving up. while (!found && attempt > 0) { - documents = app.countDocuments(clazz) - initialCount - if (documents == expectedCount) { + documents.value = app.countDocuments(clazz) - initialCount + if (documents.value == expectedCount) { found = true } else { attempt -= 1 delay(1.seconds) } } - assertTrue(found, "Number of documents was: $documents") + assertTrue(found, "Number of documents was: ${documents.value} [initialCount: $initialCount, expectedCount: $expectedCount]") } private fun useDynamicRealm(function: (DynamicMutableRealm) -> Unit) { diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt index 6b8bab5b5e..427d3da81d 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncClientResetIntegrationTests.kt @@ -64,6 +64,7 @@ import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse +import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertTrue import kotlin.test.fail @@ -742,6 +743,11 @@ class SyncClientResetIntegrationTests { "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) + assertIs(exception.cause) + assertEquals( + "User exception", + exception.cause?.message + ) channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } @@ -754,6 +760,11 @@ class SyncClientResetIntegrationTests { "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) + assertIs(exception.cause) + assertEquals( + "User exception", + exception.cause?.message + ) channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } }).build()