Skip to content
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

Closing the write and the watch stream after 60s of idleness #275

Merged
merged 35 commits into from
Nov 4, 2017

Conversation

schmidt-sebastian
Copy link
Contributor

This is a port of firebase/firebase-ios-sdk#388

@@ -23,6 +23,9 @@ import firebase from './firebase_export';
import { EmptyCredentialsProvider } from '../../../src/api/credentials';
import { PlatformSupport } from '../../../src/platform/platform';
import { AsyncQueue } from '../../../src/util/async_queue';
import { Firestore, FirestoreDatabase } from '../../../src/api/database';
import { FirebaseApp } from '../../../../app/index';
Copy link
Contributor

@jshcrowthe jshcrowthe Oct 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will break, if you are trying to reference other modules you need to do it via the module names

i.e.

import { FirebaseApp } from '@firebase/app';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Thanks for debugging this!

Copy link
Contributor

@wilhuff wilhuff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly LGTM

@@ -455,6 +455,11 @@ export class Firestore implements firestore.Firestore, FirebaseService {
);
}
}

/** Creates a new AsyncQueue. Can be overwritten to provide a custom queue. */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/overwritten/overridden/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

* or null if empty.
* @param afterBatchId If provided, the batch to search after.
* @returns The next mutation or null if there wasn't one.
* or null if empty.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for fixing these things up.

return this.handleIdleCloseTimer();
}, IDLE_TIMEOUT_MS)
.catch((err: FirestoreError) => {
// We ignore Promise rejections for cancelled idle checks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment doesn't line up with the actual behavior. We don't ignore the rejection: we assert that it can only be of one type. A better comment here would articulate why we're able to make this assertion, perhaps because this can't fail except via the timeout and that's guaranteed to have Code.CANCELLED?

Copy link
Contributor Author

@schmidt-sebastian schmidt-sebastian Oct 30, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I added this is that running the tests with this spewed a bunch of warnings in Chrome about unrejected Promises that were not handled. I made this comment better.

/**
* Closes the stream and cleans up as necessary:
*
* <ul>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Typescript comments aren't thinly veiled HTML the way Javadoc comments are. Do this as a markdown list, like we did in mutation.ts.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adjusted. Thanks for pointing this out.

error?: FirestoreError
): Promise<void> {
assert(
finalState == PersistentStreamState.Error || isNullOrUndefined(error),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As in the iOS PR, this allows the error to be nil if the finalState is FSTStreamStateError. Is that desirable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets invoked by 'handleStreamClose' (the GRPC callback handler) and making this stricter would require that the server only ever closes a stream with an error.

const deferred = new Deferred<T>();
setTimeout(() => {
let handle = window.setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why window.setTimeout() instead of the global? This is incompatible with node without extra hacks to define window.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setTimeout() is defined to (perhaps incorrectly) return a Timer. See microsoft/TypeScript#842 (comment)

const deferred = new Deferred<T>();
setTimeout(() => {
let handle = window.setTimeout(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this can be const handle instead of let handle

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

this.delayedOperations.forEach((deferred, handle) => {
window.clearTimeout(handle);
deferred.reject(
new FirestoreError(Code.CANCELLED, 'Cancelled during queue drain')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These failures are potentially user visible so we need a better message about what can cause these to happen and how to avoid them if that's a problem.

If draining only happens during shutdown, the message can reflect that: 'Operation cancelled by shutdown'.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used in tests, but I could see that we may want to call this during shutdown as well.

Copy link
Contributor

@wilhuff wilhuff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM mod resolution of travis failures

Copy link
Contributor

@jshcrowthe jshcrowthe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the package-lock.json we have the top level yarn.lock that serves the same purpose.

@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt-closewhenidle branch 3 times, most recently from cf8f359 to 15c781c Compare November 1, 2017 18:30
@schmidt-sebastian
Copy link
Contributor Author

@wilhuff I fixed the failing integration tests. The types we referred to in the tests ended up being minified, while the Public API that we checked against was not. I migrated the test to use the internal representation of the Public API. You can see a test run here:
https://gist.github.com/schmidt-sebastian/18116a7c92ffbfdf1d535720850e63e8

schmidt-sebastian and others added 11 commits November 3, 2017 14:10
…ment (#279)

* Adds a demo app to help test and facilitate Firebase Auth API development.
Fixes verifyAssertion unrecoverable errors when returnIdpCredential is set to true.
In this case, the error code is returned along with the credential in the errorMessage without any
ID token/refresh token.

Catch, suppress/handle when localStorage is null or when `localStorage.getItem`
throws a security error due to access being disabled by the browser for whatever reason.
@googlebot
Copy link

So there's good news and bad news.

👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there.

😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request.

Note to project maintainer: This is a terminal state, meaning the cla/google commit status will not change from this State. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@schmidt-sebastian
Copy link
Contributor Author

@mikelehen I addressed your feedback.

The drain() method in the AsyncQueue now optionally allows for immediate execution of delayed tasks. Furthermore, I have exposed this queue via firestore.INTERNAL. The tests can now access the public Firestore client and no longer need to rely on an internal client that exposes the queue.

@schmidt-sebastian schmidt-sebastian force-pushed the mrschmidt-closewhenidle branch 2 times, most recently from b209f80 to 84c6938 Compare November 3, 2017 23:30
// may also contain entries that have already been run (in which case `handle` is
// null).
//
// tslint:disable-next-line:no-any Accept any type of delayed operation.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use AnyJs instead of "any" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

this.delayedOpCount--; // decrement once it's actually queued.
this.delayedOperationsCount--;
if (this.delayedOperationsCount > 0) {
delayedOp.handle = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit but I'm not a fan of this sort of optimization. It saves an assignment in some cases, but reads much less naturally (to me at least). I'd just do:

delayedOp.handle = null;
if (this.delayedOperationsCount === 0) {
  this.delayedOperations = [];
}

If you insist on optimizing this sort of thing, you should go ahead and optimize away the decrement too!

if (this.delayedOperationsCount !== 1) {
  this.delayedOperationsCount--;
  delayedOp.handle = null;
} else {
  this.delayedOperations = [];
}

(please don't actually do that :-))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't meant to be the newest and greatest performance optimization. Rather, it just felt naturally to do this at the time. Your version is a bit more concise, so I went with that. Thanks.

const deferred = new Deferred<T>();
setTimeout(() => {
this.delayedOperationsCount++;
const nextIndex = this.delayedOperations.length;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can move this down and do:

const delayedOpIndex = this.delayedOperations.push(delayedOp);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually need the index anymore and removed it altogether.

return this.schedule(() => Promise.resolve(undefined));
/**
* Waits until all currently scheduled tasks are finished executing. Tasks
* schedule with a delay can be rejected or queued for immediate execution.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

schedule => scheduled

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -374,7 +375,8 @@ export class Firestore implements firestore.Firestore, FirebaseService {
},
// Exposed via INTERNAL for use in tests.
disableNetwork: () => this._firestoreClient.disableNetwork(),
enableNetwork: () => this._firestoreClient.enableNetwork()
enableNetwork: () => this._firestoreClient.enableNetwork(),
queue: this._queue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a big fan of exposing the whole queue externally since:

  1. Although not documented or supported, this is now exposed to consumers of the SDK and could be abused.
  2. The only reason this works is because our minification sucks right now and we only minify things that begin or end with _. Really, the entire AsyncQueue class should be minified and all of the methods should have obfuscated names.
  3. In general, I'd rather we make the testable API explicit and minimal. Exposing more than we need to leads to a slippery slope heading towards undesirable tight coupling between tests and implementation.

So I'd recommend exposing drainAsyncQueue() or similar (FWIW if you wanted you could make drain() always execute delayed tasks; I don't think it needs to be optional) and either remove the delayedOperationsCount assertions from tests or have the exposed method return the count of operations that were drained or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with drainAsyncTasks. In the integration tests that use Firestore, I can indeed always execute the delayed tasks. I can't do this in the spec tests (since that always ends up closing the stream and confuses the tests).

The stream tests use Datastore directly and I allowed the queue to be passed in. That way, I can verify that I have at least one pending task.

Copy link
Contributor

@mikelehen mikelehen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Thanks!

@schmidt-sebastian schmidt-sebastian merged commit d43d461 into master Nov 4, 2017
@schmidt-sebastian schmidt-sebastian deleted the mrschmidt-closewhenidle branch November 4, 2017 20:38
@firebase firebase locked and limited conversation to collaborators Oct 25, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants