From 15a01b97f6ccbc299b7e4c3d41585518466529a1 Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 22 Nov 2023 13:08:11 +0100 Subject: [PATCH 01/15] Add error propagation on JVM --- .../kotlin/internal/interop/sync/SyncError.kt | 14 ++++--- packages/external/core | 2 +- .../src/main/jni/realm_api_helpers.cpp | 38 +++++-------------- .../ClientResetRequiredException.kt | 7 +++- .../common/SyncClientResetIntegrationTests.kt | 11 ++++++ 5 files changed, 36 insertions(+), 36 deletions(-) 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/external/core b/packages/external/core index 7556b535aa..424a49e25a 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 7556b535aa7b27d49c13444894f7e9db778b3203 +Subproject commit 424a49e25aaa2aa975e9076620a0504c914143b8 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 03783aaf80..ec6fdcd2ab 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 @@ -90,15 +90,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); @@ -716,7 +707,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); @@ -788,7 +779,7 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) } } - return jenv->NewObject( + jobject result = jenv->NewObject( JavaClassGlobalDef::sync_error(), sync_error_constructor, category, @@ -799,8 +790,13 @@ 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.usercode_error) ); + + jni_check_exception(jenv); + jenv->DeleteGlobalRef(static_cast(error.usercode_error)); + return result; } void sync_set_error_handler(realm_sync_config_t* sync_config, jobject error_handler) { @@ -887,14 +883,7 @@ before_client_reset(void* userdata, realm_t* before_realm) { "(Lio/realm/kotlin/internal/interop/NativePointer;)V"); auto before_pointer = wrap_pointer(env, reinterpret_cast(before_realm), false); env->CallVoidMethod(static_cast(userdata), java_before_callback_function, before_pointer); - 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)); - return false; - } - - return true; + return jni_check_exception(env); } bool @@ -913,14 +902,7 @@ after_client_reset(void* userdata, realm_t* before_realm, auto 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); - 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)); - return false; - } - - return true; + return jni_check_exception(env); } void 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/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 01fd16cb75..62304d3d3e 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 @@ -62,6 +62,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 @@ -740,6 +741,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.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } @@ -752,6 +758,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.trySend(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } }).build() From f9ea7150a127e04f5d92922d5eabf79e5c0f1550 Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 22 Nov 2023 13:26:47 +0100 Subject: [PATCH 02/15] Add support for darwin --- .../kotlin/internal/interop/RealmInterop.kt | 44 ++++++++----------- .../common/SyncClientResetIntegrationTests.kt | 4 +- 2 files changed, 21 insertions(+), 27 deletions(-) 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 213be632f4..ed7d380369 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 @@ -2487,8 +2487,11 @@ actual object RealmInterop { isFatal = is_fatal, isUnrecognizedByClient = is_unrecognized_by_client, isClientResetRequested = is_client_reset_requested, - compensatingWrites = compensatingWrites - ) + compensatingWrites = compensatingWrites, + userError = usercode_error?.asStableRef()?.get() + ).also { + usercode_error?.let { disposeUserData(it) } + } } val errorCallback = safeUserData(userData) val session = CPointerWrapper(realm_clone(syncSession)) @@ -2518,16 +2521,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(), @@ -2544,22 +2541,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/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 62304d3d3e..1c3e1a9a16 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 @@ -741,7 +741,7 @@ class SyncClientResetIntegrationTests { "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) - assertIs(exception.cause) + assertIs(exception.cause) assertEquals( "User exception", exception.cause?.message @@ -758,7 +758,7 @@ class SyncClientResetIntegrationTests { "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", exception.message ) - assertIs(exception.cause) + assertIs(exception.cause) assertEquals( "User exception", exception.cause?.message From 39896f5b21df8fbee73fdb11990a879671f8e3c2 Mon Sep 17 00:00:00 2001 From: Clemente Date: Wed, 22 Nov 2023 13:37:40 +0100 Subject: [PATCH 03/15] Add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c67227d993..91cf2bdff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Enhancements * Support for experimental K2-compilation with `kotlin.experimental.tryK2=true`. (Issue [#1483](https://github.com/realm/realm-kotlin/issues/1483)) +* `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 * None. From e31fded6d59c51e589fe7ea0610decaee16c3750 Mon Sep 17 00:00:00 2001 From: Clemente Date: Thu, 21 Dec 2023 15:47:10 +0100 Subject: [PATCH 04/15] Bump core --- packages/external/core | 2 +- .../src/main/jni/realm_api_helpers.cpp | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/external/core b/packages/external/core index 424a49e25a..71f94d75e2 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 424a49e25aaa2aa975e9076620a0504c914143b8 +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 16c8c7bbbb..3b58ad2fea 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 @@ -79,10 +79,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); @@ -1081,11 +1081,11 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) is_unrecognized_by_client, is_client_reset_requested, j_compensating_write_info_array, - static_cast(error.usercode_error) + static_cast(error.user_code_error) ); jni_check_exception(jenv); - jenv->DeleteGlobalRef(static_cast(error.usercode_error)); + jenv->DeleteGlobalRef(static_cast(error.user_code_error)); return jenv->PopLocalFrame(result); } @@ -1185,7 +1185,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 = jni_check_exception(env); + bool result = jni_check_exception(env, true); return result; } @@ -1207,7 +1207,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 = jni_check_exception(env); + bool result = jni_check_exception(env, true); return result; } From b908e9c3ffe70370ee9287bc67b9d8e7ad47b2e3 Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 22 Dec 2023 09:47:58 +0100 Subject: [PATCH 05/15] Update native realminterop --- .../io/realm/kotlin/internal/interop/RealmInterop.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 dcca1077dd..4adab1896d 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) } @@ -2535,9 +2535,9 @@ actual object RealmInterop { isUnrecognizedByClient = is_unrecognized_by_client, isClientResetRequested = is_client_reset_requested, compensatingWrites = compensatingWrites, - userError = usercode_error?.asStableRef()?.get() + userError = user_code_error?.asStableRef()?.get() ).also { - usercode_error?.let { disposeUserData(it) } + user_code_error?.let { disposeUserData(it) } } } val errorCallback = safeUserData(userData) From 9247a237abaa7bf5bd4006bceea13d7904b0dc04 Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 22 Dec 2023 10:27:33 +0100 Subject: [PATCH 06/15] Add missing error code --- .../kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt | 1 + .../src/jvm/kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt | 1 + .../kotlin/io/realm/kotlin/internal/interop/ErrorCode.kt | 1 + 3 files changed, 3 insertions(+) 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/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), From 406af1994f391cb66eedd41a9cd7d43ed29e3b4c Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 22 Dec 2023 12:51:29 +0100 Subject: [PATCH 07/15] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d6a1b42c..74ef8c00d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,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) From 0b6a4cf4859f51c667443a2c9ea7654e78da85ff Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 22 Dec 2023 13:08:02 +0100 Subject: [PATCH 08/15] Update jni helpers --- packages/jni-swig-stub/src/main/jni/realm_api_helpers.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 3b58ad2fea..b7de9cd9b7 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 @@ -1085,7 +1085,9 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) ); jni_check_exception(jenv); - jenv->DeleteGlobalRef(static_cast(error.user_code_error)); + if(error.user_code_error) { + jenv->DeleteGlobalRef(static_cast(error.user_code_error)); + } return jenv->PopLocalFrame(result); } @@ -1186,6 +1188,7 @@ before_client_reset(void* userdata, realm_t* before_realm) { jobject before_pointer = wrap_pointer(env, reinterpret_cast(before_realm), false); env->CallVoidMethod(static_cast(userdata), java_before_callback_function, before_pointer); bool result = jni_check_exception(env, true); + env->PopLocalFrame(NULL); return result; } @@ -1208,6 +1211,7 @@ after_client_reset(void* userdata, realm_t* before_realm, env->CallVoidMethod(static_cast(userdata), java_after_callback_function, before_pointer, after_pointer, did_recover); realm_close(after_realm_ptr); bool result = jni_check_exception(env, true); + env->PopLocalFrame(NULL); return result; } From 5f8e6c7e74c01841caf387b9812280efef7ecd7e Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 22 Dec 2023 13:58:28 +0100 Subject: [PATCH 09/15] PR change requests --- .../src/main/jni/realm_api_helpers.cpp | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) 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 b7de9cd9b7..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; @@ -152,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; } @@ -550,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*) { @@ -560,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) { @@ -1187,7 +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 = jni_check_exception(env, true); + bool result = jni_check_exception_for_callback(env); env->PopLocalFrame(NULL); return result; } @@ -1210,7 +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 = jni_check_exception(env, true); + bool result = jni_check_exception_for_callback(env); env->PopLocalFrame(NULL); return result; } From 55fb5cf88ac8baceffa9ecffe4cee4677fa6de83 Mon Sep 17 00:00:00 2001 From: Clemente Date: Fri, 22 Dec 2023 22:39:13 +0100 Subject: [PATCH 10/15] bump baas server --- dependencies.list | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From f1114e7b935a0e410c0cea098992a27d4307467b Mon Sep 17 00:00:00 2001 From: Clemente Date: Sat, 23 Dec 2023 00:04:12 +0100 Subject: [PATCH 11/15] Force document upload --- .../io/realm/kotlin/test/mongodb/common/AsymmetricSyncTests.kt | 3 +++ 1 file changed, 3 insertions(+) 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..313d5c6d58 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 @@ -133,6 +133,9 @@ class AsymmetricSyncTests { ) } } + + realm.syncSession.uploadAllLocalChanges() + verifyDocuments(clazz = "Measurement", expectedCount = newDocuments, initialCount = initialServerDocuments) } From 94a36a73b71b372ce3867328df1fb4a6101ddd7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Wed, 10 Jan 2024 10:49:05 +0100 Subject: [PATCH 12/15] Clean up server collection between AsymmetricSyncTests --- .../io/realm/kotlin/test/mongodb/util/AppAdmin.kt | 10 ++++++++++ .../kotlin/test/mongodb/util/AppServicesClient.kt | 2 +- .../kotlin/test/mongodb/common/AsymmetricSyncTests.kt | 5 ++++- 3 files changed, 15 insertions(+), 2 deletions(-) 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..e9f715a894 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 @@ -623,7 +623,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 313d5c6d58..0ce737f0f4 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 @@ -114,6 +114,9 @@ class AsymmetricSyncTests { @AfterTest fun tearDown() { realm.close() + runBlocking { + app.deleteDocuments(app.clientAppId, Measurement::class.simpleName!!, "{}") + } if (this::app.isInitialized) { app.close() } @@ -328,7 +331,7 @@ class AsymmetricSyncTests { delay(1.seconds) } } - assertTrue(found, "Number of documents was: $documents") + assertTrue(found, "Number of documents was: $documents [initialCount: $initialCount, expectedCount: $expectedCount]") } private fun useDynamicRealm(function: (DynamicMutableRealm) -> Unit) { From e7b43cf506c0ba9a2b7b820a0a6b464faf421653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Wed, 10 Jan 2024 12:57:28 +0100 Subject: [PATCH 13/15] Extended debugging information --- .../realm/kotlin/test/mongodb/util/AppServicesClient.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 e9f715a894..da0c7464b6 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 @@ -20,6 +20,7 @@ import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel +import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.delete @@ -222,7 +223,13 @@ class AppServicesClient( install(Logging) { // Set to LogLevel.ALL to debug Admin API requests. All relevant // data for each request/response will be console or LogCat. - level = LogLevel.INFO + level = LogLevel.ALL + logger = object : Logger { + override fun log(message: String) { + println("LOG: $message") + } + + } } } From 9ad3bae7e30165102ed6eacf2d76294056d01642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Thu, 11 Jan 2024 09:53:32 +0100 Subject: [PATCH 14/15] Another round for linting --- .../io/realm/kotlin/test/mongodb/util/AppServicesClient.kt | 1 - 1 file changed, 1 deletion(-) 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 da0c7464b6..581aceaa66 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 @@ -228,7 +228,6 @@ class AppServicesClient( override fun log(message: String) { println("LOG: $message") } - } } } From 9cd344b07e32cd0ba9a75faa3c1d9d4ba81b5ae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Fri, 12 Jan 2024 14:57:22 +0100 Subject: [PATCH 15/15] Fix document count verification --- .../kotlin/test/mongodb/util/AppServicesClient.kt | 9 +-------- .../kotlin/test/mongodb/common/AsymmetricSyncTests.kt | 11 +++++++---- 2 files changed, 8 insertions(+), 12 deletions(-) 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 581aceaa66..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 @@ -20,7 +20,6 @@ import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.logging.LogLevel -import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.delete @@ -48,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 @@ -223,12 +221,7 @@ class AppServicesClient( install(Logging) { // Set to LogLevel.ALL to debug Admin API requests. All relevant // data for each request/response will be console or LogCat. - level = LogLevel.ALL - logger = object : Logger { - override fun log(message: String) { - println("LOG: $message") - } - } + level = LogLevel.INFO } } 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 0ce737f0f4..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 @@ -317,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 [initialCount: $initialCount, expectedCount: $expectedCount]") + assertTrue(found, "Number of documents was: ${documents.value} [initialCount: $initialCount, expectedCount: $expectedCount]") } private fun useDynamicRealm(function: (DynamicMutableRealm) -> Unit) {