diff --git a/.github/workflows/include-static-analysis.yml b/.github/workflows/include-static-analysis.yml index 2232554864..60ec663580 100644 --- a/.github/workflows/include-static-analysis.yml +++ b/.github/workflows/include-static-analysis.yml @@ -35,6 +35,7 @@ jobs: run: ./gradlew ktlintCheck - name: Stash Ktlint results + if: always() run: | rm -rf /tmp/ktlint rm -rf /tmp/detekt @@ -50,6 +51,7 @@ jobs: - name: Publish Ktlint results uses: actions/upload-artifact@v4 + if: always() with: name: Ktlint Analyzer report path: /tmp/ktlint/* @@ -85,7 +87,8 @@ jobs: - name: Run Detekt run: ./gradlew detekt - - name: Stash Detekt results + - name: Stash Detekt results + if: always() run: | rm -rf /tmp/detekt mkdir /tmp/detekt @@ -99,6 +102,7 @@ jobs: - name: Publish Detekt results uses: actions/upload-artifact@v4 + if: always() with: name: Detekt Analyzer report path: /tmp/detekt/* diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 7e9c45a8e2..bf138c4591 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -122,7 +122,7 @@ jobs: key: jni-linux-lib-${{ needs.check-cache.outputs.packages-sha }} - name: Setup Java 11 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: ${{ vars.VERSION_JAVA_DISTRIBUTION }} java-version: ${{ vars.VERSION_JAVA }} @@ -431,11 +431,15 @@ jobs: run: |- echo "::add-matcher::.github/problem-matchers/kotlin.json" - - name: Setup Java 17 + - name: Setup Java uses: actions/setup-java@v4 with: distribution: ${{ vars.VERSION_JAVA_DISTRIBUTION }} - java-version: '17' + # JVM 17 is required for android-actions/setup-android@v3 + # Last version will be used and available globally. Other Java versions can be accessed through env variables with such specification as 'JAVA_HOME_{{ MAJOR_VERSION }}_{{ ARCHITECTURE }}' + java-version: | + 17 + ${{ vars.VERSION_JAVA }} - name: Setup Gradle and task/dependency caching uses: gradle/actions/setup-gradle@v3 @@ -473,10 +477,14 @@ jobs: echo '#!/bin/bash\nccache clang++ "$@"%"' > /usr/local/bin/ccache-clang++ - name: Setup Android SDK - uses: android-actions/setup-android@v2 - + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_ARM64 }} + uses: android-actions/setup-android@v3 + - name: Install NDK run: sdkmanager --install "ndk;${{ env.NDK_VERSION }}" + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_ARM64 }} # We cannot use artifacts as they cannot be shared between workflows, so use cache instead. - name: Setup build cache @@ -543,11 +551,15 @@ jobs: with: submodules: "recursive" - - name: Setup Java 11 + - name: Setup Java uses: actions/setup-java@v4 with: distribution: ${{ vars.VERSION_JAVA_DISTRIBUTION }} - java-version: ${{ vars.VERSION_JAVA }} + # JVM 17 is required for android-actions/setup-android@v3 + # Last version will be used and available globally. Other Java versions can be accessed through env variables with such specification as 'JAVA_HOME_{{ MAJOR_VERSION }}_{{ ARCHITECTURE }}' + java-version: | + 17 + ${{ vars.VERSION_JAVA }} - name: Setup Gradle and task/dependency caching uses: gradle/actions/setup-gradle@v3 @@ -602,9 +614,13 @@ jobs: echo '#!/bin/bash\nccache clang++ "$@"%"' > /usr/local/bin/ccache-clang++ - name: Setup Android SDK - uses: android-actions/setup-android@v2 - + uses: android-actions/setup-android@v3 + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} + - name: Install NDK + env: + JAVA_HOME: ${{ env.JAVA_HOME_17_X64 }} run: sdkmanager --install "ndk;${{ env.NDK_VERSION }}" - name: Build Android Base Test Apk diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c287e4c3..6e0ccd7e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,30 @@ * None. ### Enhancements +* Reduce the size of the local transaction log produced by creating objects, improving the performance of insertion-heavy transactions (Core issue [realm/realm-core#7734](https://github.com/realm/realm-core/pull/7734)). +* Performance has been improved for range queries on integers and timestamps. Requires that you use the "BETWEEN" operation in RQL or the Query::between() method when you build the query. (Core issue [realm/realm-core#7785](https://github.com/realm/realm-core/pull/7785)) +* [Sync] Report the originating error that caused a client reset to occur. (Core issue [realm/realm-core#6154](https://github.com/realm/realm-core/issues/6154)). +* [Sync] It is no longer an error to set a base url for an App with a trailing slash - for example, `https://services.cloud.mongodb.com/` instead of `https://services.cloud.mongodb.com` - before this change that would result in a 404 error from the server (Core issue [realm/realm-core#7791](https://github.com/realm/realm-core/pull/7791)). +* [Sync] On Windows devices Device Sync will additionally look up SSL certificates in the Windows Trusted Root Certification Authorities certificate store when establishing a connection. (Core issue [realm/realm-core#7882](https://github.com/realm/realm-core/pull/7882)) * [Sync] Add support for switching users with `App.switchUser(User)`. (Issue [#1813](https://github.com/realm/realm-kotlin/issues/1813)/[RKOTLIN-1115](https://jira.mongodb.org/browse/RKOTLIN-1115)). ### Fixed -* None. +* Comparing a numeric property with an argument list containing a string would throw. (Core issue [realm/realm-core#7714](https://github.com/realm/realm-core/issues/7714), since v2.0.0). +* After compacting, a file upgrade would be triggered. This could cause loss of data if schema mode is SoftResetFile (Core issue [realm/realm-core#7747](https://github.com/realm/realm-core/issues/7747), since v1.15.0). +* Encrypted files on Windows had a maximum size of 2GB even on x64 due to internal usage of `off_t`, which is a 32-bit type on 64-bit Windows (Core issue [realm/realm-core#7698](https://github.com/realm/realm-core/pull/7698)). +* The encryption code no longer behaves differently depending on the system page size, which should entirely eliminate a recurring source of bugs related to copying encrypted Realm files between platforms with different page sizes. One known outstanding bug was ([RNET-1141](https://github.com/realm/realm-dotnet/issues/3592)), where opening files on a system with a larger page size than the writing system would attempt to read sections of the file which had never been written to (Core issue [realm/realm-core#7698](https://github.com/realm/realm-core/pull/7698)). +* There were several complicated scenarios which could result in stale reads from encrypted files in multiprocess scenarios. These were very difficult to hit and would typically lead to a crash, either due to an assertion failure or DecryptionFailure being thrown (Core issue [realm/realm-core#7698](https://github.com/realm/realm-core/pull/7698), since v1.8.0). +* Encrypted files have some benign data races where we can memcpy a block of memory while another thread is writing to a limited range of it. It is logically impossible to ever read from that range when this happens, but Thread Sanitizer quite reasonably complains about this. We now perform a slower operations when running with TSan which avoids this benign race (Core issue [realm/realm-core#7698](https://github.com/realm/realm-core/pull/7698)). +* Tokenizing strings for full-text search could pass values outside the range [-1, 255] to `isspace()`, which is undefined behavior (Core issue [realm/realm-core#7698](https://github.com/realm/realm-core/pull/7698), since the introduction of FTS). +* Clearing a List of RealmAnys in an upgraded file would lead to an assertion failing (Core issue [realm/realm-core#7771](https://github.com/realm/realm-core/issues/7771), since v1.15.0) +* You could get unexpected merge results when assigning to a nested collection (Core issue [realm/realm-core#7809](https://github.com/realm/realm-core/issues/7809), since v1.15.0) +* Fixed removing backlinks from the wrong objects if the link came from a nested list, nested dictionary, top-level dictionary, or list of mixed, and the source table had more than 256 objects. This could manifest as `array_backlink.cpp:112: Assertion failed: int64_t(value >> 1) == key.value` when removing an object. (Core issue [realm/realm-core#7594](https://github.com/realm/realm-core/issues/7594), since Core v11 for dictionaries) +* Fixed the collapse/rejoin of clusters which contained nested collections with links. This could manifest as `array.cpp:319: Array::move() Assertion failed: begin <= end [2, 1]` when removing an object. (Core issue [realm/realm-core#7839](https://github.com/realm/realm-core/issues/7839), since the introduction of nested collections in v1.15.0) +* [Sync] Platform networking was not enabled even if setting `AppConfiguration.Builder.usePlatformNetworking`. (Issue [#1811](https://github.com/realm/realm-kotlin/issues/1811)/[RKOTLIN-1114](https://jira.mongodb.org/browse/RKOTLIN-1114)). +* [Sync] Fix some client resets (such as migrating to flexible sync) potentially failing with AutoClientResetFailed if a new client reset condition (such as rolling back a flexible sync migration) occurred before the first one completed. (Core issue [realm/realm-core#7542](https://github.com/realm/realm-core/pull/7542), since v1.9.0) +* [Sync] Fixed a change of mode from Strong to All when removing links from an embedded object that links to a tombstone. This affects sync apps that use embedded objects which have a `Lst` that contains a link to another top level object which has been deleted by another sync client (creating a tombstone locally). In this particular case, the switch would cause any remaining link removals to recursively delete the destination object if there were no other links to it. (Core issue [realm/realm-core#7828](https://github.com/realm/realm-core/issues/7828), since v1.15.0) +* [Sync] `SyncSession.uploadAllLocalChanges` was inconsistent in how it handled commits which did not produce any changesets to upload. Previously it would sometimes complete immediately if all commits waiting to be uploaded were empty, and at other times it would wait for a server roundtrip. It will now always complete immediately. (Core issue [realm/realm-core#7796](https://github.com/realm/realm-core/pull/7796)). +* [Sync] Sync client can crash if a session is resumed while the session is being suspended. (Core issue [realm/realm-core#7860](https://github.com/realm/realm-core/issues/7860), since v1.0.0) ### Compatibility * File format: Generates Realms with file format v24 (reads and upgrades file format v10 or later). @@ -25,7 +45,7 @@ * Minimum R8: 8.0.34. ### Internal -* None. +* Updated to Realm Core 14.10.4 commit 4f83c590c4340dd7760d5f070e2e81613eb536aa. ## 2.1.1-SNAPSHOT (YYYY-MM-DD) diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index de839b4a52..2595d08e98 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -519,7 +519,6 @@ expect object RealmInterop { // App fun realm_app_get( appConfig: RealmAppConfigurationPointer, - syncClientConfig: RealmSyncClientConfigurationPointer, basePath: String, ): RealmAppPointer fun realm_app_get_current_user(app: RealmAppPointer): RealmUserPointer? @@ -600,8 +599,7 @@ expect object RealmInterop { fun realm_user_refresh_custom_data(app: RealmAppPointer, user: RealmUserPointer, callback: AppCallback) // Sync client config - fun realm_sync_client_config_new(): RealmSyncClientConfigurationPointer - + fun realm_app_config_get_sync_client_config(configPointer: RealmAppConfigurationPointer): RealmSyncClientConfigurationPointer fun realm_sync_client_config_set_default_binding_thread_observer( syncClientConfig: RealmSyncClientConfigurationPointer, appId: String @@ -652,6 +650,8 @@ expect object RealmInterop { user: RealmUserPointer, partition: String ): RealmSyncConfigurationPointer + // Flexible Sync + fun realm_flx_sync_config_new(user: RealmUserPointer): RealmSyncConfigurationPointer fun realm_sync_config_set_error_handler( syncConfig: RealmSyncConfigurationPointer, errorHandler: SyncErrorCallback @@ -789,9 +789,6 @@ expect object RealmInterop { syncConfiguration: RealmSyncConfigurationPointer ) - // Flexible Sync - fun realm_flx_sync_config_new(user: RealmUserPointer): RealmSyncConfigurationPointer - // Flexible Sync Subscription fun realm_sync_subscription_id(subscription: RealmSubscriptionPointer): ObjectId fun realm_sync_subscription_name(subscription: RealmSubscriptionPointer): String? diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt index e5b291899e..849e75f94d 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmInterop.kt @@ -1154,7 +1154,6 @@ actual object RealmInterop { actual fun realm_app_get( appConfig: RealmAppConfigurationPointer, - syncClientConfig: RealmSyncClientConfigurationPointer, basePath: String ): RealmAppPointer { return LongPointerWrapper(realmc.realm_app_create(appConfig.cptr()), managed = true) @@ -1314,8 +1313,9 @@ actual object RealmInterop { ) } - actual fun realm_sync_client_config_new(): RealmSyncClientConfigurationPointer { - return LongPointerWrapper(realmc.realm_sync_client_config_new()) + actual fun realm_app_config_get_sync_client_config(configPointer: RealmAppConfigurationPointer): RealmSyncClientConfigurationPointer { + // The configuration is owned by Core so don't track and release it through garbage collection of the NativePointer + return LongPointerWrapper(realmc.realm_app_config_get_sync_client_config(configPointer.cptr()), false) } actual fun realm_sync_client_config_set_default_binding_thread_observer(syncClientConfig: RealmSyncClientConfigurationPointer, appId: String) { @@ -1762,6 +1762,10 @@ actual object RealmInterop { } } + actual fun realm_flx_sync_config_new(user: RealmUserPointer): RealmSyncConfigurationPointer { + return LongPointerWrapper(realmc.realm_flx_sync_config_new(user.cptr())) + } + actual fun realm_config_set_sync_config(realmConfiguration: RealmConfigurationPointer, syncConfiguration: RealmSyncConfigurationPointer) { realmc.realm_config_set_sync_config(realmConfiguration.cptr(), syncConfiguration.cptr()) } @@ -1984,10 +1988,6 @@ actual object RealmInterop { realmc.realm_object_delete(obj.cptr()) } - actual fun realm_flx_sync_config_new(user: RealmUserPointer): RealmSyncConfigurationPointer { - return LongPointerWrapper(realmc.realm_flx_sync_config_new(user.cptr())) - } - actual fun realm_sync_subscription_id(subscription: RealmSubscriptionPointer): ObjectId { val nativeBytes: ShortArray = realmc.realm_sync_subscription_id(subscription.cptr()).bytes val byteArray = ByteArray(nativeBytes.size) 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 323a82962a..f39b2e8772 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 @@ -2062,7 +2062,6 @@ actual object RealmInterop { actual fun realm_app_get( appConfig: RealmAppConfigurationPointer, - syncClientConfig: RealmSyncClientConfigurationPointer, basePath: String ): RealmAppPointer { return CPointerWrapper(realm_wrapper.realm_app_create(appConfig.cptr()), managed = true) @@ -2469,8 +2468,9 @@ actual object RealmInterop { ) } - actual fun realm_sync_client_config_new(): RealmSyncClientConfigurationPointer { - return CPointerWrapper(realm_wrapper.realm_sync_client_config_new()) + actual fun realm_app_config_get_sync_client_config(configPointer: RealmAppConfigurationPointer): RealmSyncClientConfigurationPointer { + // The configuration is owned by Core so don't track and release it through garbage collection of the NativePointer + return CPointerWrapper(realm_wrapper.realm_app_config_get_sync_client_config(configPointer.cptr()), false) } actual fun realm_sync_client_config_set_default_binding_thread_observer( @@ -3321,6 +3321,10 @@ actual object RealmInterop { } } + actual fun realm_flx_sync_config_new(user: RealmUserPointer): RealmSyncConfigurationPointer { + return CPointerWrapper(realm_wrapper.realm_flx_sync_config_new((user.cptr()))) + } + actual fun realm_app_sync_client_reconnect(app: RealmAppPointer) { realm_wrapper.realm_app_sync_client_reconnect(app.cptr()) } @@ -3336,10 +3340,6 @@ actual object RealmInterop { realm_wrapper.realm_config_set_sync_config(realmConfiguration.cptr(), syncConfiguration.cptr()) } - actual fun realm_flx_sync_config_new(user: RealmUserPointer): RealmSyncConfigurationPointer { - return CPointerWrapper(realm_wrapper.realm_flx_sync_config_new((user.cptr()))) - } - actual fun realm_sync_subscription_id(subscription: RealmSubscriptionPointer): ObjectId { return ObjectId(realm_wrapper.realm_sync_subscription_id(subscription.cptr()).getBytes()) } diff --git a/packages/external/core b/packages/external/core index c280bdb175..1f0378ae53 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit c280bdb17522323d5c30dc32a2b9efc9dc80ca3b +Subproject commit 1f0378ae53f73d67a309c9499aec512f4cde53f1 diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 96593c782c..3e8d2af436 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -533,8 +533,6 @@ $result = SWIG_JavaArrayOutLonglong(jenv, (long long *)result, 2); %ignore "realm_dictionary_add_notification_callback"; %ignore "realm_results_add_notification_callback"; -%ignore "realm_app_config_get_sync_client_config"; - // Swig doesn't understand __attribute__ so eliminate it #define __attribute__(x) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt index 39d88e2e12..5350877d65 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/exceptions/SyncExceptions.kt @@ -68,6 +68,7 @@ public open class UnrecoverableSyncException internal constructor(message: Strin * Thrown when the type of sync used by the server does not match the one used by the client, i.e. * the server and client disagrees whether to use Partition-based or Flexible Sync. */ +@Suppress("DEPRECATION") public class WrongSyncTypeException internal constructor(message: String) : UnrecoverableSyncException(message) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt index 1f7a7bdb21..fe6ce4d9a2 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/AppConfigurationImpl.kt @@ -89,7 +89,9 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) applicationInfo = info.toString() } val sdkInfo = "RealmKotlin/$SDK_VERSION" + val synClientConfig: RealmSyncClientConfigurationPointer = initializeSyncClientConfig( + appConfigPointer, websocketTransport, sdkInfo, applicationInfo.toString() @@ -101,7 +103,6 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) websocketTransport, RealmInterop.realm_app_get( appConfigPointer, - synClientConfig, appFilesDirectory() ) ) @@ -158,11 +159,12 @@ public class AppConfigurationImpl @OptIn(ExperimentalKBsonSerializerApi::class) } private fun initializeSyncClientConfig( + appConfigPointer: RealmAppConfigurationPointer, webSocketTransport: WebSocketTransport?, sdkInfo: String?, applicationInfo: String? ): RealmSyncClientConfigurationPointer = - RealmInterop.realm_sync_client_config_new() + RealmInterop.realm_app_config_get_sync_client_config(appConfigPointer) .also { syncClientConfig -> // Initialize client configuration first RealmInterop.realm_sync_client_config_set_default_binding_thread_observer(syncClientConfig, appId) diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt index 46fbc13e17..a6f015b9fb 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/RealmSyncUtils.kt @@ -17,7 +17,6 @@ import io.realm.kotlin.mongodb.exceptions.FunctionExecutionException import io.realm.kotlin.mongodb.exceptions.InvalidCredentialsException import io.realm.kotlin.mongodb.exceptions.ServiceException import io.realm.kotlin.mongodb.exceptions.SyncException -import io.realm.kotlin.mongodb.exceptions.UnrecoverableSyncException import io.realm.kotlin.mongodb.exceptions.UserAlreadyConfirmedException import io.realm.kotlin.mongodb.exceptions.UserAlreadyExistsException import io.realm.kotlin.mongodb.exceptions.UserNotFoundException @@ -91,19 +90,26 @@ internal fun convertSyncError(syncError: SyncError): SyncException { syncError.compensatingWrites, syncError.isFatal ) + ErrorCode.RLM_ERR_SYNC_PROTOCOL_INVARIANT_FAILED, ErrorCode.RLM_ERR_SYNC_PROTOCOL_NEGOTIATION_FAILED, - ErrorCode.RLM_ERR_SYNC_PERMISSION_DENIED -> { + ErrorCode.RLM_ERR_SYNC_PERMISSION_DENIED, + -> { // Permission denied errors should be unrecoverable according to Core, i.e. the // client will disconnect sync and transition to the "inactive" state - UnrecoverableSyncException(message) + @Suppress("DEPRECATION") io.realm.kotlin.mongodb.exceptions.UnrecoverableSyncException( + message + ) } + else -> { // An error happened we are not sure how to handle. Just report as a generic // SyncException. when (syncError.isFatal) { false -> SyncException(message, syncError.isFatal) - true -> UnrecoverableSyncException(message) + true -> @Suppress("DEPRECATION") io.realm.kotlin.mongodb.exceptions.UnrecoverableSyncException( + message + ) } } } diff --git a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt index 9fcfdc8a1b..c7dd207ae4 100644 --- a/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt +++ b/packages/library-sync/src/commonMain/kotlin/io/realm/kotlin/mongodb/internal/SyncSessionImpl.kt @@ -168,7 +168,7 @@ internal open class SyncSessionImpl( nativePointer, error, message, - true + false ) } diff --git a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt index 0d0b52a402..b6e49173fc 100644 --- a/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt +++ b/packages/library-sync/src/jvm/kotlin/io/realm/kotlin/mongodb/internal/OkHttpWebsocketClient.kt @@ -75,6 +75,7 @@ public class OkHttpWebsocketClient( scope.launch { okHttpClient.newWebSocket(request, this@OkHttpWebsocketClient) } + logger.debug("init") } override fun onOpen(webSocket: WebSocket, response: Response) { diff --git a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index cfc7d1f33f..706f9815c4 100644 --- a/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/androidMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -56,6 +56,10 @@ actual object PlatformUtils { } SystemClock.sleep(5000) // 5 seconds to give the GC some time to process } + + actual fun copyFile(originPath: String, targetPath: String) { + File(originPath).copyTo(File(targetPath)) + } } // Allocs as much garbage as we can. Pass maxSize = 0 to use all available memory in the process. diff --git a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index ee6a77661d..60b595f793 100644 --- a/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/commonMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -7,6 +7,7 @@ import kotlin.time.Duration expect object PlatformUtils { fun createTempDir(prefix: String = Utils.createRandomString(16), readOnly: Boolean = false): String fun deleteTempDir(path: String) + fun copyFile(originPath: String, targetPath: String) fun sleep(duration: Duration) fun threadId(): ULong fun triggerGC() diff --git a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index 498ba62df6..c9708e5148 100644 --- a/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/jvmMain/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -16,6 +16,7 @@ package io.realm.kotlin.test.platform +import java.io.File import java.nio.file.Files import java.nio.file.Path import java.nio.file.Paths @@ -41,6 +42,10 @@ actual object PlatformUtils { return dir.absolutePathString() } + actual fun copyFile(originPath: String, targetPath: String) { + File(originPath).copyTo(File(targetPath)) + } + actual fun deleteTempDir(path: String) { val rootPath: Path = Paths.get(path) val pathsToDelete: List = diff --git a/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt b/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt index d92ec1d549..324734b843 100644 --- a/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt +++ b/packages/test-base/src/nativeDarwin/kotlin/io/realm/kotlin/test/platform/PlatformUtils.kt @@ -72,4 +72,8 @@ actual object PlatformUtils { actual fun triggerGC() { GC.collect() } + + actual fun copyFile(originPath: String, targetPath: String) { + platform.Foundation.NSFileManager.defaultManager.copyItemAtPath(originPath, targetPath, null) + } } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt index 04c23304a5..9c47e77df8 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/AppTests.kt @@ -424,12 +424,20 @@ class AppTests { // Create a configuration pointing to the metadata Realm for that app val metadataDir = "${app.configuration.syncRootDirectory}/mongodb-realm/${app.configuration.appId}/server-utility/metadata/" + + // Workaround for https://github.com/realm/realm-core/issues/7876 + // We cannot validate if the test app metadata realm is encrypted directly, as it is cached + // and subsequent access wont validate the encryption key. Copying the Realm allows to bypass + // the cache. + PlatformUtils.copyFile(metadataDir + "sync_metadata.realm", metadataDir + "copy_sync_metadata.realm") + val wrongKey = TestHelper.getRandomKey() val config = RealmConfiguration .Builder(setOf()) - .name("sync_metadata.realm") + .name("copy_sync_metadata.realm") .directory(metadataDir) .encryptionKey(wrongKey) + .schemaVersion(7) .build() assertTrue(fileExists(config.path)) @@ -464,10 +472,18 @@ class AppTests { // Create a configuration pointing to the metadata Realm for that app val metadataDir = "${app.configuration.syncRootDirectory}/mongodb-realm/${app.configuration.appId}/server-utility/metadata/" + + // Workaround for https://github.com/realm/realm-core/issues/7876 + // We cannot validate if the test app metadata realm is encrypted directly, as it is cached + // and subsequent access wont validate the encryption key. Copying the Realm allows to bypass + // the cache. + PlatformUtils.copyFile(metadataDir + "sync_metadata.realm", metadataDir + "copy_sync_metadata.realm") + val config = RealmConfiguration .Builder(setOf()) - .name("sync_metadata.realm") + .name("copy_sync_metadata.realm") .directory(metadataDir) + .schemaVersion(7) .build() assertTrue(fileExists(config.path)) diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FLXProgressListenerTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FLXProgressListenerTests.kt index 9c67b39196..1ff2992425 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FLXProgressListenerTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/FLXProgressListenerTests.kt @@ -148,16 +148,19 @@ class FLXProgressListenerTests { fun uploadProgressListener_changesOnly() = runBlocking { Realm.open(createSyncConfig(app.createUserAndLogin())).use { realm -> for (i in 0..3) { - realm.writeSampleData(TEST_SIZE, timeout = TIMEOUT) - realm.syncSession.progressAsFlow(Direction.UPLOAD, ProgressMode.CURRENT_CHANGES) - .run { - withTimeout(TIMEOUT) { + val task = async { + realm.syncSession.progressAsFlow(Direction.UPLOAD, ProgressMode.CURRENT_CHANGES) + .run { last().let { assertTrue(it.isTransferComplete) assertEquals(1.0, it.estimate) } } - } + } + realm.writeSampleData(TEST_SIZE, timeout = TIMEOUT) + withTimeout(TIMEOUT) { + task.await() + } } } } @@ -239,7 +242,7 @@ class FLXProgressListenerTests { try { val flow = realm.syncSession.progressAsFlow(Direction.UPLOAD, ProgressMode.INDEFINITELY) val job = async { - withTimeout(10.seconds) { + withTimeout(30.seconds) { flow.collect { channel.trySend(true) } 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 66a26acc6b..4dc2108318 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 @@ -65,6 +65,7 @@ import kotlin.reflect.KClass import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test +import kotlin.test.assertContains import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertIs @@ -710,10 +711,8 @@ class SyncClientResetIntegrationTests { exception: ClientResetRequiredException ) { // Notify that this callback has been invoked - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertContains(exception.message!!, "User-provided callback failed") + assertIs(exception.cause) assertEquals( "User exception", @@ -788,10 +787,8 @@ class SyncClientResetIntegrationTests { exception: ClientResetRequiredException ) { // Notify that this callback has been invoked - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertContains(exception.message!!, "User-provided callback failed") + channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } }).build() @@ -1080,7 +1077,7 @@ class SyncClientResetIntegrationTests { assertNotNull(exception.originalFilePath) assertFalse(fileExists(exception.recoveryFilePath)) assertTrue(fileExists(exception.originalFilePath)) - assertTrue(exception.message!!.contains("Simulate Client Reset")) + assertContains(exception.message!!, "Simulate Client Reset") } } channel.close() @@ -1123,10 +1120,8 @@ class SyncClientResetIntegrationTests { exception: ClientResetRequiredException ) { // Notify that this callback has been invoked - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertContains(exception.message!!, "User-provided callback failed") + channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } }).build() @@ -1193,11 +1188,7 @@ class SyncClientResetIntegrationTests { // Validate that files have been moved after explicit reset assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) - - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertContains(exception.message!!, "User-provided callback failed") channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } @@ -1400,10 +1391,7 @@ class SyncClientResetIntegrationTests { assertFalse(fileExists(originalFilePath)) assertTrue(fileExists(recoveryFilePath)) - assertEquals( - "[Sync][AutoClientResetFailed(1028)] A fatal error occurred during client reset: 'User-provided callback failed'.", - exception.message - ) + assertTrue(exception.message!!.contains("User-provided callback failed")) channel.trySendOrFail(ClientResetEvents.ON_MANUAL_RESET_FALLBACK) } diff --git a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt index b5bf6141a9..4d30150a34 100644 --- a/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt +++ b/packages/test-sync/src/commonTest/kotlin/io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt @@ -350,14 +350,14 @@ class SyncedRealmTests { syncSession = (realm.syncSession as SyncSessionImpl).nativePointer, error = ErrorCode.RLM_ERR_ACCOUNT_NAME_IN_USE, errorMessage = "Non fatal error", - isFatal = true, // flipped https://jira.mongodb.org/browse/RCORE-2146 + isFatal = false, ) RealmInterop.realm_sync_session_handle_error_for_testing( syncSession = (realm.syncSession as SyncSessionImpl).nativePointer, error = ErrorCode.RLM_ERR_INTERNAL_SERVER_ERROR, errorMessage = "Fatal error", - isFatal = false, // flipped https://jira.mongodb.org/browse/RCORE-2146 + isFatal = true, ) } } @@ -1650,14 +1650,17 @@ class SyncedRealmTests { println("Partition based sync bundled realm is in ${config2.path}") } - // This test cannot run multiple times on the same server instance as the primary - // key of the objects from asset-pbs.realm will not be unique on secondary runs. @Test fun initialRealm_partitionBasedSync() { val (email, password) = randomEmail() to "password1234" val user = runBlocking { app.createUserAndLogIn(email, password) } + + runBlocking { + app.asTestApp.deleteDocuments(app.configuration.appId, "ParentPk", "{}") + } + val config1 = createPartitionSyncConfig( user = user, partitionValue = partitionValue, name = "db1", errorHandler = object : SyncSession.ErrorHandler { diff --git a/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/PlatformNetworkingTests.kt b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/PlatformNetworkingTests.kt new file mode 100644 index 0000000000..b61ddeaa1d --- /dev/null +++ b/packages/test-sync/src/jvmTest/kotlin/io/realm/kotlin/test/mongodb/jvm/PlatformNetworkingTests.kt @@ -0,0 +1,92 @@ +package io.realm.kotlin.test.mongodb.jvm + +import io.realm.kotlin.Realm +import io.realm.kotlin.entities.sync.SyncObjectWithAllTypes +import io.realm.kotlin.ext.query +import io.realm.kotlin.internal.platform.runBlocking +import io.realm.kotlin.log.LogCategory +import io.realm.kotlin.log.LogLevel +import io.realm.kotlin.log.RealmLog +import io.realm.kotlin.mongodb.User +import io.realm.kotlin.mongodb.sync.SyncConfiguration +import io.realm.kotlin.mongodb.syncSession +import io.realm.kotlin.test.mongodb.TestApp +import io.realm.kotlin.test.mongodb.common.FLEXIBLE_SYNC_SCHEMA +import io.realm.kotlin.test.mongodb.common.utils.CustomLogCollector +import io.realm.kotlin.test.mongodb.createUserAndLogIn +import io.realm.kotlin.test.mongodb.use +import io.realm.kotlin.test.mongodb.util.DefaultFlexibleSyncAppInitializer +import io.realm.kotlin.test.util.use +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds + +class PlatformNetworkingTests { + + private val TIMEOUT = 10.seconds + + @Test + fun syncRoundTrip_coreNetworking() = runBlocking { + roundTrip(platformNetworking = false) + } + + @Test + fun syncRoundTrip_platformNetworking() = runBlocking { + roundTrip(platformNetworking = true) + } + + private suspend fun roundTrip(platformNetworking: Boolean) { + TestApp(this::class.simpleName, DefaultFlexibleSyncAppInitializer, builder = { + it.usePlatformNetworking(platformNetworking) + }).use { app -> + val selector = org.mongodb.kbson.ObjectId().toString() + + // Setup logger to capture WebSocketClient log messages + val logger = CustomLogCollector() + RealmLog.add(logger) + RealmLog.setLevel(LogLevel.DEBUG, LogCategory.Realm.Sdk) + + Realm.open(createSyncConfig(app.createUserAndLogIn(), selector)) + .use { uploadRealm -> + Realm.open(createSyncConfig(app.createUserAndLogIn(), selector)) + .use { realm -> + uploadRealm.write { + copyToRealm( + SyncObjectWithAllTypes().apply { + stringField = selector + } + ) + } + uploadRealm.syncSession.uploadAllLocalChanges(TIMEOUT) + withTimeout(TIMEOUT) { + realm.query().asFlow().first { + it.list.size == 1 + }.list.first().also { + assertEquals(selector, it.stringField) + } + } + } + } + assertTrue( + if (platformNetworking) { + logger.logs.any { it.contains("\\[Websocket.*\\] onOpen".toRegex()) } + } else { + logger.logs.none { it.contains("\\[Websocket.*\\] onOpen".toRegex()) } + }, + "Failed to verify log statements for : platformNetworking=$platformNetworking" + ) + } + } + + private fun createSyncConfig( + user: User, + selector: String + ): SyncConfiguration { + return SyncConfiguration.Builder(user, FLEXIBLE_SYNC_SCHEMA).initialSubscriptions { + add(it.query("stringField = $0", selector)) + }.build() + } +}