record pool / watch work

This commit is contained in:
Christien Rioux 2024-01-06 20:47:10 -05:00
parent 31f562119a
commit ba4ef05a28
4 changed files with 178 additions and 156 deletions

View File

@ -58,6 +58,8 @@ Future<AccountInfo> fetchAccountInfo(FetchAccountInfoRef ref,
return AccountInfo(status: AccountInfoStatus.accountLocked, active: active);
}
xxx login should open this key and leave it open, logout should close it
// Pull the account DHT key, decode it and return it
final pool = await DHTRecordPool.instance();
final account = await (await pool.openOwned(

View File

@ -19,7 +19,10 @@ class DHTRecord {
_writer = writer,
_open = true,
_valid = true,
_subkeySeqCache = {};
_subkeySeqCache = {},
needsWatchStateUpdate = false,
inWatchStateUpdate = false;
final VeilidRoutingContext _routingContext;
final DHTRecordDescriptor _recordDescriptor;
final int _defaultSubkey;
@ -28,7 +31,10 @@ class DHTRecord {
final DHTRecordCrypto _crypto;
bool _open;
bool _valid;
StreamSubscription<VeilidUpdateValueChange>? _watchSubscription;
StreamController<VeilidUpdateValueChange>? watchController;
bool needsWatchStateUpdate;
bool inWatchStateUpdate;
WatchState? watchState;
int subkeyOrDefault(int subkey) => (subkey == -1) ? _defaultSubkey : subkey;
@ -37,6 +43,7 @@ class DHTRecord {
PublicKey get owner => _recordDescriptor.owner;
KeyPair? get ownerKeyPair => _recordDescriptor.ownerKeyPair();
DHTSchema get schema => _recordDescriptor.schema;
int get subkeyCount => _recordDescriptor.schema.subkeyCount();
KeyPair? get writer => _writer;
OwnedDHTRecordPointer get ownedDHTRecordPointer =>
OwnedDHTRecordPointer(recordKey: key, owner: ownerKeyPair!);
@ -48,8 +55,9 @@ class DHTRecord {
if (!_open) {
return;
}
await watchController?.close();
await _routingContext.closeDHTRecord(_recordDescriptor.key);
await DHTRecordPool.instance.recordClosed(_recordDescriptor.key);
DHTRecordPool.instance.recordClosed(_recordDescriptor.key);
_open = false;
}
@ -258,14 +266,36 @@ class DHTRecord {
{List<ValueSubkeyRange>? subkeys,
Timestamp? expiration,
int? count}) async {
// register watch with pool
_watchSubscription = await DHTRecordPool.instance.recordWatch(
_recordDescriptor.key, onUpdate,
subkeys: subkeys, expiration: expiration, count: count);
// Set up watch requirements which will get picked up by the next tick
watchState =
WatchState(subkeys: subkeys, expiration: expiration, count: count);
needsWatchStateUpdate = true;
}
Future<StreamSubscription<VeilidUpdateValueChange>> listen(
Future<void> Function(VeilidUpdateValueChange update) onUpdate,
) async {
// Set up watch requirements
watchController ??=
StreamController<VeilidUpdateValueChange>.broadcast(onCancel: () {
// If there are no more listeners then we can get rid of the controller
watchController = null;
});
return watchController!.stream.listen(
(update) {
Future.delayed(Duration.zero, () => onUpdate(update));
},
cancelOnError: true,
onError: (e) async {
await watchController!.close();
watchController = null;
});
}
Future<void> cancelWatch() async {
// register watch with pool
await _watchSubscription?.cancel();
// Tear down watch requirements
watchState = null;
needsWatchStateUpdate = true;
}
}

View File

@ -1,3 +1,6 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:bloc/bloc.dart';
import '../../veilid_support.dart';
@ -5,49 +8,76 @@ import '../../veilid_support.dart';
class DhtRecordCubit<T> extends Cubit<AsyncValue<T>> {
DhtRecordCubit({
required DHTRecord record,
required Future<T?> Function(DHTRecord, VeilidUpdateValueChange)
required Future<T?> Function(DHTRecord) initialStateFunction,
required Future<T?> Function(DHTRecord, List<ValueSubkeyRange>, ValueData)
stateFunction,
List<ValueSubkeyRange> watchSubkeys = const [],
}) : _record = record,
super(const AsyncValue.loading()) {
}) : super(const AsyncValue.loading()) {
Future.delayed(Duration.zero, () async {
await record.watch((update) async {
// Make initial state update
try {
final initialState = await initialStateFunction(record);
if (initialState != null) {
emit(AsyncValue.data(initialState));
}
} on Exception catch (e) {
emit(AsyncValue.error(e));
}
_subscription = await record.listen((update) async {
try {
final newState = await stateFunction(record, update);
final newState =
await stateFunction(record, update.subkeys, update.valueData);
if (newState != null) {
emit(AsyncValue.data(newState));
}
} on Exception catch (e) {
emit(AsyncValue.error(e));
}
}, subkeys: watchSubkeys);
});
});
}
@override
Future<void> close() async {
await _record.cancelWatch();
await _subscription?.cancel();
_subscription = null;
await super.close();
}
DHTRecord _record;
StreamSubscription<VeilidUpdateValueChange>? _subscription;
}
class SingleDHTRecordCubit<T> extends DhtRecordCubit<T> {
SingleDHTRecordCubit(
{required super.record,
required T? Function(List<int> data) decodeState,
int singleSubkey = 0})
: super(
stateFunction: (record, update) async {
//
if (update.subkeys.isNotEmpty) {
final newState = decodeState(update.valueData.data);
return newState;
}
// Cubit that watches the default subkey value of a dhtrecord
class DefaultDHTRecordCubit<T> extends DhtRecordCubit<T> {
DefaultDHTRecordCubit({
required super.record,
required T Function(List<int> data) decodeState,
}) : super(
initialStateFunction: (record) async {
final initialData = await record.get();
if (initialData == null) {
return null;
},
watchSubkeys: [
ValueSubkeyRange(low: singleSubkey, high: singleSubkey)
]);
}
return decodeState(initialData);
},
stateFunction: (record, subkeys, valueData) async {
final defaultSubkey = record.subkeyOrDefault(-1);
if (subkeys.containsSubkey(defaultSubkey)) {
final Uint8List data;
final firstSubkey = subkeys.firstOrNull!.low;
if (firstSubkey != defaultSubkey) {
final maybeData = await record.get(forceRefresh: true);
if (maybeData == null) {
return null;
}
data = maybeData;
} else {
data = valueData.data;
}
final newState = decodeState(data);
return newState;
}
return null;
},
);
}

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:mutex/mutex.dart';
import '../../../../veilid_support.dart';
@ -38,8 +37,8 @@ class OwnedDHTRecordPointer with _$OwnedDHTRecordPointer {
}
/// Watch state
class _WatchState {
_WatchState(
class WatchState {
WatchState(
{required this.subkeys, required this.expiration, required this.count});
List<ValueSubkeyRange>? subkeys;
Timestamp? expiration;
@ -47,39 +46,20 @@ class _WatchState {
Timestamp? realExpiration;
}
/// Opened DHTRecord state
class _OpenedDHTRecord {
_OpenedDHTRecord(this.routingContext)
: mutex = Mutex(),
needsWatchStateUpdate = false,
inWatchStateUpdate = false;
Future<void> close() async {
await watchController?.close();
}
Mutex mutex;
StreamController<VeilidUpdateValueChange>? watchController;
bool needsWatchStateUpdate;
bool inWatchStateUpdate;
_WatchState? watchState;
VeilidRoutingContext routingContext;
}
class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
DHTRecordPool._(Veilid veilid, VeilidRoutingContext routingContext)
: _state = DHTRecordPoolAllocations(
childrenByParent: IMap(),
parentByChild: IMap(),
rootRecords: ISet()),
_opened = <TypedKey, _OpenedDHTRecord>{},
_opened = <TypedKey, DHTRecord>{},
_routingContext = routingContext,
_veilid = veilid;
// Persistent DHT record list
DHTRecordPoolAllocations _state;
// Which DHT records are currently open
final Map<TypedKey, _OpenedDHTRecord> _opened;
final Map<TypedKey, DHTRecord> _opened;
// Default routing context to use for new keys
final VeilidRoutingContext _routingContext;
// Convenience accessor
@ -116,59 +96,18 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
Veilid get veilid => _veilid;
Future<void> _recordOpened(
TypedKey key, VeilidRoutingContext routingContext) async {
// no race because dart is single threaded until async breaks
final odr = _opened[key] ?? _OpenedDHTRecord(routingContext);
_opened[key] = odr;
await odr.mutex.acquire();
}
Future<StreamSubscription<VeilidUpdateValueChange>> recordWatch(
TypedKey key, Future<void> Function(VeilidUpdateValueChange) onUpdate,
{required List<ValueSubkeyRange>? subkeys,
required Timestamp? expiration,
required int? count}) async {
final odr = _opened[key];
if (odr == null) {
throw StateError("can't watch unopened record");
void _recordOpened(DHTRecord record) {
if (_opened.containsKey(record.key)) {
throw StateError('record already opened');
}
// Set up watch requirements
odr
..watchState =
_WatchState(subkeys: subkeys, expiration: expiration, count: count)
..needsWatchStateUpdate = true
..watchController ??=
StreamController<VeilidUpdateValueChange>.broadcast(onCancel: () {
// Request watch state change for cancel
odr
..watchState = null
..needsWatchStateUpdate = true;
// If there are no more listeners then we can get rid of the controller
if (!(odr.watchController?.hasListener ?? true)) {
odr.watchController = null;
}
});
return odr.watchController!.stream.listen(
(update) {
Future.delayed(Duration.zero, () => onUpdate(update));
},
cancelOnError: true,
onError: (e) async {
await odr.watchController!.close();
odr.watchController = null;
});
_opened[record.key] = record;
}
Future<void> recordClosed(TypedKey key) async {
final odr = _opened.remove(key);
if (odr == null) {
void recordClosed(TypedKey key) {
final rec = _opened.remove(key);
if (rec == null) {
throw StateError('record already closed');
}
await odr.close();
odr.mutex.release();
}
Future<void> deleteDeep(TypedKey parent) async {
@ -178,10 +117,6 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
while (currentDeps.isNotEmpty) {
final nextDep = currentDeps.removeLast();
// Ensure we get the exclusive lock on this record
// Can use default routing context here because we are only deleting
await _recordOpened(nextDep, _routingContext);
// Remove this child from its parent
await _removeDependency(nextDep);
@ -191,11 +126,16 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
currentDeps.addAll(childDeps);
}
// Delete all records
// Delete all dependent records in parallel
final allFutures = <Future<void>>[];
for (final dep in allDeps) {
// If record is opened, close it first
final rec = _opened[dep];
if (rec != null) {
await rec.close();
}
// Then delete
allFutures.add(_routingContext.deleteDHTRecord(dep));
await recordClosed(dep);
}
await Future.wait(allFutures);
}
@ -288,7 +228,8 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
recordDescriptor.ownerTypedKeyPair()!));
await _addDependency(parent, rec.key);
await _recordOpened(rec.key, dhtctx);
_recordOpened(rec);
return rec;
}
@ -301,28 +242,22 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
DHTRecordCrypto? crypto}) async {
final dhtctx = routingContext ?? _routingContext;
await _recordOpened(recordKey, dhtctx);
late final DHTRecord rec;
try {
// If we are opening a key that already exists
// make sure we are using the same parent if one was specified
_validateParent(parent, recordKey);
// If we are opening a key that already exists
// make sure we are using the same parent if one was specified
_validateParent(parent, recordKey);
// Open from the veilid api
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null);
rec = DHTRecord(
routingContext: dhtctx,
recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey,
crypto: crypto ?? const DHTRecordCryptoPublic());
// Open from the veilid api
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, null);
rec = DHTRecord(
routingContext: dhtctx,
recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey,
crypto: crypto ?? const DHTRecordCryptoPublic());
// Register the dependency
await _addDependency(parent, rec.key);
} on Exception catch (_) {
await recordClosed(recordKey);
rethrow;
}
// Register the dependency
await _addDependency(parent, rec.key);
_recordOpened(rec);
return rec;
}
@ -338,31 +273,25 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
}) async {
final dhtctx = routingContext ?? _routingContext;
await _recordOpened(recordKey, dhtctx);
late final DHTRecord rec;
try {
// If we are opening a key that already exists
// make sure we are using the same parent if one was specified
_validateParent(parent, recordKey);
// If we are opening a key that already exists
// make sure we are using the same parent if one was specified
_validateParent(parent, recordKey);
// Open from the veilid api
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer);
rec = DHTRecord(
routingContext: dhtctx,
recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey,
writer: writer,
crypto: crypto ??
await DHTRecordCryptoPrivate.fromTypedKeyPair(
TypedKeyPair.fromKeyPair(recordKey.kind, writer)));
// Open from the veilid api
final recordDescriptor = await dhtctx.openDHTRecord(recordKey, writer);
rec = DHTRecord(
routingContext: dhtctx,
recordDescriptor: recordDescriptor,
defaultSubkey: defaultSubkey,
writer: writer,
crypto: crypto ??
await DHTRecordCryptoPrivate.fromTypedKeyPair(
TypedKeyPair.fromKeyPair(recordKey.kind, writer)));
// Register the dependency if specified
await _addDependency(parent, rec.key);
} on Exception catch (_) {
await recordClosed(recordKey);
rethrow;
}
// Register the dependency if specified
await _addDependency(parent, rec.key);
_recordOpened(rec);
return rec;
}
@ -389,15 +318,46 @@ class DHTRecordPool with TableDBBacked<DHTRecordPoolAllocations> {
crypto: crypto,
);
/// Look up an opened DHRRecord
DHTRecord? getOpenedRecord(TypedKey recordKey) => _opened[recordKey];
/// Get the parent of a DHTRecord key if it exists
TypedKey? getParentRecord(TypedKey child) {
TypedKey? getParentRecordKey(TypedKey child) {
final childJson = child.toJson();
return _state.parentByChild[childJson];
}
/// Handle the DHT record updates coming from Veilid
void processUpdateValueChange(VeilidUpdateValueChange updateValueChange) {
if (updateValueChange.subkeys.isNotEmpty) {}
if (updateValueChange.subkeys.isNotEmpty) {
// Change
for (final kv in _opened.entries) {
if (kv.key == updateValueChange.key) {
kv.value.watchController?.add(updateValueChange);
break;
}
}
} else {
// Expired, process renewal if desired
for (final kv in _opened.entries) {
if (kv.key == updateValueChange.key) {
// Renew watch state
kv.value.needsWatchStateUpdate = true;
// See if the watch had an expiration and if it has expired
// otherwise the renewal will keep the same parameters
final watchState = kv.value.watchState;
if (watchState != null) {
final exp = watchState.expiration;
if (exp != null && exp.value < Veilid.instance.now().value) {
// Has expiration, and it has expired, clear watch state
kv.value.watchState = null;
}
}
break;
}
}
}
}
/// Ticker to check watch state change requests