croupier: Start and Stop Discovery Better
3 main changes
- Stop Scan and Advertise using a Future
- Scan and Advertise with attributes
- Do not start scanning until we have settings
Advertise and Scan were also made a LOT less race-y.
https://vanadium-review.googlesource.com/#/c/17722 inspired
the fixed, though it turns out I was mishandling a lot of
Futures along the way.
Closes https://github.com/vanadium/issues/issues/845
Change-Id: I8003a490a82c6522982fde3db2a2c9bee4f356bd
diff --git a/lib/logic/croupier.dart b/lib/logic/croupier.dart
index 5cb2eb4..eec18d6 100644
--- a/lib/logic/croupier.dart
+++ b/lib/logic/croupier.dart
@@ -2,11 +2,13 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-import 'game/game.dart'
- show Game, GameType, GameStartData, stringToGameType, gameTypeToString;
+import 'dart:async';
+
+import '../src/syncbase/settings_manager.dart' show SettingsManager;
import 'create_game.dart' as cg;
import 'croupier_settings.dart' show CroupierSettings;
-import '../src/syncbase/settings_manager.dart' show SettingsManager;
+import 'game/game.dart'
+ show Game, GameType, GameStartData, stringToGameType, gameTypeToString;
enum CroupierState {
Welcome,
@@ -30,6 +32,10 @@
Game game; // null until chosen
NoArgCb informUICb;
+ // Futures to use in order to cancel scans and advertisements.
+ Future _scanFuture;
+ Future _advertiseFuture;
+
Croupier() {
state = CroupierState.Welcome;
settings_everyone = new Map<int, CroupierSettings>();
@@ -91,12 +97,6 @@
case CroupierState.Welcome:
// data should be empty.
assert(data == null);
-
- // Start scanning for games if that's what's next for you.
- if (nextState == CroupierState.JoinGame) {
- settings_manager.scanSettings(); // don't wait for this future.
- }
-
break;
case CroupierState.Settings:
// data should be empty.
@@ -114,18 +114,20 @@
GameType gt = data as GameType;
game = cg.createGame(gt, 0); // Start as player 0 of whatever game type.
- settings_manager
+ _advertiseFuture = settings_manager
.createGameSyncgroup(gameTypeToString(gt), game.gameID)
.then((GameStartData gsd) {
// Only the game chooser should be advertising the game.
- settings_manager
- .advertiseSettings(gsd); // don't wait for this future.
- });
+ return settings_manager.advertiseSettings(gsd);
+ }); // don't wait for this future.
break;
case CroupierState.JoinGame:
// Note that if we were in join game, we must have been scanning.
- settings_manager.stopScanSettings();
+ _scanFuture.then((_) {
+ settings_manager.stopScanSettings();
+ _scanFuture = null;
+ });
if (data == null) {
// Back button pressed.
@@ -148,7 +150,12 @@
break;
case CroupierState.ArrangePlayers:
// Note that if we were arranging players, we might have been advertising.
- settings_manager.stopAdvertiseSettings();
+ if (_advertiseFuture != null) {
+ _advertiseFuture.then((_) {
+ settings_manager.stopAdvertiseSettings();
+ _advertiseFuture = null;
+ });
+ }
// data should be empty.
// All rearrangements affect the Game's player number without changing app state.
@@ -173,6 +180,10 @@
if (nextState == CroupierState.Welcome) {
games_found = new Map<String, GameStartData>();
players_found = new Map<int, int>();
+ } else if (nextState == CroupierState.JoinGame) {
+ // Start scanning for games since that's what's next for you.
+ _scanFuture =
+ settings_manager.scanSettings(); // don't wait for this future.
}
state = nextState;
diff --git a/lib/src/mocks/log_writer.dart b/lib/src/mocks/log_writer.dart
index 26e9075..19e6d58 100644
--- a/lib/src/mocks/log_writer.dart
+++ b/lib/src/mocks/log_writer.dart
@@ -6,10 +6,10 @@
enum SimulLevel { TURN_BASED, INDEPENDENT, DEPENDENT }
-typedef void updateCallbackT(String key, String value);
+typedef void keyValueCallback(String key, String value);
class LogWriter {
- final updateCallbackT updateCallback;
+ final keyValueCallback updateCallback;
final List<int> users;
String logPrefix; // This can be completely ignored.
diff --git a/lib/src/mocks/settings_manager.dart b/lib/src/mocks/settings_manager.dart
index f6aac0a..71d497a 100644
--- a/lib/src/mocks/settings_manager.dart
+++ b/lib/src/mocks/settings_manager.dart
@@ -9,9 +9,9 @@
import '../../logic/croupier_settings.dart' show CroupierSettings;
class SettingsManager {
- final util.updateCallbackT updateCallback;
- final util.updateCallbackT updateGamesCallback;
- final util.updateCallbackT updatePlayerFoundCallback;
+ final util.keyValueCallback updateCallback;
+ final util.keyValueCallback updateGamesCallback;
+ final util.keyValueCallback updatePlayerFoundCallback;
SettingsManager(this.updateCallback, this.updateGamesCallback,
this.updatePlayerFoundCallback);
@@ -44,13 +44,17 @@
return new Future(() => null);
}
- void stopScanSettings() {}
+ Future stopScanSettings() {
+ return new Future(() => null);
+ }
Future advertiseSettings(logic_game.GameStartData gsd) {
return new Future(() => null);
}
- void stopAdvertiseSettings() {}
+ Future stopAdvertiseSettings() {
+ return new Future(() => null);
+ }
Future createGameSyncgroup(String type, int gameID) {
return new Future(() => null);
diff --git a/lib/src/mocks/util.dart b/lib/src/mocks/util.dart
new file mode 100644
index 0000000..e466b8d
--- /dev/null
+++ b/lib/src/mocks/util.dart
@@ -0,0 +1,5 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+typedef void keyValueCallback(String key, String value);
diff --git a/lib/src/syncbase/discovery_client.dart b/lib/src/syncbase/discovery_client.dart
index bcf0482..51dcb9f 100644
--- a/lib/src/syncbase/discovery_client.dart
+++ b/lib/src/syncbase/discovery_client.dart
@@ -17,10 +17,13 @@
/// Make this into the Dart Discovery client
/// https://github.com/vanadium/issues/issues/835
class DiscoveryClient {
- final Map<String, ProxyHandlePair<discovery.AdvertiserProxy>> advertisers =
- new Map<String, ProxyHandlePair<discovery.AdvertiserProxy>>();
- final Map<String, ProxyHandlePair<discovery.ScannerProxy>> scanners =
- new Map<String, ProxyHandlePair<discovery.ScannerProxy>>();
+ final Map<String,
+ Future<ProxyHandlePair<discovery.AdvertiserProxy>>> _advertisers =
+ new Map<String, Future<ProxyHandlePair<discovery.AdvertiserProxy>>>();
+ final Map<String, Future<ProxyHandlePair<discovery.ScannerProxy>>> _scanners =
+ new Map<String, Future<ProxyHandlePair<discovery.ScannerProxy>>>();
+ final Map<String, Future> _stoppingAdvertisers = new Map<String, Future>();
+ final Map<String, Future> _stoppingScanners = new Map<String, Future>();
static final String discoveryUrl = 'https://mojo2.v.io/discovery.mojo';
@@ -46,77 +49,132 @@
// Scans for this query and handles found/lost objects with the handler.
// Keeps track of this scanner via the key.
Future scan(String key, String query, discovery.ScanHandler handler) async {
- // Cancel the scan if one is already going for this key.
- if (scanners.containsKey(key)) {
- stopScan(key);
+ // Return the existing scan if one is already going.
+ if (_scanners.containsKey(key)) {
+ return _scanners[key];
}
- discovery.ScannerProxy s = new discovery.ScannerProxy.unbound();
+ Future _scanHelper() async {
+ discovery.ScannerProxy s = new discovery.ScannerProxy.unbound();
- print('Starting up discovery scanner ${key}. Looking for ${query}');
+ print('Starting up discovery scanner ${key}. Looking for ${query}');
- shell.connectToService(discoveryUrl, s);
+ shell.connectToService(discoveryUrl, s);
- // Use a ScanHandlerStub (Mojo-encodable interface) to wrap the scan handler.
- discovery.ScanHandlerStub shs = new discovery.ScanHandlerStub.unbound();
- shs.impl = handler;
+ // Use a ScanHandlerStub (Mojo-encodable interface) to wrap the scan handler.
+ discovery.ScanHandlerStub shs = new discovery.ScanHandlerStub.unbound();
+ shs.impl = handler;
- print('Scanning begins!');
- return s.ptr
- .scan(query, shs)
- .then((discovery.ScannerScanResponseParams response) {
- print(
- "${key} scanning started. The cancel handle is ${response.handle}.");
- scanners[key] =
- new ProxyHandlePair<discovery.ScannerProxy>(s, response.handle);
+ print('Scanning begins!');
+ return s.ptr
+ .scan(query, shs)
+ .then((discovery.ScannerScanResponseParams response) {
+ print(
+ "${key} scanning started. The cancel handle is ${response.handle}.");
+
+ return new ProxyHandlePair<discovery.ScannerProxy>(s, response.handle);
+ });
+ }
+
+ // Otherwise, set _scanners[key] and do the preparation inside the future
+ // so that stopScan can stop it if the two are called back to back.
+ _scanners[key] = _scanHelper();
+
+ return _scanners[key];
+ }
+
+ // This sends a stop signal to the scanner. Handles repeated stop calls on
+ // the same key by returning the same Future.
+ Future stopScan(String key) {
+ if (!_scanners.containsKey(key)) {
+ return new Future.value();
+ }
+ if (_stoppingScanners.containsKey(key)) {
+ return _stoppingScanners[key];
+ }
+
+ _stoppingScanners[key] = _stopScanHelper(key);
+
+ return _stoppingScanners[key].then((_) {
+ // Success! Let's clean up both _scanners and _stoppingScanners.
+ _scanners.remove(key);
+ _stoppingScanners.remove(key);
+ }).catchError((e) {
+ // Failure. We can only clean up _stoppingScanners.
+ _stoppingScanners.remove(key);
+ throw e;
});
}
- // This sends a stop signal to the scanner. Since it is non-blocking, the
- // scan handle may not stop instantaneously.
- void stopScan(String key) {
- if (scanners[key] != null) {
- print("Stopping scan for ${key}.");
- scanners[key].proxy.ptr.stop(scanners[key].handle);
- scanners[key].proxy.close(); // don't wait for this future.
- scanners.remove(key);
- }
+ Future _stopScanHelper(String key) async {
+ ProxyHandlePair<discovery.ScannerProxy> sp = await _scanners[key];
+ await sp.proxy.ptr.stop(sp.handle);
+ await sp.proxy.close();
+ print("Scan was stopped for ${key}!");
}
// Advertises the given service information. Keeps track of the advertiser
// handle via the key.
Future advertise(String key, discovery.Service serviceInfo,
{List<String> visibility}) async {
- // Cancel the advertisement if one is already going for this key.
- if (advertisers.containsKey(key)) {
- stopAdvertise(key);
+ // Return the existing advertisement if one is already going.
+ if (_advertisers.containsKey(key)) {
+ return _advertisers[key];
}
- discovery.AdvertiserProxy a = new discovery.AdvertiserProxy.unbound();
+ Future _advertiseHelper() async {
+ discovery.AdvertiserProxy a = new discovery.AdvertiserProxy.unbound();
- print(
- 'Starting up discovery advertiser ${key}. Broadcasting for ${serviceInfo.instanceName}');
-
- shell.connectToService(discoveryUrl, a);
-
- return a.ptr
- .advertise(serviceInfo, visibility ?? <String>[])
- .then((discovery.AdvertiserAdvertiseResponseParams response) {
print(
- "${key} advertising started. The cancel handle is ${response.handle}.");
- advertisers[key] =
- new ProxyHandlePair<discovery.AdvertiserProxy>(a, response.handle);
+ 'Starting up discovery advertiser ${key}. Broadcasting for ${serviceInfo.instanceName}');
+
+ shell.connectToService(discoveryUrl, a);
+
+ return a.ptr
+ .advertise(serviceInfo, visibility ?? <String>[])
+ .then((discovery.AdvertiserAdvertiseResponseParams response) {
+ print(
+ "${key} advertising started. The cancel handle is ${response.handle}.");
+
+ return new ProxyHandlePair<discovery.AdvertiserProxy>(
+ a, response.handle);
+ });
+ }
+
+ // Otherwise, set _advertisers[key] and do the preparation inside the future
+ // so that stopAdvertise can stop it if the two are called back to back.
+ _advertisers[key] = _advertiseHelper();
+
+ return _advertisers[key];
+ }
+
+ // This sends a stop signal to the advertiser. Handles repeated stop calls on
+ // the same key by returning the same Future.
+ Future stopAdvertise(String key) {
+ if (!_advertisers.containsKey(key)) {
+ return new Future.value();
+ }
+ if (_stoppingAdvertisers.containsKey(key)) {
+ return _stoppingAdvertisers[key];
+ }
+
+ _stoppingAdvertisers[key] = _stopAdvertiseHelper(key);
+
+ return _stoppingAdvertisers[key].then((_) {
+ // Success! Let's clean up both _advertisers and _stoppingAdvertisers.
+ _advertisers.remove(key);
+ _stoppingAdvertisers.remove(key);
+ }).catchError((e) {
+ // Failure. We can only clean up _stoppingAdvertisers.
+ _stoppingAdvertisers.remove(key);
+ throw e;
});
}
- // This sends a stop signal to the advertiser. Since it is non-blocking, the
- // advertise handle may not stop instantaneously.
- void stopAdvertise(String key) {
- if (advertisers[key] != null) {
- print("Stopping advertise for ${key}.");
- advertisers[key].proxy.ptr.stop(advertisers[key].handle);
- advertisers[key].proxy.close(); // don't wait for this future.
- advertisers.remove(key);
- }
+ Future _stopAdvertiseHelper(String key) async {
+ ProxyHandlePair<discovery.AdvertiserProxy> ap = await _advertisers[key];
+ await ap.proxy.ptr.stop(ap.handle);
+ await ap.proxy.close();
+ print("Advertise was stopped for ${key}!");
}
}
diff --git a/lib/src/syncbase/log_writer.dart b/lib/src/syncbase/log_writer.dart
index a588731..a146839 100644
--- a/lib/src/syncbase/log_writer.dart
+++ b/lib/src/syncbase/log_writer.dart
@@ -30,7 +30,7 @@
class LogWriter {
// This callback is called on each watch update, passing the key and value.
- final util.updateCallbackT updateCallback;
+ final util.keyValueCallback updateCallback;
// The users that we should look for when coming to a proposal consensus.
final List<int> users;
diff --git a/lib/src/syncbase/settings_manager.dart b/lib/src/syncbase/settings_manager.dart
index 66fccee..7c11939 100644
--- a/lib/src/syncbase/settings_manager.dart
+++ b/lib/src/syncbase/settings_manager.dart
@@ -27,9 +27,9 @@
import 'package:syncbase/syncbase_client.dart' as sc;
class SettingsManager {
- final util.updateCallbackT updateSettingsCallback;
- final util.updateCallbackT updateGamesCallback;
- final util.updateCallbackT updatePlayerFoundCallback;
+ final util.keyValueCallback updateSettingsCallback;
+ final util.keyValueCallback updateGamesCallback;
+ final util.keyValueCallback updatePlayerFoundCallback;
final CroupierClient _cc;
sc.SyncbaseTable tb;
@@ -59,8 +59,8 @@
tb = await _cc.createTable(db, util.tableNameSettings);
// Start to watch the stream for the shared settings table.
- Stream<sc.WatchChange> watchStream = db.watch(util.tableNameSettings,
- _settingsWatchSyncPrefix, UTF8.encode("now"));
+ Stream<sc.WatchChange> watchStream = db.watch(
+ util.tableNameSettings, _settingsWatchSyncPrefix, UTF8.encode("now"));
_startWatchSettings(watchStream); // Don't wait for this future.
_loadSettings(tb); // Don't wait for this future.
}
@@ -198,7 +198,7 @@
new logic_game.GameStartData(type, 0, gameID, id);
await _cc.createSyncgroup(
- _cc.makeSyncgroupName(util.syncgameSuffix(gsd.toJSONString())),
+ _cc.makeSyncgroupName(util.syncgameSuffix("${gsd.gameID}")),
util.tableNameGames,
prefix: util.syncgamePrefix(gameID));
@@ -250,30 +250,31 @@
Future scanSettings() async {
SettingsScanHandler ssh =
new SettingsScanHandler(_cc, this.updateGamesCallback);
- _cc.discoveryClient
- .scan(_discoverySettingsKey, 'v.InterfaceName="${util.discoveryInterfaceName}"', ssh);
+ return _cc.discoveryClient.scan(_discoverySettingsKey,
+ 'v.InterfaceName="${util.discoveryInterfaceName}"', ssh);
}
- void stopScanSettings() {
- _cc.discoveryClient.stopScan(_discoverySettingsKey);
+ Future stopScanSettings() {
+ return _cc.discoveryClient.stopScan(_discoverySettingsKey);
}
// Someone who wants to join a game should advertise their presence.
Future advertiseSettings(logic_game.GameStartData gsd) async {
String suffix = await _syncSuffix();
- String gameSuffix = util.syncgameSuffix(gsd.toJSONString());
- _cc.discoveryClient.advertise(
+ String gameSuffix = util.syncgameSuffix("${gsd.gameID}");
+ return _cc.discoveryClient.advertise(
_discoverySettingsKey,
DiscoveryClient.serviceMaker(
interfaceName: util.discoveryInterfaceName,
- addrs: <String>[
- _cc.makeSyncgroupName(suffix),
- _cc.makeSyncgroupName(gameSuffix)
- ]));
+ attrs: <String, String>{
+ util.syncgameSettingsAttr: _cc.makeSyncgroupName(suffix),
+ util.syncgameGameStartDataAttr: gsd.toJSONString()
+ },
+ addrs: <String>[_cc.makeSyncgroupName(gameSuffix)]));
}
- void stopAdvertiseSettings() {
- _cc.discoveryClient.stopAdvertise(_discoverySettingsKey);
+ Future stopAdvertiseSettings() {
+ return _cc.discoveryClient.stopAdvertise(_discoverySettingsKey);
}
Future<int> _getUserID() async {
@@ -306,7 +307,7 @@
CroupierClient _cc;
Map<String, String> settingsAddrs;
Map<String, String> gameAddrs;
- util.updateCallbackT updateGamesCallback;
+ util.keyValueCallback updateGamesCallback;
SettingsScanHandler(this._cc, this.updateGamesCallback) {
settingsAddrs = new Map<String, String>();
@@ -317,15 +318,15 @@
util.log(
"SettingsScanHandler Found ${s.instanceId} ${s.instanceName} ${s.addrs}");
- if (s.addrs.length == 2) {
- // Note: Assumes 2 addresses.
- settingsAddrs[s.instanceId] = s.addrs[0];
- gameAddrs[s.instanceId] = s.addrs[1];
+ if (s.addrs.length == 1 && s.attrs != null) {
+ // Note: Assumes 1 address and attributes for the game.
+ settingsAddrs[s.instanceId] = s.attrs[util.syncgameSettingsAttr];
+ gameAddrs[s.instanceId] = s.addrs[0];
- String json = _getPartFromBack(s.addrs[1], "-", 0);
- updateGamesCallback(s.addrs[1], json);
+ String gameSettingsJSON = s.attrs[util.syncgameGameStartDataAttr];
+ updateGamesCallback(gameAddrs[s.instanceId], gameSettingsJSON);
- _cc.joinSyncgroup(s.addrs[0]);
+ _cc.joinSyncgroup(settingsAddrs[s.instanceId]);
} else {
// An unexpected service was found. Who is advertising it?
// https://github.com/vanadium/issues/issues/846
diff --git a/lib/src/syncbase/util.dart b/lib/src/syncbase/util.dart
index 1853bbe..4bc8bb2 100644
--- a/lib/src/syncbase/util.dart
+++ b/lib/src/syncbase/util.dart
@@ -4,25 +4,25 @@
import 'package:syncbase/syncbase_client.dart' show Perms, SyncbaseClient;
-const appName = 'app';
-const dbName = 'db';
-const tableNameGames = 'games';
-const tableNameSettings = 'table_settings';
+const String appName = 'app';
+const String dbName = 'db';
+const String tableNameGames = 'games';
+const String tableNameSettings = 'table_settings';
// TODO(alexfandrianto): This may need to be the global mount table with a
// proxy. Otherwise, it will be difficult for other users to run.
// https://github.com/vanadium/issues/issues/782
-const mtAddr = '/192.168.86.254:8101';
-const sgPrefix = 'croupierAlex/%%sync';
-const sgSuffix = 'discovery';
-const sgSuffixGame = 'gaming';
+const String mtAddr = '/192.168.86.254:8101';
+const String sgPrefix = 'croupierAlex/%%sync';
+const String sgSuffix = 'discovery';
+const String sgSuffixGame = 'gaming';
-const discoveryInterfaceName = 'CroupierSettingsAndGame2';
+const String discoveryInterfaceName = 'CroupierSettingsAndGame2';
typedef void NoArgCb();
-typedef void updateCallbackT(String key, String value);
+typedef void keyValueCallback(String key, String value);
-String openPermsJson =
+const String openPermsJson =
'{"Admin":{"In":["..."]},"Write":{"In":["..."]},"Read":{"In":["..."]},"Resolve":{"In":["..."]},"Debug":{"In":["..."]}}';
Perms openPerms = SyncbaseClient.perms(openPermsJson);
@@ -40,3 +40,5 @@
return "${gameID}";
}
+const String syncgameSettingsAttr = "settings_sgname";
+const String syncgameGameStartDataAttr = "game_start_data";