From 24b19c275cabb7f6c123231c508fcef63b52f444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claus=20R=C3=B8rbech?= Date: Fri, 8 Sep 2023 14:36:09 +0200 Subject: [PATCH 01/14] Fix test around releasing intermediate versions (#1509) --- .../kotlin/io/realm/kotlin/test/android/MemoryTests.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/test-base/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/android/MemoryTests.kt b/packages/test-base/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/android/MemoryTests.kt index 2861a1e9f4..4caaaeabd8 100644 --- a/packages/test-base/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/android/MemoryTests.kt +++ b/packages/test-base/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/android/MemoryTests.kt @@ -149,14 +149,15 @@ class MemoryTests { // Perform various writes and deletes and garbage collect the references to allow core to // release the underlying versions + val referenceHolder = mutableListOf() for (i in 1..3) { - val referenceHolder = mutableListOf() - realm.writeBlocking { - for (i in 1..10) { + for (i in 1..10) { + val y: MemoryTest = realm.writeBlocking { copyToRealm(MemoryTest()).apply { stringField = oneMBstring - }.also { referenceHolder.add(it) } + } } + referenceHolder.add(y) } realm.writeBlocking { delete(query("stringField != 'INITIAL'")) From b3406e34ed2471a3b98c82cee1ab1f575dccb236 Mon Sep 17 00:00:00 2001 From: clementetb Date: Thu, 14 Sep 2023 22:01:26 +0200 Subject: [PATCH 02/14] Update core to include `numberOfActiveVersions` fix (#1516) --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ packages/external/core | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3503f7247..7621882b9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 1.11.2 (YYYY-MM-DD) + +### Enhancements +* None. + +### Fixed +* `Realm.getNumberOfActiveVersions` now returns the actual number of active versions. (Core issue [#6960](https://github.com/realm/realm-core/pull/6960)) + +### Compatibility +* File format: Generates Realms with file format v23. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* This release is compatible with the following Kotlin releases: + * Kotlin 1.8.0 and above. The K2 compiler is not supported yet. + * Ktor 2.1.2 and above. + * Coroutines 1.7.0 and above. + * AtomicFu 0.18.3 and above. + * The new memory model only. See https://github.com/realm/realm-kotlin#kotlin-memory-model-and-coroutine-compatibility +* Minimum Kbson 0.3.0. +* Minimum Gradle version: 6.8.3. +* Minimum Android Gradle Plugin version: 4.1.3. +* Minimum Android SDK: 16. + +### Internal +* Updated to Realm Core 13.20.0, commit a3717e492ec606e42995169cf706a82fc1cd0698. + + ## 1.11.1 (2023-09-07) ### Enhancements diff --git a/packages/external/core b/packages/external/core index c258e2681b..a3717e492e 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit c258e2681bca5fb33bbd23c112493817b43bfa86 +Subproject commit a3717e492ec606e42995169cf706a82fc1cd0698 From 330a06ca691fc8392f441cf9c746e5d5205e1d63 Mon Sep 17 00:00:00 2001 From: clementetb Date: Fri, 6 Oct 2023 09:59:53 +0200 Subject: [PATCH 03/14] Fix JVM memory leaks (#1526) --- CHANGELOG.md | 1 + .../kotlin/internal/interop/MemAllocator.kt | 12 +-- .../kotlin/internal/interop/RealmInterop.kt | 85 ++++++++++++++----- .../internal/interop/RealmValueAllocator.kt | 66 ++++++++------ .../src/main/jni/realm_api_helpers.cpp | 24 ++++-- .../src/main/jni/realm_api_helpers.h | 6 ++ 6 files changed, 137 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7621882b9c..fb960338d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Fixed * `Realm.getNumberOfActiveVersions` now returns the actual number of active versions. (Core issue [#6960](https://github.com/realm/realm-core/pull/6960)) +* Fix memory leaks on the JVM platform, see PR for more information. (Issue [#1526](https://github.com/realm/realm-kotlin/pull/1526)) ### Compatibility * File format: Generates Realms with file format v23. diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/MemAllocator.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/MemAllocator.kt index 52f1d89d71..7825ea7f30 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/MemAllocator.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/MemAllocator.kt @@ -93,12 +93,6 @@ interface MemAllocator { * Instantiates a [RealmValue] representing a `realm_value_t` of type `RLM_TYPE_LINK`. */ fun realmObjectTransport(value: RealmObjectInterop?): RealmValue - - /** - * Instantiates a [RealmQueryArgumentList] representing a `realm_query_arg_t` that describe and - * references the incoming [RealmValueList] arguments. - */ - fun queryArgsOf(queryArgs: List): RealmQueryArgumentList } /** @@ -118,6 +112,12 @@ interface MemTrackingAllocator : MemAllocator { */ fun byteArrayTransport(value: ByteArray?): RealmValue + /** + * Instantiates a [RealmQueryArgumentList] representing a `realm_query_arg_t` that describe and + * references the incoming [RealmValueList] arguments. + */ + fun queryArgsOf(queryArgs: List): RealmQueryArgumentList + /** * Frees resources linked to this allocator. See implementations for more details. */ 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 c2b2a07d8f..14674de642 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 @@ -17,6 +17,7 @@ package io.realm.kotlin.internal.interop import io.realm.kotlin.internal.interop.Constants.ENCRYPTION_KEY_LENGTH +import io.realm.kotlin.internal.interop.RealmInterop.cptr import io.realm.kotlin.internal.interop.sync.ApiKeyWrapper import io.realm.kotlin.internal.interop.sync.AuthProvider import io.realm.kotlin.internal.interop.sync.CoreConnectionState @@ -117,7 +118,35 @@ actual object RealmInterop { realmc.classArray_setitem(cclasses, i, cclass) realmc.propertyArrayArray_setitem(cproperties, i, classProperties) } - return LongPointerWrapper(realmc.realm_schema_new(cclasses, count.toLong(), cproperties)) + try { + return LongPointerWrapper(realmc.realm_schema_new(cclasses, count.toLong(), cproperties)) + } finally { + // Clean up classes + for (classIndex in 0 until count) { + val classInfo = realmc.classArray_getitem(cclasses, classIndex) + + // Clean properties + val propertyArray = realmc.propertyArrayArray_getitem(cproperties, classIndex) + + val propertyCount = classInfo.getNum_properties() + classInfo.getNum_computed_properties() + for (propertyIndex in 0 until propertyCount) { + val property = realmc.propertyArray_getitem(propertyArray, propertyIndex.toInt()) + + realmc.realm_property_info_t_cleanup(property) + property.delete() + } + + realmc.delete_propertyArray(propertyArray) + + // end clean properties + + realmc.realm_class_info_t_cleanup(classInfo) + classInfo.delete() + } + + realmc.delete_propertyArrayArray(cproperties) + realmc.delete_classArray(cclasses) + } } actual fun realm_config_new(): RealmConfigurationPointer { @@ -316,23 +345,27 @@ actual object RealmInterop { val properties = realmc.new_propertyArray(max.toInt()) val outCount = longArrayOf(0) realmc.realm_get_class_properties(realm.cptr(), classKey.key, properties, max, outCount) - return if (outCount[0] > 0) { - (0 until outCount[0]).map { i -> - with(realmc.propertyArray_getitem(properties, i.toInt())) { - PropertyInfo( - name, - public_name, - PropertyType.from(type), - CollectionType.from(collection_type), - link_target, - link_origin_property_name, - PropertyKey(key), - flags - ) + try { + return if (outCount[0] > 0) { + (0 until outCount[0]).map { i -> + with(realmc.propertyArray_getitem(properties, i.toInt())) { + PropertyInfo( + name, + public_name, + PropertyType.from(type), + CollectionType.from(collection_type), + link_target, + link_origin_property_name, + PropertyKey(key), + flags + ) + } } + } else { + emptyList() } - } else { - emptyList() + } finally { + realmc.delete_propertyArray(properties) } } @@ -922,6 +955,8 @@ actual object RealmInterop { builder.initIndicesArray(builder::modificationIndices, modificationIndices) builder.initIndicesArray(builder::modificationIndicesAfter, modificationIndicesAfter) builder.movesCount = movesCount[0].toInt() + + realmc.delete_collectionMoveArray(moves) } actual fun realm_collection_changes_get_ranges( @@ -969,6 +1004,12 @@ actual object RealmInterop { builder.initRangesArray(builder::insertionRanges, insertionRanges, insertRangesCount[0]) builder.initRangesArray(builder::modificationRanges, modificationRanges, modificationRangesCount[0]) builder.initRangesArray(builder::modificationRangesAfter, modificationRangesAfter, modificationRangesCount[0]) + + realmc.delete_indexRangeArray(insertionRanges) + realmc.delete_indexRangeArray(modificationRanges) + realmc.delete_indexRangeArray(modificationRangesAfter) + realmc.delete_indexRangeArray(deletionRanges) + realmc.delete_collectionMoveArray(moves) } actual fun realm_dictionary_get_changes( @@ -1107,6 +1148,8 @@ actual object RealmInterop { } } else { emptyList() + }.also { + realmc.delete_identityArray(keys) } } @@ -1609,7 +1652,7 @@ actual object RealmInterop { realm: RealmPointer, classKey: ClassKey, query: String, - args: RealmQueryArgumentList + args: RealmQueryArgumentList, ): RealmQueryPointer { return LongPointerWrapper( realmc.realm_query_parse( @@ -1625,7 +1668,7 @@ actual object RealmInterop { actual fun realm_query_parse_for_results( results: RealmResultsPointer, query: String, - args: RealmQueryArgumentList + args: RealmQueryArgumentList, ): RealmQueryPointer { return LongPointerWrapper( realmc.realm_query_parse_for_results( @@ -1640,7 +1683,7 @@ actual object RealmInterop { actual fun realm_query_parse_for_list( list: RealmListPointer, query: String, - args: RealmQueryArgumentList + args: RealmQueryArgumentList, ): RealmQueryPointer { return LongPointerWrapper( realmc.realm_query_parse_for_list( @@ -1655,7 +1698,7 @@ actual object RealmInterop { actual fun realm_query_parse_for_set( set: RealmSetPointer, query: String, - args: RealmQueryArgumentList + args: RealmQueryArgumentList, ): RealmQueryPointer { return LongPointerWrapper( realmc.realm_query_parse_for_set( @@ -1693,7 +1736,7 @@ actual object RealmInterop { actual fun realm_query_append_query( query: RealmQueryPointer, filter: String, - args: RealmQueryArgumentList + args: RealmQueryArgumentList, ): RealmQueryPointer { return LongPointerWrapper( realmc.realm_query_append_query(query.cptr(), filter, args.size, args.head) diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmValueAllocator.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmValueAllocator.kt index 9999c7dfee..72331fd1db 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmValueAllocator.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/RealmValueAllocator.kt @@ -93,28 +93,6 @@ object JvmMemAllocator : MemAllocator { link = realmc.realm_object_as_link(it.objectPointer.cptr()) } - override fun queryArgsOf(queryArgs: List): RealmQueryArgumentList { - val cArgs = realmc.new_queryArgArray(queryArgs.size) - queryArgs.mapIndexed { index, arg -> - val queryArg = realm_query_arg_t().apply { - when (arg) { - is RealmQueryListArgument -> { - nb_args = arg.arguments.size.toLong() - is_list = true - this.arg = arg.arguments.head - } - is RealmQuerySingleArgument -> { - nb_args = 1 - is_list = false - this.arg = arg.argument.value - } - } - } - realmc.queryArgArray_setitem(cArgs, index, queryArg) - } - return RealmQueryArgumentList(queryArgs.size.toLong(), cArgs) - } - private inline fun createTransport( value: T?, type: Int, @@ -151,6 +129,32 @@ class JvmMemTrackingAllocator : MemAllocator by JvmMemAllocator, MemTrackingAllo } } + override fun queryArgsOf(queryArgs: List): RealmQueryArgumentList { + val cArgs = realmc.new_queryArgArray(queryArgs.size) + queryArgs.mapIndexed { index, arg -> + val queryArg = realm_query_arg_t().apply { + when (arg) { + is RealmQueryListArgument -> { + nb_args = arg.arguments.size.toLong() + is_list = true + this.arg = arg.arguments.head + + scope.manageQueryListArgument(arg) + } + is RealmQuerySingleArgument -> { + nb_args = 1 + is_list = false + this.arg = arg.argument.value + } + } + } + realmc.queryArgArray_setitem(cArgs, index, queryArg) + } + return RealmQueryArgumentList(queryArgs.size.toLong(), cArgs).also { + scope.manageQueryArgumentList(it) + } + } + /** * Frees resources linked to this allocator's [scope], more specifically strings and binary * buffers. See [MemScope.free] for more details. @@ -180,16 +184,28 @@ class JvmMemTrackingAllocator : MemAllocator by JvmMemAllocator, MemTrackingAllo * copied out afterwards). */ class MemScope { - val values: MutableSet = mutableSetOf() + val values: MutableSet = mutableSetOf() fun manageRealmValue(value: RealmValueT): RealmValueT { values.add(value) return value } + fun manageQueryArgumentList(value: RealmQueryArgumentList): RealmQueryArgumentList = value.also { + values.add(value) + } + + fun manageQueryListArgument(value: RealmQueryListArgument): RealmQueryListArgument = value.also { + values.add(value) + } + fun free() { - values.map { - realmc.realm_value_t_cleanup(it) + values.forEach { + when (it) { + is RealmValueT -> realmc.realm_value_t_cleanup(it) + is RealmQueryArgumentList -> realmc.delete_queryArgArray(it.head) + is RealmQueryListArgument -> realmc.delete_valueArray(it.arguments.head) + } } } } 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 392e1a7797..1db4de4d21 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 @@ -721,10 +721,10 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) jboolean is_unrecognized_by_client = error.is_unrecognized_by_client; jboolean is_client_reset_requested = error.is_client_reset_requested; - auto user_info_map = new std::map(); + auto user_info_map = std::map(); for (int i = 0; i < error.user_info_length; i++) { realm_sync_error_user_info_t user_info = error.user_info_map[i]; - user_info_map->insert(std::make_pair(user_info.key, user_info.value)); + user_info_map.insert(std::make_pair(user_info.key, user_info.value)); } static JavaMethod core_compensating_write_info_constructor( @@ -766,16 +766,16 @@ jobject convert_to_jvm_sync_error(JNIEnv* jenv, const realm_sync_error_t& error) // mark the file for deletion. Having 'original path' in the user_info_map is a side effect of // using the same code for client reset. if (error.user_info_length > 0) { - auto end_it = user_info_map->end(); + auto end_it = user_info_map.end(); - auto original_it = user_info_map->find(error.c_original_file_path_key); + auto original_it = user_info_map.find(error.c_original_file_path_key); if (end_it != original_it) { auto original_file_path = original_it->second; joriginal_file_path = to_jstring(jenv, original_file_path); } // Sync errors may not have the path to the recovery file unless a Client Reset is requested - auto recovery_it = user_info_map->find(error.c_recovery_file_path_key); + auto recovery_it = user_info_map.find(error.c_recovery_file_path_key); if (error.is_client_reset_requested && (end_it != recovery_it)) { auto recovery_file_path = recovery_it->second; jrecovery_file_path = to_jstring(jenv, recovery_file_path); @@ -1053,3 +1053,17 @@ realm_scheduler_t* realm_create_generic_scheduler() { return new realm_scheduler_t { realm::util::Scheduler::make_dummy() }; } + +void +realm_property_info_t_cleanup(realm_property_info_t* value) { + delete[] value->link_origin_property_name; + delete[] value->link_target; + delete[] value->name; + delete[] value->public_name; +} + +void +realm_class_info_t_cleanup(realm_class_info_t * value) { + delete[] value->primary_key; + delete[] value->name; +} diff --git a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h index 210202a6c9..022bca21a7 100644 --- a/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h +++ b/packages/jni-swig-stub/src/main/jni/realm_api_helpers.h @@ -137,4 +137,10 @@ realm_sync_thread_error(realm_userdata_t userdata, const char* error); realm_scheduler_t* realm_create_generic_scheduler(); +void +realm_property_info_t_cleanup(realm_property_info_t* value); + +void +realm_class_info_t_cleanup(realm_class_info_t * value); + #endif //TEST_REALM_API_HELPERS_H From 2d3c9d1e4ad36eda0022f06a3cc07a49a7f4f169 Mon Sep 17 00:00:00 2001 From: clementetb Date: Wed, 11 Oct 2023 10:29:36 +0200 Subject: [PATCH 04/14] Fix darwin native leak (#1530) --- CHANGELOG.md | 3 +- .../kotlin/internal/interop/RealmInterop.kt | 58 ++++++++++--------- .../interop/SynchronizableObjectDarwin.kt | 6 +- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb960338d4..b91fe6a523 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,8 @@ ### Fixed * `Realm.getNumberOfActiveVersions` now returns the actual number of active versions. (Core issue [#6960](https://github.com/realm/realm-core/pull/6960)) -* Fix memory leaks on the JVM platform, see PR for more information. (Issue [#1526](https://github.com/realm/realm-kotlin/pull/1526)) +* Fixed memory leak on Darwin caused by a reference cycle between resources and the GC cleaner. (Issue [#1530](https://github.com/realm/realm-kotlin/pull/1530)) +* Fixed memory leaks on the JVM platform, see PR for more information. (Issue [#1526](https://github.com/realm/realm-kotlin/pull/1526)) ### Compatibility * File format: Generates Realms with file format v23. 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 4418168802..b33d0cbe22 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 @@ -111,7 +111,6 @@ import realm_wrapper.realm_property_info_t import realm_wrapper.realm_query_arg_t import realm_wrapper.realm_release import realm_wrapper.realm_results_t -import realm_wrapper.realm_scheduler_notify_func_t import realm_wrapper.realm_scheduler_t import realm_wrapper.realm_set_t import realm_wrapper.realm_string_t @@ -160,11 +159,22 @@ private fun checkedPointerResult(pointer: CPointer?): CPointer if (pointer == null) throwOnError(); return pointer } -// FIXME API-INTERNAL Consider making NativePointer/CPointerWrapper generic to enforce typing +/** + * Class with a pointer reference and its status. It breaks the reference cycle between CPointerWrapper + * and its GC cleaner, otherwise the cleaner would never be invoked. + * + * See leaking cleaner: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.native.ref/create-cleaner.html + */ +data class ReleasablePointer( + private val _ptr: CPointer?, + val released: AtomicBoolean = atomic(false) +) { + fun release() { + if (released.compareAndSet(expect = false, update = true)) { + realm_release(_ptr) + } + } -class CPointerWrapper(ptr: CPointer?, managed: Boolean = true) : NativePointer { - private val released: AtomicBoolean = atomic(false) - private val _ptr = checkedPointerResult(ptr) val ptr: CPointer? get() { return if (!released.value) { @@ -173,23 +183,28 @@ class CPointerWrapper(ptr: CPointer?, managed: Boolean throw POINTER_DELETED_ERROR } } +} + +// FIXME API-INTERNAL Consider making NativePointer/CPointerWrapper generic to enforce typing +class CPointerWrapper(ptr: CPointer?, managed: Boolean = true) : NativePointer { + val _ptr = ReleasablePointer( + checkedPointerResult(ptr) + ) + + val ptr: CPointer? = _ptr.ptr @OptIn(ExperimentalStdlibApi::class) val cleaner = if (managed) { createCleaner(_ptr) { - if (released.compareAndSet(expect = false, update = true)) { - realm_release(ptr) - } + it.release() } } else null override fun release() { - if (released.compareAndSet(expect = false, update = true)) { - realm_release(_ptr) - } + _ptr.release() } - override fun isReleased(): Boolean = released.value + override fun isReleased(): Boolean = _ptr.released.value } // Convenience type cast @@ -582,7 +597,7 @@ actual object RealmInterop { ) ) ?: error("Couldn't create scheduler") - scheduler.set_scheduler(capi_scheduler) + scheduler.setScheduler(capi_scheduler) return CPointerWrapper(capi_scheduler) } @@ -3378,11 +3393,6 @@ actual object RealmInterop { } } - data class CoreCallback( - val callback: realm_scheduler_notify_func_t, - val callbackUserdata: CPointer, - ) - interface Scheduler { fun notify() } @@ -3391,15 +3401,11 @@ actual object RealmInterop { val threadId: ULong, dispatcher: CoroutineDispatcher ) : Scheduler { - val scope: CoroutineScope = CoroutineScope(dispatcher) - val ref: CPointer - lateinit var scheduler: CPointer - - init { - ref = StableRef.create(this).asCPointer() - } + private val scope: CoroutineScope = CoroutineScope(dispatcher) + val ref: CPointer = StableRef.create(this).asCPointer() + private lateinit var scheduler: CPointer - fun set_scheduler(scheduler: CPointer) { + fun setScheduler(scheduler: CPointer) { this.scheduler = scheduler } diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/SynchronizableObjectDarwin.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/SynchronizableObjectDarwin.kt index 4eb9f0b68b..45a7dba42d 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/SynchronizableObjectDarwin.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/SynchronizableObjectDarwin.kt @@ -32,9 +32,9 @@ actual class SynchronizableObject { private val mutex = nativeHeap.alloc() @OptIn(ExperimentalStdlibApi::class) - private val cleaner = createCleaner(mutex) { - native_pthread_mutex_destroy(mutex.ptr) - nativeHeap.free(mutex) + private val cleaner = createCleaner(mutex) { capturedMutex -> + native_pthread_mutex_destroy(capturedMutex.ptr) + nativeHeap.free(capturedMutex) } init { From e0ad270c56d204c13d5a0ecd4788175bc8fe3157 Mon Sep 17 00:00:00 2001 From: clementetb Date: Fri, 13 Oct 2023 13:20:59 +0200 Subject: [PATCH 05/14] Tests GC realm pointer release (#1542) --- .../io/realm/kotlin/test/CinteropTest.kt | 38 ++++++++++++ .../realm/kotlin/test/darwin/MemoryTests.kt | 59 ------------------- 2 files changed, 38 insertions(+), 59 deletions(-) diff --git a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt index 01fd6fec06..b7f3e11c5f 100644 --- a/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt +++ b/packages/cinterop/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/CinteropTest.kt @@ -16,6 +16,7 @@ package io.realm.kotlin.test +import io.realm.kotlin.internal.interop.CPointerWrapper import io.realm.kotlin.internal.interop.ClassFlags import io.realm.kotlin.internal.interop.ClassInfo import io.realm.kotlin.internal.interop.CollectionType @@ -24,6 +25,7 @@ import io.realm.kotlin.internal.interop.PropertyFlags import io.realm.kotlin.internal.interop.PropertyInfo import io.realm.kotlin.internal.interop.PropertyType import io.realm.kotlin.internal.interop.RealmInterop +import io.realm.kotlin.internal.interop.RealmSchemaT import io.realm.kotlin.internal.interop.SchemaMode import io.realm.kotlin.internal.interop.SchemaValidationMode import io.realm.kotlin.internal.interop.set @@ -68,6 +70,7 @@ import realm_wrapper.realm_schema_t import realm_wrapper.realm_schema_validate import realm_wrapper.realm_string_t import realm_wrapper.realm_t +import kotlin.native.internal.GC import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -79,6 +82,41 @@ import kotlin.test.assertTrue // These test are not thought as being exhaustive, but is more to provide a playground for // experiments and maybe more relevant for reproduction of C-API issues. class CinteropTest { + /** + * Tests whether our autorelease pointer wrapper releases native memory. + * + * Allocates a Realm pointer wrapped with our GC autorelease wrapper, then returns the reference + * to the releasable pointer that would tell if the underlying pointer has been released. + */ + @Test + fun cpointerWrapper_releasesWhenGCed() { + val releasablePointer = { + memScoped { + val realmSchemaNew = realm_schema_new( + classes = allocArray(0), + num_classes = 0u, + class_properties = allocArray(0) + ) + + CPointerWrapper(realmSchemaNew)._ptr + } + }() + + // The pointer has not been reclaimed + assertFalse(releasablePointer.released.value) + + // Trigger GC and wait for some time to allow it to collect the object + for (i in 0..5) { + GC.collect() + platform.posix.sleep(5u) + + // if reclaimed stop looping + if (releasablePointer.released.value) break + } + + // The pointer has been reclaimed + assertTrue(releasablePointer.released.value, "Pointer was not reclaimed") + } @Test fun cinterop_cinterop() { diff --git a/packages/test-base/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/darwin/MemoryTests.kt b/packages/test-base/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/darwin/MemoryTests.kt index d2ea4d483d..3c111a3891 100644 --- a/packages/test-base/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/darwin/MemoryTests.kt +++ b/packages/test-base/src/nativeDarwinTest/kotlin/io/realm/kotlin/test/darwin/MemoryTests.kt @@ -19,7 +19,6 @@ package io.realm.kotlin.test.darwin import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.entities.Sample -import io.realm.kotlin.ext.query import io.realm.kotlin.test.platform.PlatformUtils.createTempDir import io.realm.kotlin.test.platform.PlatformUtils.deleteTempDir import io.realm.kotlin.test.platform.PlatformUtils.triggerGC @@ -34,7 +33,6 @@ import platform.posix.popen import kotlin.math.roundToInt import kotlin.test.AfterTest import kotlin.test.BeforeTest -import kotlin.test.Ignore import kotlin.test.Test import kotlin.test.assertEquals @@ -57,63 +55,6 @@ class MemoryTests { deleteTempDir(tmpDir) } - // TODO Only run on macOS, filter using https://developer.apple.com/documentation/foundation/nsprocessinfo/3608556-iosapponmac when upgrading to XCode 12 - @Test - @Ignore // Investigate https://github.com/realm/realm-kotlin/issues/327 - fun garbageCollectorShouldFreeNativeResources() { - @OptIn(ExperimentalStdlibApi::class) - println("NEW_MEMORY_MODEL: " + isExperimentalMM()) - - val referenceHolder = mutableListOf(); - { - val realm = openRealmFromTmpDir() - // TODO use Realm.delete once this is implemented - realm.writeBlocking { - delete(query()) - } - - // allocating a 1 MB string - val oneMBstring = StringBuilder("").apply { - for (i in 1..4096) { - // 128 length (256 bytes) - append("v7TPOZtm50q8kMBoKiKRaD2JhXgjM6OUNzHojXuFXvxdtwtN9fCVIW4njdwVdZ9aChvXCtW4nzUYeYWbI6wuSspbyjvACtMtjQTtOoe12ZEPZPII6PAFTfbrQQxc3ymJ") - } - }.toString() - - // inserting ~ 100MB of data - val elements: List = - realm.writeBlocking { - IntRange(1, 100).map { - copyToRealm(Sample()).apply { - stringField = oneMBstring - } - } - } - referenceHolder.addAll(elements) - }() - assertEquals( - "99.0M", - runSystemCommand(amountOfMemoryMappedInProcessCMD), - "We should have at least 99 MB allocated as mmap" - ) - // After releasing all the 'realm_object_create' reference the Realm should be closed and the - // no memory mapped file is allocated in the process - referenceHolder.clear() - triggerGC() - - platform.posix.sleep(1 * 5) // give chance to the Collector Thread to process references - - // We should find a way to just meassure the increase over these tests. Referencing - // NSProcessInfo.Companion.processInfo().operatingSystemVersionString - // as done in Darwin SystemUtils.kt can also cause allocations. Thus, just lazy evaluating - // those system constants for now to avoid affecting the tests. - assertEquals( - "", - runSystemCommand(amountOfMemoryMappedInProcessCMD), - "Freeing the references should close the Realm so no memory mapped allocation should be present" - ) - } - // TODO Only run on macOS, filter using https://developer.apple.com/documentation/foundation/nsprocessinfo/3608556-iosapponmac when upgrading to XCode 12 @Test fun closeShouldFreeMemory() { From be153b6d5e9b28f021b907ce81010fae707fcb49 Mon Sep 17 00:00:00 2001 From: clementetb Date: Mon, 30 Oct 2023 13:24:03 +0100 Subject: [PATCH 06/14] Fix new temp dirs not different on native (#1550) --- .../test/common/utils/PlatformUtilsTests.kt | 44 +++++++++++++++++++ .../kotlin/test/platform/PlatformUtils.kt | 13 +++--- 2 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/PlatformUtilsTests.kt diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/PlatformUtilsTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/PlatformUtilsTests.kt new file mode 100644 index 0000000000..7649677710 --- /dev/null +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/utils/PlatformUtilsTests.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.kotlin.test.common.utils + +import io.realm.kotlin.internal.platform.directoryExists +import io.realm.kotlin.test.platform.PlatformUtils +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class PlatformUtilsTests { + @Test + fun createTempDir_createDifferentDirs() { + val testDir1 = PlatformUtils.createTempDir("test-dir") + val testDir2 = PlatformUtils.createTempDir("test-dir") + + assertTrue(directoryExists(testDir1)) + assertTrue(directoryExists(testDir2)) + + assertNotEquals(testDir1, testDir2) + } + + @Test + fun createTempDir_deleteDifferentDirs() { + val testDir = PlatformUtils.createTempDir("test-dir") + assertTrue(directoryExists(testDir)) + PlatformUtils.deleteTempDir(testDir) + assertFalse(directoryExists(testDir)) + } +} 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 25c99d0556..87cd82b932 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 @@ -16,16 +16,17 @@ package io.realm.kotlin.test.platform +import io.realm.kotlin.test.util.Utils import kotlinx.cinterop.ULongVar import kotlinx.cinterop.alloc import kotlinx.cinterop.cValue -import kotlinx.cinterop.cstr import kotlinx.cinterop.memScoped import kotlinx.cinterop.ptr import kotlinx.cinterop.value import platform.posix.S_IRGRP import platform.posix.S_IROTH import platform.posix.S_IRUSR +import platform.posix.S_IRWXU import platform.posix.nanosleep import platform.posix.pthread_threadid_np import platform.posix.timespec @@ -34,10 +35,12 @@ import kotlin.time.Duration actual object PlatformUtils { actual fun createTempDir(prefix: String, readOnly: Boolean): String { - // X is a special char which will be replace by mkdtemp template - val mask = prefix.replace('X', 'Z', ignoreCase = true) - val path = "${platform.Foundation.NSTemporaryDirectory()}$mask" - platform.posix.mkdtemp(path.cstr) + // Currently we cannot template using platform.posix.mkdtemp + // the return value is not of use. + val suffix = "-${Utils.createRandomString(6)}" + val path = "${platform.Foundation.NSTemporaryDirectory()}$prefix$suffix" + platform.posix.mkdir(path, S_IRWXU.toUShort()) + if (readOnly) { platform.posix.chmod(path, (S_IRUSR or S_IRGRP or S_IROTH).toUShort()) } From 161ea050cc100a7c8da9e8ccbe442f5e5f27fc75 Mon Sep 17 00:00:00 2001 From: clementetb Date: Tue, 31 Oct 2023 09:56:55 +0100 Subject: [PATCH 07/14] Avoid initial version pinning (#1519) --- CHANGELOG.md | 1 + .../io/realm/kotlin/internal/LiveRealm.kt | 10 +-- .../io/realm/kotlin/internal/RealmImpl.kt | 78 ++++++++++--------- .../kotlin/internal/SuspendableNotifier.kt | 2 +- .../kotlin/internal/SuspendableWriter.kt | 3 +- .../io/realm/kotlin/internal/VersionData.kt | 4 +- .../io/realm/kotlin/internal/VersionInfo.kt | 2 +- .../realm/kotlin/internal/VersionTracker.kt | 29 ++++++- .../test/common/VersionTrackingTests.kt | 42 +++++++++- 9 files changed, 121 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b91fe6a523..dd9ffe5935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * `Realm.getNumberOfActiveVersions` now returns the actual number of active versions. (Core issue [#6960](https://github.com/realm/realm-core/pull/6960)) * Fixed memory leak on Darwin caused by a reference cycle between resources and the GC cleaner. (Issue [#1530](https://github.com/realm/realm-kotlin/pull/1530)) * Fixed memory leaks on the JVM platform, see PR for more information. (Issue [#1526](https://github.com/realm/realm-kotlin/pull/1526)) +* Removed pin on the initial realm version after opening a Realm. (Issue [#1519](https://github.com/realm/realm-kotlin/pull/1519)) ### Compatibility * File format: Generates Realms with file format v23. diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt index f0c7b7fd97..b2704d976b 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/LiveRealm.kt @@ -99,7 +99,7 @@ internal abstract class LiveRealm( internal fun gcTrackedSnapshot(): FrozenRealmReference { return snapshotLock.withLock { _snapshot.value.also { snapshot -> - if (_closeSnapshotWhenAdvancing) { + if (_closeSnapshotWhenAdvancing && !snapshot.isClosed()) { log.trace("${this@LiveRealm} ENABLE-TRACKING ${snapshot.version()}") _closeSnapshotWhenAdvancing = false } @@ -129,14 +129,14 @@ internal abstract class LiveRealm( log.trace("${this@LiveRealm} CLOSE-UNTRACKED $version") _snapshot.value.close() } else { - // TODO Split into track and clean up as we don't need to hold headLock while - // cleaning up as version tracker is only accessed from the same thread - versionTracker.trackAndCloseExpiredReferences(_snapshot.value) + versionTracker.trackReference(_snapshot.value) } _snapshot.value = realmReference.snapshot(owner) log.trace("${this@LiveRealm} ADVANCING $version -> ${_snapshot.value.version()}") _closeSnapshotWhenAdvancing = true } + + versionTracker.closeExpiredReferences() } protected open fun onSchemaChanged(schema: RealmSchemaPointer) { @@ -156,7 +156,7 @@ internal abstract class LiveRealm( // Close actual live reference. From this point off the snapshot will not be updated. realmReference.close() // Close current reference - _snapshot.value?.let { + _snapshot.value.let { log.trace("$this CLOSE-ACTIVE ${it.version()}") it.close() } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt index 64d6ed3eaf..85f415f028 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt @@ -82,9 +82,8 @@ public class RealmImpl private constructor( // closed. internal val realmStateFlow = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - - private var _realmReference: AtomicRef = atomic(null) - private val realmReferenceLock = SynchronizableObject() + // Initial realm reference that would be used until the notifier or writer are available. + internal var initialRealmReference: AtomicRef = atomic(null) /** * The current Realm reference that points to the underlying frozen C++ SharedRealm. @@ -99,7 +98,7 @@ public class RealmImpl private constructor( // Maybe we could just rely on the notifier to issue the initial frozen version, but that // would require us to sync that up. Didn't address this as we already have a todo on fixing // constructing the initial frozen version in the initialization of updatableRealm. - private val versionTracker = VersionTracker(this, log) + internal val versionTracker = VersionTracker(this, log) // Injection point for synchronized Realms. This property should only be used to hold state // required by synchronized realms. See `SyncedRealmContext` for more details. @@ -131,13 +130,17 @@ public class RealmImpl private constructor( } val (frozenReference, fileCreated) = configuration.openRealm(this@RealmImpl) realmFileCreated = assetFileCopied || fileCreated - versionTracker.trackAndCloseExpiredReferences(frozenReference) - _realmReference.value = frozenReference + versionTracker.trackReference(frozenReference) + initialRealmReference.value = frozenReference configuration.initializeRealmData(this@RealmImpl, realmFileCreated) } realmScope.launch { notifier.realmChanged().collect { + removeInitialRealmReference() + // Closing this reference might be done by the GC: + // https://github.com/realm/realm-kotlin/issues/1527 + versionTracker.closeExpiredReferences() notifierFlow.emit(UpdatedRealmImpl(this@RealmImpl)) } } @@ -226,39 +229,44 @@ public class RealmImpl private constructor( return notifier.registerObserver(t) } - public fun realmReference(): FrozenRealmReference { - realmReferenceLock.withLock { - val value1 = _realmReference.value - // We don't consider advancing the version if is is already closed. - value1?.let { - if (it.isClosed()) return it - } - - // Consider versions of current realm, notifier and writer to identify if we should - // advance the user facing realms version to a newer frozen snapshot. - val version = value1?.version() - val notifierSnapshot = notifier.version - val writerSnapshot = writer.version - - var newest: LiveRealmHolder? = null - if (notifierSnapshot != null && version != null && notifierSnapshot > version) { - newest = notifier - } - @Suppress("ComplexCondition") - if (writerSnapshot != null && version != null && ((writerSnapshot > version) || (notifierSnapshot != null && writerSnapshot > notifierSnapshot))) { - newest = writer - } - if (newest != null) { - _realmReference.value = newest.snapshot - log.debug("$this ADVANCING $version -> ${_realmReference.value?.version()}") - } + /** + * Removes the local reference to start relying on the notifier - writer for snapshots. + */ + private fun removeInitialRealmReference() { + if (initialRealmReference.value != null) { + log.trace("REMOVING INITIAL VERSION") + // There is at least a new version available in the notifier, stop tracking the local one + initialRealmReference.value = null } - return _realmReference.value ?: sdkError("Accessing realmReference before realm has been opened") + } + + public fun realmReference(): FrozenRealmReference { + // We don't require to return the latest snapshot to the user but the closest the best. + // `initialRealmReference` is accessed from different threads, grab a copy to safely operate on it. + return initialRealmReference.value.let { localReference -> + // Find whether the user-facing, notifier or writer has the latest snapshot. + // Sort is stable, it will try to preserve the following order. + listOf( + { localReference } to localReference?.uncheckedVersion(), + { writer.snapshot } to writer.version, + { notifier.snapshot } to notifier.version, + ).sortedByDescending { + it.second + }.first().first.invoke() + } ?: sdkError("Accessing realmReference before realm has been opened") } public fun activeVersions(): VersionInfo { - val mainVersions: VersionData? = _realmReference.value?.let { VersionData(it.uncheckedVersion(), versionTracker.versions()) } - return VersionInfo(mainVersions, notifier.versions(), writer.versions()) + val mainVersions: VersionData = VersionData( + current = initialRealmReference.value?.uncheckedVersion(), + active = versionTracker.versions() + ) + + return VersionInfo( + main = mainVersions, + notifier = notifier.versions(), + writer = writer.versions() + ) } override fun close() { diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt index 3d8bfb49e4..260b254399 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableNotifier.kt @@ -53,7 +53,7 @@ internal class SuspendableNotifier( private inner class NotifierRealm : LiveRealm( owner = owner, configuration = owner.configuration, - scheduler = scheduler + scheduler = scheduler, ) { // This is guaranteed to be triggered before any other notifications for the same // update as we get all callbacks on the same single thread dispatcher diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt index e18f20b11a..295c2637f7 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/SuspendableWriter.kt @@ -59,7 +59,7 @@ internal class SuspendableWriter( LiveRealm( owner = owner, configuration = owner.configuration, - scheduler = scheduler + scheduler = scheduler, ), InternalMutableRealm, InternalTypedRealm, @@ -85,6 +85,7 @@ internal class SuspendableWriter( // Must only be accessed from the dispatchers thread override val realm: WriterRealm by realmInitializer + private val shouldClose = kotlinx.atomicfu.atomic(false) private val transactionMutex = Mutex(false) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionData.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionData.kt index e964136c3f..7e1fe0656a 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionData.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionData.kt @@ -21,6 +21,6 @@ import io.realm.kotlin.VersionId /** * Version meta data for a single instance. */ -public data class VersionData(val current: VersionId, val active: Set) { - val versions: Set = setOf(current) + active +public data class VersionData(val current: VersionId?, val active: Set) { + val versions: Set = (current?.let { setOf(it) } ?: emptySet()) + active } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionInfo.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionInfo.kt index 2d73960d9b..d18bac8d4c 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionInfo.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionInfo.kt @@ -22,7 +22,7 @@ import io.realm.kotlin.VersionId * Version meta data for an overall [Realm]-instance with [VersionData] for the user-facing [Realm] * and the underlying [SuspendableNotifier]'s and [SuspendableWriter]'s live realms. */ -public data class VersionInfo(val main: VersionData?, val notifier: VersionData?, val writer: VersionData?) { +public data class VersionInfo(val main: VersionData, val notifier: VersionData?, val writer: VersionData?) { val all: Set = setOf(main, notifier, writer).mapNotNull { it?.versions }.flatten().toSet() val allTracked: Set = setOf(main, notifier, writer).mapNotNull { it?.active }.flatten().toSet() } diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionTracker.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionTracker.kt index e4b47096bd..f0d75d5831 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionTracker.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/VersionTracker.kt @@ -23,6 +23,7 @@ import io.realm.kotlin.internal.platform.WeakReference import kotlinx.atomicfu.AtomicRef import kotlinx.atomicfu.atomic +internal typealias IntermediateReference = Pair> /** * Bookkeeping of intermediate versions that needs to be closed when no longer referenced or when * explicitly closing a realm. @@ -32,14 +33,33 @@ import kotlinx.atomicfu.atomic internal class VersionTracker(private val owner: BaseRealmImpl, private val log: ContextLogger) { // Set of currently open realms. Storing the native pointer explicitly to enable us to close // the realm when the RealmReference is no longer referenced anymore. - private val intermediateReferences: AtomicRef>>> = atomic(mutableSetOf()) + private val intermediateReferences: AtomicRef> = + atomic(mutableSetOf()) - fun trackAndCloseExpiredReferences(realmReference: FrozenRealmReference? = null) { - val references = mutableSetOf>>() - realmReference?.let { + fun trackReference(realmReference: FrozenRealmReference) { + // We need a new object to update the atomic reference + val references = mutableSetOf().apply { + addAll(intermediateReferences.value) + } + + realmReference.let { log.trace("$owner TRACK-VERSION ${realmReference.version()}") references.add(Pair(realmReference.dbPointer, WeakReference(it))) } + + intermediateReferences.value = references + } + /** + * Closes any realm reference that has been reclaimed by the GC. + * + * @return false if there is no reference left to clean. + */ + // Closing expired references might be done by the GC: + // https://github.com/realm/realm-kotlin/issues/1527 + fun closeExpiredReferences() { + // We need a new object to update the atomic reference + val references = mutableSetOf() + intermediateReferences.value.forEach { entry -> val (pointer, ref) = entry if (ref.get() == null) { @@ -49,6 +69,7 @@ internal class VersionTracker(private val owner: BaseRealmImpl, private val log: references.add(entry) } } + intermediateReferences.value = references } diff --git a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt index 919c21ba0d..6a28982495 100644 --- a/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt +++ b/packages/test-base/src/commonTest/kotlin/io/realm/kotlin/test/common/VersionTrackingTests.kt @@ -32,6 +32,7 @@ import io.realm.kotlin.log.RealmLog import io.realm.kotlin.notifications.RealmChange import io.realm.kotlin.notifications.ResultsChange import io.realm.kotlin.test.platform.PlatformUtils +import io.realm.kotlin.test.util.receiveOrFail import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel @@ -83,7 +84,7 @@ class VersionTrackingTests { assertEquals(1, allTracked.size) // The notifier might or might not had time to run notifier?.let { - assertEquals(2, it.current.version) + assertEquals(2, it.current?.version) assertEquals(0, it.active.size) } assertNull(writer) @@ -211,6 +212,45 @@ class VersionTrackingTests { samples.map { it.list.version() }.joinToString { it.toString() } ) } + + @Test + @Suppress("invisible_member", "invisible_reference") + fun initialVersionDereferencedAfterFirstWrite() { + (realm as RealmImpl).let { realm -> + assertNotNull(realm.initialRealmReference.value, toString()) + assertEquals(1, realm.versionTracker.versions().size, toString()) + + val realmUpdates = Channel(1) + + runBlocking { + val deferred = async { + realm.asFlow().collect { + realmUpdates.trySend(Unit) + } + } + + // Wait for the notifier to start + realmUpdates.receiveOrFail() + + realm.write { } + + // Wait for the notifier to start + realmUpdates.receiveOrFail() + + assertNull(realm.initialRealmReference.value, toString()) + assertEquals(1, realm.versionTracker.versions().size, toString()) + + deferred.cancel() + realmUpdates.close() + } + } + } +} + +@Suppress("invisible_member", "invisible_reference") +internal fun Realm.userFacingRealmVersions(): Int = (this as RealmImpl).let { realm -> + if (realm.initialRealmReference.value != null) 1 + else 0 } internal fun Realm.activeVersions(): VersionInfo = (this as RealmImpl).activeVersions() From 38053817726102ca4f989784564a29d7d9f7e586 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 1 Nov 2023 07:03:16 +0000 Subject: [PATCH 08/14] Update Core to 13.23.2 (#1557) --- CHANGELOG.md | 2 +- .../realm/kotlin/test/sync/SyncEnumTests.kt | 9 ++++ .../interop/sync/ProtocolErrorCode.kt | 19 +++++++ .../interop/sync/ProtocolErrorCode.kt | 44 ++++++++++++++++ packages/cinterop/src/native/realm.def | 2 +- .../interop/sync/ProtocolErrorCode.kt | 50 +++++++++++++++++++ packages/external/core | 2 +- packages/jni-swig-stub/realm.i | 8 ++- .../test/mongodb/common/SyncedRealmTests.kt | 1 - 9 files changed, 131 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf9ce2d1e..2ca238e662 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ GET requests. (Issue [#1517](https://github.com/realm/realm-kotlin/pull/1517)) * Minimum Android SDK: 16. ### Internal -* Updated to Realm Core 13.23.1, commit 3618b2e9d679cd2880be8df17b79d4cc6d71ff76. +* Updated to Realm Core 13.23.2, commit e6271d72308b40399890060f58a88cf568c2ee22. ## 1.11.1 (2023-09-07) diff --git a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt index 1effc3cd32..4d12f28409 100644 --- a/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt +++ b/packages/cinterop/src/androidInstrumentedTest/kotlin/io/realm/kotlin/test/sync/SyncEnumTests.kt @@ -28,6 +28,7 @@ import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e import io.realm.kotlin.internal.interop.realm_sync_errno_session_e import io.realm.kotlin.internal.interop.realm_sync_session_resync_mode_e import io.realm.kotlin.internal.interop.realm_sync_session_state_e +import io.realm.kotlin.internal.interop.realm_sync_socket_callback_result_e import io.realm.kotlin.internal.interop.realm_user_state_e import io.realm.kotlin.internal.interop.realm_web_socket_errno_e import io.realm.kotlin.internal.interop.sync.AuthProvider @@ -38,6 +39,7 @@ import io.realm.kotlin.internal.interop.sync.MetadataMode import io.realm.kotlin.internal.interop.sync.SyncConnectionErrorCode import io.realm.kotlin.internal.interop.sync.SyncSessionErrorCode import io.realm.kotlin.internal.interop.sync.SyncSessionResyncMode +import io.realm.kotlin.internal.interop.sync.WebsocketCallbackResult import io.realm.kotlin.internal.interop.sync.WebsocketErrorCode import org.junit.Test import kotlin.reflect.KClass @@ -135,6 +137,13 @@ class SyncEnumTests { } } + @Test + fun websocketResultCode() { + checkEnum(realm_sync_socket_callback_result_e::class) { nativeValue -> + WebsocketCallbackResult.of(nativeValue) + } + } + private inline fun checkEnum( enumClass: KClass, mapNativeValue: (Int) -> T? diff --git a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index b7984a1d84..dae3a68dd8 100644 --- a/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/commonMain/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -124,3 +124,22 @@ expect enum class WebsocketErrorCode : CodeDescription { internal fun of(nativeValue: Int): WebsocketErrorCode? } } + +/** + * Wrapper for C-API `realm_sync_socket_callback_result` + * See https://github.com/realm/realm-core/blob/master/src/realm/error_codes.h#L298 + */ +expect enum class WebsocketCallbackResult : CodeDescription { + RLM_ERR_SYNC_SOCKET_SUCCESS, + RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED, + RLM_ERR_SYNC_SOCKET_RUNTIME, + RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY, + RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED, + RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED, + RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED, + RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT; + + companion object { + fun of(nativeValue: Int): WebsocketCallbackResult? + } +} diff --git a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 8ba8028fa8..d0fa2de33e 100644 --- a/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/jvm/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -19,6 +19,7 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription import io.realm.kotlin.internal.interop.realm_sync_errno_connection_e import io.realm.kotlin.internal.interop.realm_sync_errno_session_e +import io.realm.kotlin.internal.interop.realm_sync_socket_callback_result_e import io.realm.kotlin.internal.interop.realm_web_socket_errno_e actual enum class SyncConnectionErrorCode( @@ -135,3 +136,46 @@ actual enum class WebsocketErrorCode( } } } + +actual enum class WebsocketCallbackResult(override val description: String, override val nativeValue: Int) : CodeDescription { + + RLM_ERR_SYNC_SOCKET_SUCCESS( + "Websocket callback success", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_SUCCESS + ), + RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED( + "Websocket callback aborted", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED + ), + RLM_ERR_SYNC_SOCKET_RUNTIME( + "Websocket Runtime error", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_RUNTIME + ), + RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY( + "Websocket out of memory ", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY + ), + RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED( + "Websocket address space exhausted", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED + ), + RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED( + "Websocket connection closed", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED + ), + RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED( + "Websocket not supported", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED + ), + RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT( + "Websocket invalid argument", + realm_sync_socket_callback_result_e.RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT + ); + + actual companion object { + actual fun of(nativeValue: Int): WebsocketCallbackResult? = + values().firstOrNull { value -> + value.nativeValue == nativeValue + } + } +} diff --git a/packages/cinterop/src/native/realm.def b/packages/cinterop/src/native/realm.def index 0c92edfbee..ec72bbd95a 100644 --- a/packages/cinterop/src/native/realm.def +++ b/packages/cinterop/src/native/realm.def @@ -11,7 +11,7 @@ headerFilter = realm.h realm/error_codes.h // libraryPaths.macos_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ // libraryPaths.ios_x64 = ../external/core/build-macos_x64/src/realm/object-store/c_api ../external/core/build-macos_x64/src/realm ../external/core/build-macos_x64/src/realm/parser ../external/core/build-macos_x64/src/realm/object-store/ linkerOpts = -lcompression -lz -framework Foundation -framework CoreFoundation -framework Security -strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session realm_web_socket_errno +strictEnums = realm_errno realm_error_category realm_sync_errno_client realm_sync_errno_connection realm_sync_errno_session realm_web_socket_errno realm_sync_socket_callback_result --- #include #include diff --git a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt index 20817a0ae5..818028eeb2 100644 --- a/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt +++ b/packages/cinterop/src/nativeDarwin/kotlin/io/realm/kotlin/internal/interop/sync/ProtocolErrorCode.kt @@ -18,6 +18,7 @@ package io.realm.kotlin.internal.interop.sync import io.realm.kotlin.internal.interop.CodeDescription import realm_wrapper.realm_sync_errno_connection import realm_wrapper.realm_sync_errno_session +import realm_wrapper.realm_sync_socket_callback_result import realm_wrapper.realm_web_socket_errno actual enum class SyncConnectionErrorCode( @@ -138,3 +139,52 @@ actual enum class WebsocketErrorCode( } } } + +actual enum class WebsocketCallbackResult( + override val description: String, + nativeError: realm_sync_socket_callback_result +) : CodeDescription { + + RLM_ERR_SYNC_SOCKET_SUCCESS( + "Websocket callback success", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_SUCCESS + ), + RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED( + "Websocket callback aborted", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_OPERATION_ABORTED + ), + RLM_ERR_SYNC_SOCKET_RUNTIME( + "Websocket Runtime error", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_RUNTIME + ), + RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY( + "Websocket out of memory ", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_OUT_OF_MEMORY + ), + RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED( + "Websocket address space exhausted", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_ADDRESS_SPACE_EXHAUSTED + ), + RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED( + "Websocket connection closed", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_CONNECTION_CLOSED + ), + RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED( + "Websocket not supported", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_NOT_SUPPORTED + ), + RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT( + "Websocket invalid argument", + realm_sync_socket_callback_result.RLM_ERR_SYNC_SOCKET_INVALID_ARGUMENT + ); + + override val nativeValue: Int = nativeError.value.toInt() + val asNativeEnum: realm_sync_socket_callback_result = nativeError + + actual companion object { + actual fun of(nativeValue: Int): WebsocketCallbackResult? = + values().firstOrNull { value -> + value.nativeValue == nativeValue + } + } +} diff --git a/packages/external/core b/packages/external/core index 3618b2e9d6..e6271d7230 160000 --- a/packages/external/core +++ b/packages/external/core @@ -1 +1 @@ -Subproject commit 3618b2e9d679cd2880be8df17b79d4cc6d71ff76 +Subproject commit e6271d72308b40399890060f58a88cf568c2ee22 diff --git a/packages/jni-swig-stub/realm.i b/packages/jni-swig-stub/realm.i index 02880c1df5..8bc6d3b223 100644 --- a/packages/jni-swig-stub/realm.i +++ b/packages/jni-swig-stub/realm.i @@ -301,7 +301,7 @@ return $jnicall; realm_flx_sync_mutable_subscription_set_t*, realm_flx_sync_subscription_desc_t*, realm_set_t*, realm_async_open_task_t*, realm_dictionary_t*, realm_sync_session_connection_state_notification_token_t*, - realm_dictionary_changes_t*, realm_scheduler_t* }; + realm_dictionary_changes_t*, realm_scheduler_t*, realm_sync_socket_t* }; // For all functions returning a pointer or bool, check for null/false and throw an error if // realm_get_last_error returns true. @@ -330,6 +330,11 @@ bool realm_object_is_valid(const realm_object_t*); } jresult = (jboolean)result; } + +%typemap(javaimports) realm_sync_socket_callback_result %{ +import static io.realm.kotlin.internal.interop.realm_errno_e.*; +%} + // Just showcasing a wrapping concept. Maybe we should just go with `long` (apply void* as above) //%typemap(jstype) realm_t* "LongPointerWrapper" //%typemap(javain) realm_t* "$javainput.ptr()" @@ -431,4 +436,3 @@ bool realm_object_is_valid(const realm_object_t*); %include "realm.h" %include "realm/error_codes.h" %include "src/main/jni/realm_api_helpers.h" - 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 e486bd9e4d..9ee6a43c5a 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 @@ -1260,7 +1260,6 @@ class SyncedRealmTests { } assertTrue(customLogger.logs.isNotEmpty()) assertTrue(customLogger.logs.any { it.contains("Connection[1]: Negotiated protocol version:") }, "Missing Connection[1]") - assertTrue(customLogger.logs.any { it.contains("MyCustomApp/1.0.0") }, "Missing MyCustomApp/1.0.0") } } From 65fa711fee39d2bbf27c3b3165e3307f77c06c98 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 1 Nov 2023 14:21:33 +0100 Subject: [PATCH 09/14] Better error messages when test times out. --- .../io/realm/kotlin/test/mongodb/common/SyncedRealmTests.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 9ee6a43c5a..fe2851414b 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 @@ -739,7 +739,7 @@ class SyncedRealmTests { realm.writeBlocking { copyToRealm(masterObject) } realm.syncSession.uploadAllLocalChanges() } - assertEquals(42, counterValue.receiveOrFail()) + assertEquals(42, counterValue.receiveOrFail(), "Failed to receive 42") // Increment counter asynchronously after download initial data (1) val increment1 = async { @@ -755,7 +755,7 @@ class SyncedRealmTests { } } } - assertEquals(43, counterValue.receiveOrFail()) + assertEquals(43, counterValue.receiveOrFail(), "Failed to receive 43") // Increment counter asynchronously after download initial data (2) val increment2 = async { @@ -771,7 +771,7 @@ class SyncedRealmTests { } } } - assertEquals(44, counterValue.receiveOrFail()) + assertEquals(44, counterValue.receiveOrFail(), "Failed to receive 44") increment1.cancel() increment2.cancel() From 6561006928ae7c2d0d6a61e0c9e63c1472c65ef7 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 1 Nov 2023 18:18:37 +0100 Subject: [PATCH 10/14] Fix merge conflict --- .../commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt index fa4c7bae35..9906b050df 100644 --- a/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt +++ b/packages/library-base/src/commonMain/kotlin/io/realm/kotlin/internal/RealmImpl.kt @@ -278,11 +278,7 @@ public class RealmImpl private constructor( // TODO Reconsider this constraint. We have the primitives to check is we are on the // writer thread and just close the realm in writer.close() writer.checkInTransaction("Cannot close the Realm while inside a transaction block") - realmReferenceLock.withLock { - if (isClosed()) { - return - } - isClosed.value = true + if (!isClosed.getAndSet(true)) { runBlocking { writer.close() realmScope.cancel() From 783b1fb8588997e23e13bc11b33411cf3fe2becb Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Wed, 1 Nov 2023 20:54:37 +0100 Subject: [PATCH 11/14] Release 1.12.0 --- CHANGELOG.md | 2 +- buildSrc/src/main/kotlin/Config.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41b984e4ee..455d9ef012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.12.0-SNAPSHOT (YYYY-MM-DD) +## 1.12.0-SNAPSHOT (2023-11-01) This release upgrades the Sync metadata in a way that is not compatible with older versions. To downgrade a Sync app from this version, you'll need to manually delete the metadata folder located at `$[SYNC-ROOT-DIRECTORY]/mongodb-realm/[APP-ID]/server-utility/metadata/`. This will log out all users. diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 3402f0e318..e4e68d9d1a 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.12.0-SNAPSHOT" + const val version = "1.12.0" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" From e0871ede49d8f5d8b5c90be390f6598b76cd9938 Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 2 Nov 2023 06:59:15 +0100 Subject: [PATCH 12/14] Release 1.12.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 455d9ef012..372727e2bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.12.0-SNAPSHOT (2023-11-01) +## 1.12.0 (2023-11-02) This release upgrades the Sync metadata in a way that is not compatible with older versions. To downgrade a Sync app from this version, you'll need to manually delete the metadata folder located at `$[SYNC-ROOT-DIRECTORY]/mongodb-realm/[APP-ID]/server-utility/metadata/`. This will log out all users. From 5f7b8fa617a58e6072d04ecb65448f548b5bf06c Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 2 Nov 2023 16:52:10 +0100 Subject: [PATCH 13/14] Prepare next dev iteration --- buildSrc/src/main/kotlin/Config.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index e4e68d9d1a..00567f83ff 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.12.0" + const val version = "1.12.1-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin" From e19b1d1b083b292513b356812e413f54cb460f2c Mon Sep 17 00:00:00 2001 From: Christian Melchior Date: Thu, 2 Nov 2023 16:53:45 +0100 Subject: [PATCH 14/14] Prepare next dev iteration --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ buildSrc/src/main/kotlin/Config.kt | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 372727e2bd..031ba5a78c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,32 @@ +## 1.13.0-SNAPSHOT (YYYY-MM-DD) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* None. + +### Compatibility +* File format: Generates Realms with file format v23. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* This release is compatible with the following Kotlin releases: + * Kotlin 1.8.0 and above. The K2 compiler is not supported yet. + * Ktor 2.1.2 and above. + * Coroutines 1.7.0 and above. + * AtomicFu 0.18.3 and above. + * The new memory model only. See https://github.com/realm/realm-kotlin#kotlin-memory-model-and-coroutine-compatibility +* Minimum Kbson 0.3.0. +* Minimum Gradle version: 6.8.3. +* Minimum Android Gradle Plugin version: 4.1.3. +* Minimum Android SDK: 16. + +### Internal +* None. + + ## 1.12.0 (2023-11-02) This release upgrades the Sync metadata in a way that is not compatible with older versions. To downgrade a Sync app from this version, you'll need to manually delete the metadata folder located at `$[SYNC-ROOT-DIRECTORY]/mongodb-realm/[APP-ID]/server-utility/metadata/`. This will log out all users. diff --git a/buildSrc/src/main/kotlin/Config.kt b/buildSrc/src/main/kotlin/Config.kt index 00567f83ff..161cda7944 100644 --- a/buildSrc/src/main/kotlin/Config.kt +++ b/buildSrc/src/main/kotlin/Config.kt @@ -62,7 +62,7 @@ val HOST_OS: OperatingSystem = findHostOs() object Realm { val ciBuild = (System.getenv("JENKINS_HOME") != null) - const val version = "1.12.1-SNAPSHOT" + const val version = "1.13.0-SNAPSHOT" const val group = "io.realm.kotlin" const val projectUrl = "https://realm.io" const val pluginPortalId = "io.realm.kotlin"