-
Notifications
You must be signed in to change notification settings - Fork 306
store: Have UpdateMachine.load check account still exists after async gaps #1386
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
Changes from all commits
228c9bd
0a2273c
7075045
dc1c806
44bdbd1
795b46e
874e3bf
bd8554a
ef8664d
0922ca5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -147,13 +147,17 @@ abstract class GlobalStore extends ChangeNotifier { | |
// It's up to us. Start loading. | ||
future = loadPerAccount(accountId); | ||
_perAccountStoresLoading[accountId] = future; | ||
store = await future; | ||
_setPerAccount(accountId, store); | ||
unawaited(_perAccountStoresLoading.remove(accountId)); | ||
return store; | ||
try { | ||
store = await future; | ||
_setPerAccount(accountId, store); | ||
return store; | ||
} finally { | ||
unawaited(_perAccountStoresLoading.remove(accountId)); | ||
} | ||
} | ||
|
||
Future<void> _reloadPerAccount(int accountId) async { | ||
assert(_accounts.containsKey(accountId)); | ||
assert(_perAccountStores.containsKey(accountId)); | ||
assert(!_perAccountStoresLoading.containsKey(accountId)); | ||
final store = await loadPerAccount(accountId); | ||
|
@@ -169,6 +173,11 @@ abstract class GlobalStore extends ChangeNotifier { | |
|
||
/// Load per-account data for the given account, unconditionally. | ||
/// | ||
/// The account for `accountId` must exist. | ||
/// | ||
/// Throws [AccountNotFoundException] if after any async gap | ||
/// the account has been removed. | ||
/// | ||
/// This method should be called only by the implementation of [perAccount]. | ||
/// Other callers interested in per-account data should use [perAccount] | ||
/// and/or [perAccountSync]. | ||
|
@@ -183,36 +192,30 @@ abstract class GlobalStore extends ChangeNotifier { | |
// The API key is invalid and the store can never be loaded | ||
// unless the user retries manually. | ||
final account = getAccount(accountId); | ||
if (account == null) { | ||
// The account was logged out during `await doLoadPerAccount`. | ||
// Here, that seems possible only by the user's own action; | ||
// the logout can't have been done programmatically. | ||
// Even if it were, it would have come with its own UI feedback. | ||
// Anyway, skip showing feedback, to not be confusing or repetitive. | ||
throw AccountNotFoundException(); | ||
} | ||
assert(account != null); // doLoadPerAccount would have thrown AccountNotFoundException | ||
final zulipLocalizations = GlobalLocalizations.zulipLocalizations; | ||
reportErrorToUserModally( | ||
zulipLocalizations.errorCouldNotConnectTitle, | ||
message: zulipLocalizations.errorInvalidApiKeyMessage( | ||
account.realmUrl.toString())); | ||
account!.realmUrl.toString())); | ||
await logOutAccount(this, accountId); | ||
throw AccountNotFoundException(); | ||
default: | ||
rethrow; | ||
} | ||
} | ||
if (!_accounts.containsKey(accountId)) { | ||
// TODO(#1354): handle this earlier | ||
// [removeAccount] was called during [doLoadPerAccount]. | ||
store.dispose(); | ||
throw AccountNotFoundException(); | ||
} | ||
// doLoadPerAccount would have thrown AccountNotFoundException | ||
assert(_accounts.containsKey(accountId)); | ||
return store; | ||
} | ||
|
||
/// Load per-account data for the given account, unconditionally. | ||
/// | ||
/// The account for `accountId` must exist. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Making this explicit in dartdocs seems useful; I think this also applies to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking about the validity of the I vaguely feel that we can make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I think I wouldn't be satisfied with an approach like that. Within some synchronous code, say point A is where we retrieve the Account data (
This wouldn't answer the need to interrupt the retry loop in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bumping #1386 (comment); I guess we still want to mention the constraint to its dartdoc? Thanks for the explanation. I think |
||
/// | ||
/// Throws [AccountNotFoundException] if after any async gap | ||
/// the account has been removed. | ||
/// | ||
/// This method should be called only by [loadPerAccount]. | ||
Future<PerAccountStore> doLoadPerAccount(int accountId); | ||
|
||
|
@@ -263,6 +266,8 @@ abstract class GlobalStore extends ChangeNotifier { | |
Future<void> doUpdateAccount(int accountId, AccountsCompanion data); | ||
|
||
/// Remove an account from the store. | ||
/// | ||
/// The account for `accountId` must exist. | ||
Future<void> removeAccount(int accountId) async { | ||
assert(_accounts.containsKey(accountId)); | ||
await doRemoveAccount(accountId); | ||
|
@@ -941,15 +946,31 @@ class UpdateMachine { | |
store.updateMachine = this; | ||
} | ||
|
||
/// Load the user's data from the server, and start an event queue going. | ||
/// Load data for the given account from the server, | ||
/// and start an event queue going. | ||
/// | ||
/// The account for `accountId` must exist. | ||
/// | ||
/// Throws [AccountNotFoundException] if after any async gap | ||
/// the account has been removed. | ||
/// | ||
/// In the future this might load an old snapshot from local storage first. | ||
static Future<UpdateMachine> load(GlobalStore globalStore, int accountId) async { | ||
Account account = globalStore.getAccount(accountId)!; | ||
final connection = globalStore.apiConnectionFromAccount(account); | ||
|
||
void stopAndThrowIfNoAccount() { | ||
final account = globalStore.getAccount(accountId); | ||
if (account == null) { | ||
assert(debugLog('Account logged out during UpdateMachine.load')); | ||
connection.close(); | ||
throw AccountNotFoundException(); | ||
} | ||
} | ||
|
||
final stopwatch = Stopwatch()..start(); | ||
final initialSnapshot = await _registerQueueWithRetry(connection); | ||
final initialSnapshot = await _registerQueueWithRetry(connection, | ||
stopAndThrowIfNoAccount: stopAndThrowIfNoAccount); | ||
final t = (stopwatch..stop()).elapsed; | ||
assert(debugLog("initial fetch time: ${t.inMilliseconds}ms")); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Further down there is another async gap at There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe there isn't a bug that we could catch by doing so.
|
||
|
@@ -991,13 +1012,20 @@ class UpdateMachine { | |
|
||
bool _disposed = false; | ||
|
||
/// Make the register-queue request, with retries. | ||
/// | ||
/// After each async gap, calls [stopAndThrowIfNoAccount]. | ||
static Future<InitialSnapshot> _registerQueueWithRetry( | ||
ApiConnection connection) async { | ||
ApiConnection connection, { | ||
required void Function() stopAndThrowIfNoAccount, | ||
}) async { | ||
BackoffMachine? backoffMachine; | ||
while (true) { | ||
InitialSnapshot? result; | ||
try { | ||
return await registerQueue(connection); | ||
result = await registerQueue(connection); | ||
} catch (e, s) { | ||
stopAndThrowIfNoAccount(); | ||
// TODO(#890): tell user if initial-fetch errors persist, or look non-transient | ||
switch (e) { | ||
case HttpException(httpStatus: 401): | ||
|
@@ -1012,8 +1040,13 @@ class UpdateMachine { | |
} | ||
assert(debugLog('Backing off, then will retry…')); | ||
await (backoffMachine ??= BackoffMachine()).wait(); | ||
stopAndThrowIfNoAccount(); | ||
assert(debugLog('… Backoff wait complete, retrying initial fetch.')); | ||
} | ||
if (result != null) { | ||
stopAndThrowIfNoAccount(); | ||
return result; | ||
} | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(This is fine to add, though.)