Skip to content

Add Store.attach. #376

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions objectbox/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

* Add an option to change code-generator's `output_dir` in `pubspec.yaml`. #341
* Support ObjectBox Admin for Android apps to browse the database, see our [docs](https://docs.objectbox.io/data-browser) to get started. #148
* 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
* Update: [objectbox-c 0.15.2](https://github.com/objectbox/objectbox-c/releases/tag/v0.15.0).
* Update: [objectbox-android 3.1.2](https://github.com/objectbox/objectbox-java/releases/tag/V3.1.0).
* Update: [objectbox-swift 1.7.0](https://github.com/objectbox/objectbox-swift/releases/tag/v1.7.0).
Expand Down
149 changes: 116 additions & 33 deletions objectbox/lib/src/native/store.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'dart:typed_data';

import 'package:ffi/ffi.dart';
import 'package:meta/meta.dart';
import 'package:objectbox/src/native/version.dart';
import 'package:path/path.dart' as path;

import '../common.dart';
Expand All @@ -27,6 +28,13 @@ part 'observable.dart';
/// Represents an ObjectBox database and works together with [Box] to allow
/// getting and putting.
class Store {
/// Path of the default directory, currently 'objectbox'.
static const String defaultDirectoryPath = 'objectbox';

/// Enables a couple of debug logs.
/// This meant for tests only; do not enable for releases!
static bool debugLogs = false;

late final Pointer<OBX_store> _cStore;
HashMap<int, Type>? _entityTypeById;
final _boxes = HashMap<Type, Box>();
Expand All @@ -36,8 +44,8 @@ class Store {
final _reader = ReaderWithCBuffer();
Transaction? _tx;

/// absolute path to the database directory
final String _dbDir;
/// Absolute path to the database directory, used for open check.
final String _absoluteDirectoryPath;

late final ByteData _reference;

Expand All @@ -51,8 +59,12 @@ class Store {
/// Default value for string query conditions [caseSensitive] argument.
final bool _queriesCaseSensitiveDefault;

static String _safeDirectoryPath(String? path) =>
(path == null || path.isEmpty) ? defaultDirectoryPath : path;

/// Creates a BoxStore using the model definition from your
/// `objectbox.g.dart` file.
/// `objectbox.g.dart` file in the given [directory] path
/// (or if null the [defaultDirectoryPath]).
///
/// For example in a Flutter app:
/// ```dart
Expand All @@ -76,10 +88,8 @@ class Store {
String? macosApplicationGroup})
: _weak = false,
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault,
_dbDir = path.context.canonicalize(
(directory == null || directory.isEmpty)
? 'objectbox'
: directory) {
_absoluteDirectoryPath =
path.context.canonicalize(_safeDirectoryPath(directory)) {
try {
if (Platform.isMacOS && macosApplicationGroup != null) {
if (!macosApplicationGroup.endsWith('/')) {
Expand All @@ -96,12 +106,7 @@ class Store {
malloc.free(cStr);
}
}
if (_openStoreDirectories.contains(_dbDir)) {
throw UnsupportedError(
'Cannot create multiple Store instances for the same directory. '
'Please use a single Store or close() the previous instance before '
'opening another one.');
}
_checkStoreDirectoryNotOpen();
final model = Model(_defs.model);

final opt = C.opt();
Expand Down Expand Up @@ -130,25 +135,14 @@ class Store {
C.opt_free(opt);
rethrow;
}
if (debugLogs) {
print('Opening store (C lib V${libraryVersion()})... path=$directory'
' isOpen=${isOpen(directory)}');
}

_cStore = C.store_open(opt);

try {
checkObxPtr(_cStore, 'failed to create store');
} on ObjectBoxException catch (e) {
// Recognize common problems when trying to open/create a database
// 10199 = OBX_ERROR_STORAGE_GENERAL
// 13 = permissions denied, 30 = read-only filesystem
if (e.message.contains(OBX_ERROR_STORAGE_GENERAL.toString()) &&
e.message.contains('Dir does not exist') &&
(e.message.endsWith(' (13)') || e.message.endsWith(' (30)'))) {
throw ObjectBoxException(e.message +
' - this usually indicates a problem with permissions; '
"if you're using Flutter you may need to use "
'getApplicationDocumentsDirectory() from the path_provider '
'package, see example/README.md');
}
rethrow;
}
_checkStorePointer(_cStore);

// Always create _reference, so it can be non-nullable.
// Ensure we only try to access the store created in the same process.
Expand All @@ -157,7 +151,7 @@ class Store {
_reference.setUint64(0 * _int64Size, pid);
_reference.setUint64(1 * _int64Size, _ptr.address);

_openStoreDirectories.add(_dbDir);
_openStoreDirectories.add(_absoluteDirectoryPath);
} catch (e) {
_reader.clear();
rethrow;
Expand Down Expand Up @@ -205,7 +199,7 @@ class Store {
{bool queriesCaseSensitiveDefault = true})
// must not close the same native store twice so [_weak]=true
: _weak = true,
_dbDir = '',
_absoluteDirectoryPath = '',
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault {
// see [reference] for serialization order
final readPid = _reference.getUint64(0 * _int64Size);
Expand All @@ -221,6 +215,95 @@ class Store {
}
}

/// Attach to a store opened in the [directoryPath]
/// (or if null the [defaultDirectoryPath]).
///
/// Use this to access an open store from other isolates.
/// This results in each isolate having access to the same underlying native
/// store.
///
/// The returned store is a new instance (e.g. different pointer value) with
/// its own lifetime and must also be closed (e.g. before an isolate exits).
/// The actual underlying store is only closed when the last store instance
/// is closed (e.g. when the app exits).
Store.attach(this._defs, String? directoryPath,
{bool queriesCaseSensitiveDefault = true})
// _weak = false so store can be closed.
: _weak = false,
_queriesCaseSensitiveDefault = queriesCaseSensitiveDefault,
_absoluteDirectoryPath =
path.context.canonicalize(_safeDirectoryPath(directoryPath)) {
try {
// Do not allow attaching to a store that is already open in the current
// isolate. While technically possible this is not the intended usage
// and e.g. transactions would have to be carefully managed to not
// overlap.
_checkStoreDirectoryNotOpen();

final path = _safeDirectoryPath(directoryPath);
final pathCStr = path.toNativeUtf8();
try {
if (debugLogs) {
final isOpen = C.store_is_open(pathCStr.cast());
print('Attaching to store... path=$path isOpen=$isOpen');
}
_cStore = C.store_attach(pathCStr.cast());
} finally {
malloc.free(pathCStr);
}

checkObxPtr(_cStore,
'could not attach to the store at given path - please ensure it was opened before');

// Not setting _reference as this is a replacement for obtaining a store
// via reference.
} catch (e) {
_reader.clear();
rethrow;
}
}

void _checkStoreDirectoryNotOpen() {
if (_openStoreDirectories.contains(_absoluteDirectoryPath)) {
throw UnsupportedError(
'Cannot create multiple Store instances for the same directory in the same isolate. '
'Please use a single Store, close() the previous instance before '
'opening another one or attach to it in another isolate.');
}
}

void _checkStorePointer(Pointer cStore) {
try {
checkObxPtr(cStore, 'failed to create store');
} on ObjectBoxException catch (e) {
// Recognize common problems when trying to open/create a database
// 10199 = OBX_ERROR_STORAGE_GENERAL
// 13 = permissions denied, 30 = read-only filesystem
if (e.message.contains(OBX_ERROR_STORAGE_GENERAL.toString()) &&
e.message.contains('Dir does not exist') &&
(e.message.endsWith(' (13)') || e.message.endsWith(' (30)'))) {
throw ObjectBoxException(e.message +
' - this usually indicates a problem with permissions; '
"if you're using Flutter you may need to use "
'getApplicationDocumentsDirectory() from the path_provider '
'package, see example/README.md');
}
rethrow;
}
}

/// Returns if an open store (i.e. opened before and not yet closed) was found
/// for the given [directoryPath] (or if null the [defaultDirectoryPath]).
static bool isOpen(String? directoryPath) {
final path = _safeDirectoryPath(directoryPath);
final cStr = path.toNativeUtf8();
try {
return C.store_is_open(cStr.cast());
} finally {
malloc.free(cStr);
}
}

/// Returns a store reference you can use to create a new store instance with
/// a single underlying native store. See [Store.fromReference] for more details.
ByteData get reference => _reference;
Expand All @@ -243,7 +326,7 @@ class Store {
_reader.clear();

if (!_weak) {
_openStoreDirectories.remove(_dbDir);
_openStoreDirectories.remove(_absoluteDirectoryPath);
checkObx(C.store_close(_cStore));
}
}
Expand Down
82 changes: 82 additions & 0 deletions objectbox/test/basics_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import 'dart:ffi' as ffi;
import 'dart:io';
import 'dart:isolate';

import 'package:async/async.dart';
import 'package:objectbox/internal.dart';
import 'package:objectbox/src/native/bindings/bindings.dart';
import 'package:objectbox/src/native/bindings/helpers.dart';
Expand Down Expand Up @@ -63,6 +65,58 @@ void main() {
env.closeAndDelete();
});

test('store attach fails if same isolate', () {
final env = TestEnv('basics');
expect(
() => Store.attach(getObjectBoxModel(), env.dir.path),
throwsA(predicate((UnsupportedError e) =>
e.message!.contains('Cannot create multiple Store instances'))));
env.closeAndDelete();
});

test('store attach remains open if main store closed', () async {
final env = TestEnv('basics');
final store1 = env.store;
final receivePort = ReceivePort();
final received = StreamQueue<dynamic>(receivePort);
await Isolate.spawn(storeAttachIsolate,
StoreAttachIsolateInit(receivePort.sendPort, env.dir.path));
final commandPort = await received.next as SendPort;

// Check native instance pointer is different.
final store2Address = await received.next as int;
expect(InternalStoreAccess.ptr(store1).address, isNot(store2Address));

final id = store1.box<TestEntity>().put(TestEntity(tString: 'foo'));
expect(id, 1);
// Close original store to test store remains open until all refs closed.
store1.close();
expect(true, Store.isOpen('testdata-basics'));

// Read data with attached store.
commandPort.send(id);
final readtString = await received.next as String?;
expect(readtString, isNotNull);
expect(readtString, 'foo');

// Close attached store, should close store completely.
commandPort.send(null);
await received.next;
expect(false, Store.isOpen('testdata-basics'));

// Dispose StreamQueue.
await received.cancel();
});

test('store is open', () {
expect(false, Store.isOpen(''));
expect(false, Store.isOpen('testdata-basics'));
final env = TestEnv('basics');
expect(true, Store.isOpen('testdata-basics'));
env.closeAndDelete();
expect(false, Store.isOpen('testdata-basics'));
});

test('transactions', () {
final env = TestEnv('basics');
expect(TxMode.values.length, 2);
Expand Down Expand Up @@ -139,3 +193,31 @@ void main() {
Directory('basics').deleteSync(recursive: true);
});
}

class StoreAttachIsolateInit {
SendPort sendPort;
String path;

StoreAttachIsolateInit(this.sendPort, this.path);
}

void storeAttachIsolate(StoreAttachIsolateInit init) async {
final store2 = Store.attach(getObjectBoxModel(), init.path);

final commandPort = ReceivePort();
init.sendPort.send(commandPort.sendPort);
init.sendPort.send(InternalStoreAccess.ptr(store2).address);

await for (final message in commandPort) {
if (message is int) {
final read = store2.box<TestEntity>().get(message);
init.sendPort.send(read?.tString);
} else if (message == null) {
store2.close();
init.sendPort.send(null);
break;
}
}

print('Store attach isolate finished');
}
Loading