From 44d201b34f90ad99bf553c3764c2702326e031a6 Mon Sep 17 00:00:00 2001 From: Uwe Trottmann <13865709+greenrobot-team@users.noreply.github.com> Date: Tue, 8 Feb 2022 09:38:16 +0100 Subject: [PATCH 1/6] Add directoryPath property to Store. --- objectbox/lib/src/native/store.dart | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart index 3b310f71b..16ee139db 100644 --- a/objectbox/lib/src/native/store.dart +++ b/objectbox/lib/src/native/store.dart @@ -44,6 +44,9 @@ class Store { final _reader = ReaderWithCBuffer(); Transaction? _tx; + /// Path to the database directory. + final String directoryPath; + /// Absolute path to the database directory, used for open check. final String _absoluteDirectoryPath; @@ -88,6 +91,7 @@ class Store { String? macosApplicationGroup}) : _weak = false, _queriesCaseSensitiveDefault = queriesCaseSensitiveDefault, + directoryPath = _safeDirectoryPath(directory), _absoluteDirectoryPath = path.context.canonicalize(_safeDirectoryPath(directory)) { try { @@ -114,13 +118,11 @@ class Store { try { checkObx(C.opt_model(opt, model.ptr)); - if (directory != null && directory.isNotEmpty) { - final cStr = directory.toNativeUtf8(); - try { - checkObx(C.opt_directory(opt, cStr.cast())); - } finally { - malloc.free(cStr); - } + final cStr = directoryPath.toNativeUtf8(); + try { + checkObx(C.opt_directory(opt, cStr.cast())); + } finally { + malloc.free(cStr); } if (maxDBSizeInKB != null && maxDBSizeInKB > 0) { C.opt_max_db_size_in_kb(opt, maxDBSizeInKB); @@ -199,6 +201,7 @@ class Store { {bool queriesCaseSensitiveDefault = true}) // must not close the same native store twice so [_weak]=true : _weak = true, + directoryPath = '', _absoluteDirectoryPath = '', _queriesCaseSensitiveDefault = queriesCaseSensitiveDefault { // see [reference] for serialization order @@ -231,6 +234,7 @@ class Store { // _weak = false so store can be closed. : _weak = false, _queriesCaseSensitiveDefault = queriesCaseSensitiveDefault, + directoryPath = _safeDirectoryPath(directoryPath), _absoluteDirectoryPath = path.context.canonicalize(_safeDirectoryPath(directoryPath)) { try { @@ -240,12 +244,12 @@ class Store { // overlap. _checkStoreDirectoryNotOpen(); - final path = _safeDirectoryPath(directoryPath); - final pathCStr = path.toNativeUtf8(); + final pathCStr = this.directoryPath.toNativeUtf8(); try { if (debugLogs) { final isOpen = C.store_is_open(pathCStr.cast()); - print('Attaching to store... path=$path isOpen=$isOpen'); + print( + 'Attaching to store... path=${this.directoryPath} isOpen=$isOpen'); } _cStore = C.store_attach(pathCStr.cast()); } finally { From b4064873e465c41aa54187dd911a098b99c236d6 Mon Sep 17 00:00:00 2001 From: Markus Date: Mon, 7 Feb 2022 11:49:00 +0100 Subject: [PATCH 2/6] Add Store.runIsolated. --- objectbox/CHANGELOG.md | 3 ++ objectbox/lib/src/native/store.dart | 70 +++++++++++++++++++++++++++++ objectbox/test/basics_test.dart | 24 ++++++++++ 3 files changed, 97 insertions(+) diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 8920c6360..ebf73fd89 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -2,6 +2,9 @@ * Support [ObjectBox Admin](https://docs.objectbox.io/data-browser) for Android apps to browse the database. #148 +* Add `Store.runIsolated` to run database operations (asynchronous) in the background. It spawns an + isolate, runs the given callback in that isolate with its own Store and returns the result of the + callback. This is similar to Flutters compute, but with the callback having access to a Store. * Add `Store.attach` to attach to a Store opened in a directory. This is an improved replacement for `Store.fromReference` to share a Store across isolates. It is no longer required to pass a Store reference and the underlying Store remains open until the last instance is closed. #376 diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart index 16ee139db..5624be944 100644 --- a/objectbox/lib/src/native/store.dart +++ b/objectbox/lib/src/native/store.dart @@ -382,6 +382,39 @@ class Store { return _runInTransaction(mode, (tx) => fn()); } + // Isolate entry point must be static or top-level. + static void _callFunctionWithStoreInIsolate(IsoPass isoPass) { + final store = Store.attach(isoPass.model, isoPass.dbDirectoryPath, + queriesCaseSensitiveDefault: isoPass.queriesCaseSensitiveDefault); + final result = isoPass.runFn(store); + store.close(); + // Note: maybe replace with Isolate.exit once min Dart SDK 2.15. + isoPass.resultPort?.send(result); + } + + /// Spawns an isolate, runs [callback] in that isolate passing it [param] with + /// its own Store and returns the result of callback. + /// + /// Instances of [callback] must be top-level functions or static methods + /// of classes, not closures or instance methods of objects. + Future runIsolated( + TxMode mode, R Function(Store, P) callback, P param) async { + final resultPort = ReceivePort(); + // Await isolate spawn to avoid waiting forever if it fails to spawn. + await Isolate.spawn( + _callFunctionWithStoreInIsolate, + IsoPass(_defs, directoryPath, _queriesCaseSensitiveDefault, + resultPort.sendPort, callback, param)); + // Use Completer to return result so type is not lost. + final result = Completer(); + resultPort.listen((dynamic message) { + result.complete(message as R); + }); + await result.future; + resultPort.close(); + return result.future; + } + /// Internal only - bypasses the main checks for async functions, you may /// only pass synchronous callbacks! R _runInTransaction(TxMode mode, R Function(Transaction) fn) { @@ -495,3 +528,40 @@ final _openStoreDirectories = HashSet(); /// Otherwise, it's we can distinguish at runtime whether a function is async. final _nullSafetyEnabled = _nullReturningFn is! Future Function(); final _nullReturningFn = () => null; + +/// Captures everything required to create a "copy" of a store in an isolate +/// and run user code. +@immutable +class IsoPass { + /// + final ModelDefinition model; + + /// Used to attach to store in separate isolate + /// (may be replaced in the future). + final String dbDirectoryPath; + + /// Config + final bool queriesCaseSensitiveDefault; + + /// Non-void functions can use this port to receive the result + final SendPort? resultPort; + + /// Parameter passed to the function + final P param; + + /// Function to be called in isolate + final R Function(Store, P) fn; + + /// creates everything that needs to be passed to the isolate. + const IsoPass( + this.model, + this.dbDirectoryPath, + // ignore: avoid_positional_boolean_parameters + this.queriesCaseSensitiveDefault, + this.resultPort, + this.fn, + this.param); + + /// Called inside this class so types are not lost (dynamic instead of P and R). + R runFn(Store store) => fn(store, param); +} diff --git a/objectbox/test/basics_test.dart b/objectbox/test/basics_test.dart index c2b2c469c..e4c6a3448 100644 --- a/objectbox/test/basics_test.dart +++ b/objectbox/test/basics_test.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:ffi' as ffi; import 'dart:io'; import 'dart:isolate'; @@ -192,6 +193,29 @@ void main() { store.close(); Directory('basics').deleteSync(recursive: true); }); + + test('store_runInIsolatedTx', () async { + final env = TestEnv('basics'); + final id = env.box.put(TestEntity(tString: 'foo')); + final futureResult = + env.store.runIsolated(TxMode.write, readStringAndRemove, id); + print('Count in main isolate: ${env.box.count()}'); + final x = await futureResult; + expect(x, 'foo!'); + expect(env.box.count(), 0); // Must be removed once awaited + env.closeAndDelete(); + }); +} + +String readStringAndRemove(Store store, int id) { + var box = store.box(); + var testEntity = box.get(id); + final result = testEntity!.tString! + '!'; + print('Result in 2nd isolate: $result'); + final removed = box.remove(id); + print('Removed in 2nd isolate: $removed'); + print('Count in 2nd isolate after remove: ${box.count()}'); + return result; } class StoreAttachIsolateInit { From a047193c325bcc0bfb874554cd888c9118e750ba Mon Sep 17 00:00:00 2001 From: Uwe Trottmann <13865709+greenrobot-team@users.noreply.github.com> Date: Tue, 8 Feb 2022 11:53:02 +0100 Subject: [PATCH 3/6] Support async functions as callback. --- objectbox/lib/src/native/store.dart | 11 ++++++----- objectbox/test/basics_test.dart | 5 +++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart index 5624be944..2d2536dd3 100644 --- a/objectbox/lib/src/native/store.dart +++ b/objectbox/lib/src/native/store.dart @@ -383,10 +383,11 @@ class Store { } // Isolate entry point must be static or top-level. - static void _callFunctionWithStoreInIsolate(IsoPass isoPass) { + static Future _callFunctionWithStoreInIsolate( + IsoPass isoPass) async { final store = Store.attach(isoPass.model, isoPass.dbDirectoryPath, queriesCaseSensitiveDefault: isoPass.queriesCaseSensitiveDefault); - final result = isoPass.runFn(store); + final result = await isoPass.runFn(store); store.close(); // Note: maybe replace with Isolate.exit once min Dart SDK 2.15. isoPass.resultPort?.send(result); @@ -398,7 +399,7 @@ class Store { /// Instances of [callback] must be top-level functions or static methods /// of classes, not closures or instance methods of objects. Future runIsolated( - TxMode mode, R Function(Store, P) callback, P param) async { + TxMode mode, FutureOr Function(Store, P) callback, P param) async { final resultPort = ReceivePort(); // Await isolate spawn to avoid waiting forever if it fails to spawn. await Isolate.spawn( @@ -550,7 +551,7 @@ class IsoPass { final P param; /// Function to be called in isolate - final R Function(Store, P) fn; + final FutureOr Function(Store, P) fn; /// creates everything that needs to be passed to the isolate. const IsoPass( @@ -563,5 +564,5 @@ class IsoPass { this.param); /// Called inside this class so types are not lost (dynamic instead of P and R). - R runFn(Store store) => fn(store, param); + FutureOr runFn(Store store) => fn(store, param); } diff --git a/objectbox/test/basics_test.dart b/objectbox/test/basics_test.dart index e4c6a3448..fb3c60048 100644 --- a/objectbox/test/basics_test.dart +++ b/objectbox/test/basics_test.dart @@ -207,7 +207,7 @@ void main() { }); } -String readStringAndRemove(Store store, int id) { +Future readStringAndRemove(Store store, int id) async { var box = store.box(); var testEntity = box.get(id); final result = testEntity!.tString! + '!'; @@ -215,7 +215,8 @@ String readStringAndRemove(Store store, int id) { final removed = box.remove(id); print('Removed in 2nd isolate: $removed'); print('Count in 2nd isolate after remove: ${box.count()}'); - return result; + // Pointless Future to test async functions are supported. + return await Future.delayed(const Duration(milliseconds: 10), () => result); } class StoreAttachIsolateInit { From 0c9d30f57d61bab78bb94996d0f148006e29d5a2 Mon Sep 17 00:00:00 2001 From: Uwe Trottmann <13865709+greenrobot-team@users.noreply.github.com> Date: Tue, 8 Feb 2022 12:06:45 +0100 Subject: [PATCH 4/6] Make IsoPass private, update docs. --- objectbox/lib/src/native/store.dart | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart index 2d2536dd3..e094a9e53 100644 --- a/objectbox/lib/src/native/store.dart +++ b/objectbox/lib/src/native/store.dart @@ -384,7 +384,7 @@ class Store { // Isolate entry point must be static or top-level. static Future _callFunctionWithStoreInIsolate( - IsoPass isoPass) async { + _IsoPass isoPass) async { final store = Store.attach(isoPass.model, isoPass.dbDirectoryPath, queriesCaseSensitiveDefault: isoPass.queriesCaseSensitiveDefault); final result = await isoPass.runFn(store); @@ -404,7 +404,7 @@ class Store { // Await isolate spawn to avoid waiting forever if it fails to spawn. await Isolate.spawn( _callFunctionWithStoreInIsolate, - IsoPass(_defs, directoryPath, _queriesCaseSensitiveDefault, + _IsoPass(_defs, directoryPath, _queriesCaseSensitiveDefault, resultPort.sendPort, callback, param)); // Use Completer to return result so type is not lost. final result = Completer(); @@ -533,36 +533,34 @@ final _nullReturningFn = () => null; /// Captures everything required to create a "copy" of a store in an isolate /// and run user code. @immutable -class IsoPass { - /// +class _IsoPass { final ModelDefinition model; /// Used to attach to store in separate isolate /// (may be replaced in the future). final String dbDirectoryPath; - /// Config final bool queriesCaseSensitiveDefault; - /// Non-void functions can use this port to receive the result + /// Non-void functions can use this port to receive the result. final SendPort? resultPort; - /// Parameter passed to the function + /// Parameter passed to [callback]. final P param; - /// Function to be called in isolate - final FutureOr Function(Store, P) fn; + /// To be called in isolate. + final FutureOr Function(Store, P) callback; - /// creates everything that needs to be passed to the isolate. - const IsoPass( + const _IsoPass( this.model, this.dbDirectoryPath, // ignore: avoid_positional_boolean_parameters this.queriesCaseSensitiveDefault, this.resultPort, - this.fn, + this.callback, this.param); - /// Called inside this class so types are not lost (dynamic instead of P and R). - FutureOr runFn(Store store) => fn(store, param); + /// Calls [callback] inside this class so types are not lost + /// (if called in isolate types would be dynamic instead of P and R). + FutureOr runFn(Store store) => callback(store, param); } From c521dd65ec79cdad574240e428ea09f207c0bdaa Mon Sep 17 00:00:00 2001 From: Uwe Trottmann <13865709+greenrobot-team@users.noreply.github.com> Date: Tue, 22 Feb 2022 08:52:27 +0100 Subject: [PATCH 5/6] Kill isolate once done with it. --- objectbox/lib/src/native/store.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart index e094a9e53..88d5c3e30 100644 --- a/objectbox/lib/src/native/store.dart +++ b/objectbox/lib/src/native/store.dart @@ -389,7 +389,8 @@ class Store { queriesCaseSensitiveDefault: isoPass.queriesCaseSensitiveDefault); final result = await isoPass.runFn(store); store.close(); - // Note: maybe replace with Isolate.exit once min Dart SDK 2.15. + // Note: maybe replace with Isolate.exit (and remove kill call in + // runIsolated) once min Dart SDK 2.15. isoPass.resultPort?.send(result); } @@ -402,7 +403,7 @@ class Store { TxMode mode, FutureOr Function(Store, P) callback, P param) async { final resultPort = ReceivePort(); // Await isolate spawn to avoid waiting forever if it fails to spawn. - await Isolate.spawn( + final isolate = await Isolate.spawn( _callFunctionWithStoreInIsolate, _IsoPass(_defs, directoryPath, _queriesCaseSensitiveDefault, resultPort.sendPort, callback, param)); @@ -413,6 +414,7 @@ class Store { }); await result.future; resultPort.close(); + isolate.kill(); return result.future; } From 153d5bb9e06cbd57bdcee1240c732174682e0c3f Mon Sep 17 00:00:00 2001 From: Uwe Trottmann <13865709+greenrobot-team@users.noreply.github.com> Date: Mon, 21 Feb 2022 16:16:46 +0100 Subject: [PATCH 6/6] Note Dart 2.15 is required for runIsolated. --- objectbox/CHANGELOG.md | 7 ++++--- objectbox/lib/src/native/store.dart | 3 +++ objectbox/test/basics_test.dart | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index ebf73fd89..942572a94 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -2,9 +2,10 @@ * Support [ObjectBox Admin](https://docs.objectbox.io/data-browser) for Android apps to browse the database. #148 -* Add `Store.runIsolated` to run database operations (asynchronous) in the background. It spawns an - isolate, runs the given callback in that isolate with its own Store and returns the result of the - callback. This is similar to Flutters compute, but with the callback having access to a Store. +* Add `Store.runIsolated` to run database operations (asynchronous) in the background + (requires Flutter 2.8.0/Dart 2.15.0 or newer). It spawns an isolate, runs the given callback in that + isolate with its own Store and returns the result of the callback. This is similar to Flutters + compute, but with the callback having access to a Store. #384 * Add `Store.attach` to attach to a Store opened in a directory. This is an improved replacement for `Store.fromReference` to share a Store across isolates. It is no longer required to pass a Store reference and the underlying Store remains open until the last instance is closed. #376 diff --git a/objectbox/lib/src/native/store.dart b/objectbox/lib/src/native/store.dart index 88d5c3e30..44b0ef096 100644 --- a/objectbox/lib/src/native/store.dart +++ b/objectbox/lib/src/native/store.dart @@ -399,6 +399,9 @@ class Store { /// /// Instances of [callback] must be top-level functions or static methods /// of classes, not closures or instance methods of objects. + /// + /// Note: this requires Dart 2.15.0 or newer + /// (shipped with Flutter 2.8.0 or newer). Future runIsolated( TxMode mode, FutureOr Function(Store, P) callback, P param) async { final resultPort = ReceivePort(); diff --git a/objectbox/test/basics_test.dart b/objectbox/test/basics_test.dart index fb3c60048..ff31e6641 100644 --- a/objectbox/test/basics_test.dart +++ b/objectbox/test/basics_test.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:isolate'; import 'package:async/async.dart'; +import 'package:meta/meta.dart'; import 'package:objectbox/internal.dart'; import 'package:objectbox/src/native/bindings/bindings.dart'; import 'package:objectbox/src/native/bindings/helpers.dart'; @@ -200,7 +201,21 @@ void main() { final futureResult = env.store.runIsolated(TxMode.write, readStringAndRemove, id); print('Count in main isolate: ${env.box.count()}'); - final x = await futureResult; + final String x; + try { + x = await futureResult; + } catch (e) { + final dartVersion = RegExp('([0-9]+).([0-9]+).([0-9]+)') + .firstMatch(Platform.version) + ?.group(0); + if (dartVersion != null && dartVersion.compareTo('2.15.0') < 0) { + print('runIsolated requires Dart 2.15, ignoring error.'); + env.closeAndDelete(); + return; + } else { + rethrow; + } + } expect(x, 'foo!'); expect(env.box.count(), 0); // Must be removed once awaited env.closeAndDelete();