Skip to content

port public shutdown to web sdk. #2045

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
12 commits merged into from
Aug 7, 2019
Merged

port public shutdown to web sdk. #2045

12 commits merged into from
Aug 7, 2019

Conversation

ghost
Copy link

@ghost ghost commented Aug 1, 2019

No description provided.

@ghost ghost added the api: firestore label Aug 1, 2019
@ghost ghost requested a review from mikelehen August 1, 2019 23:51
@ghost ghost requested review from Feiyang1, hiranya911 and hsubox76 as code owners August 1, 2019 23:51
@ghost ghost assigned mikelehen and Feiyang1 Aug 2, 2019
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.

Mostly looks good from a porting perspective. I flagged a few nits.

That said, (and I'm sorry for providing this feedback now when it's pretty late) I'm not super psyched about how much this client shutdown feature is bleeding into the AsyncQueue. We now have the following on the AsyncQueue:

  • isShuttingDown which indicates the client is shutdown / shutting down (but the AsyncQueue is still functional as long as you call the right version of its methods)`
  • enqueueEvenAfterShutdown / enqueueAndForgetEvenAfterShutdown - Same as the normal enqueue functions except they don't no-op once the queue is shutdown.
  • enqueueAndInitializeShutdown() same as the normal enqueue functions except it also sets the shutdown flag.
  • enqueue() goes into a special no-op mode (returns Promises that don't resolve) once the queue is shutting down.

This is more churn on AsyncQueue than I would have expected and it feels like AsyncQueue is being contorted to meet the client's shutdown needs in a way that doesn't feel 100% natural to me.

I find myself wondering why the AsyncQueue needs to be intimately aware of client shutdown instead of the client just shutting down its components and entering a mode where it no longer queues new work on the queue? Did we deem this harder to implement? Less safe? Some combination? It seems like it would be cleaner to me, but if you / @wilhuff / @schmidt-sebastian have explored this sufficiently already, I'm okay just moving forward rather than revisiting.

INTERNAL: { delete: () => Promise<void> };
INTERNAL: {
delete: () => Promise<void>;
shutdown: () => Promise<void>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess you are putting these here so as not to publicly expose them? That isn't completely crazy, but in the past what we've done is:

  1. Name the method with a leading underscore to indicate it's "private", e.g. _shutdown()
  2. not added it to our .d.ts files so that folks using TypeScript won't be able to use it (unless they cast to any or something).

E.g. that's what Brian did for clearPersistence(). here's the change where we finally made it public for real: 7a15e7e

INTERNAL is currently meant for internal APIs meant for FirebaseApp to call (e.g. FirebaseApp will call our delete() method when somebody calls FirebaseApp.delete() but we don't want users to call our delete() method directly).

Copy link
Author

Choose a reason for hiding this comment

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

Done.

return this.INTERNAL.delete();
},

isShutdown: (): boolean => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be a readonly property instead of a method... which may be hard/impossible to do since it's on INTERNAL which isn't a class. If you move it to Firestore (where it will eventually live), it should look something like

Copy link
Author

Choose a reason for hiding this comment

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

Done.

},

isShutdown: (): boolean => {
return this._firestoreClient!.clientShutdown;
Copy link
Contributor

Choose a reason for hiding this comment

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

This will probably fail if you do firebase.firestore().INTERNAL.isShutdown(). since _firestoreClient doesn't get initialized until you call a method on the Firestore instance. So this method probably needs to either call this.ensureClientConfigured(); to initialize _firestoreClient or else tolerate an uninitialized _firestoreClient

Copy link
Author

Choose a reason for hiding this comment

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

Done.

*/
// TODO(b/135755126): make this public.
shutdown: (): Promise<void> => {
(this._config.firebaseApp as _FirebaseApp)._removeServiceInstance(
Copy link
Contributor

Choose a reason for hiding this comment

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

this.app ?

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@@ -199,6 +203,12 @@ export class AsyncQueue {
// assertion sanity-checks.
private operationInProgress = false;

// Is this AsyncQueue being shut down? If true, this instance will not enqueue
// any new operations, Promises from enqueue requests will not resolve.
get isShuttingDown(): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

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

I assume this name was decided in previous ports, in which case, feel free to just leave it. But I would find "isShutdown" clearer than "isShuttingDown" since the latter sounds like a very transitive state during the actual shutdown process itself, rather than a permanent state.

Copy link
Author

Choose a reason for hiding this comment

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

Yeah, this is from other ports. The reason is async queue is not actually shutdown..so i picked this name originally, it is leaking the details a bit. But async queue is an internal class used frequently, maybe it's not too bad to indicate how it is implemented on the name.

FirestoreClient has clientShutdown instead, because that is one abstraction up.

*
* Shutdown does not cancel any pending writes and any promises that are awaiting a response
* from the server will not be resolved. The next time you start this instance,
* it will resume attempting to send these writes to the server.
Copy link
Contributor

Choose a reason for hiding this comment

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

This should probably be "If you have persistence enabled, the next time you start ..."

Copy link
Author

Choose a reason for hiding this comment

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

Done. Although i might need to back port this.

op: () => Promise<T>
): Promise<T> {
this.verifyNotFailed();
// tslint:disable-next-line:no-floating-promises
Copy link
Contributor

Choose a reason for hiding this comment

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

this shouldn't be necessary.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

Copy link
Author

Choose a reason for hiding this comment

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

Actually yarn lint complains about this.

this.verifyNotFailed();
if (this._isShuttingDown) {
// Return a Promise resolves right away if it is already shutdown.
return new Promise<T>(resolve => resolve(undefined));
Copy link
Contributor

Choose a reason for hiding this comment

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

You would normally use Promise.resolve() for this... but I see that gives a type error because undefined isn't convertible to T, which is a valid complaint. So I think if this is the behavior we want, the right thing to do here is return Promise.resolve(undefined as T) but we are subverting the type system (T likely isn't supposed to include undefined).

But as it happens, the only code we pass to enqueueAndInitilizeShutdown() returns Promise<void> so we should probably just make this method non-generic, which solves our problems. Finally, you could:

  1. Make this method async so you can use await and the compiler will automatically make all code paths return Promise (i.e. it'll generate a Promise.resolve(undefined) for the no-op case.
  2. Use your enqueueEvenAfterShutdown() so you don't need to do the enqueueInternal() / isShuttingDown=true reverse ordering trick.

So you end up with:

  async enqueueAndInitilizeShutdown(op: () => Promise<void>): Promise<void> {
    this.verifyNotFailed();
    if (!this._isShuttingDown) {
      this._isShuttingDown = true;
      await this.enqueueEvenAfterShutdown(op);
    }
  }

This will cause an error in your async_queue.test.ts test which you can fix by changing:

    queue.enqueueAndInitilizeShutdown(() => doStep(2));

to:

    queue.enqueueAndInitilizeShutdown(async () => { doStep(2); });

or you could just change doStop() to return Promise<void> (as I mention again below).

Copy link
Author

Choose a reason for hiding this comment

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

Thanks..much better!

const doStep = (n: number): Promise<number> =>
defer(() => {
const result = completedSteps.push(n);
return result;
Copy link
Contributor

Choose a reason for hiding this comment

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

Since you're not using the result, you could drop this return and change this to return Promise<void>.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

// auth tokens.
this.credentials.removeChangeListener();
this._clientShutdown = true;
return this.asyncQueue.enqueueAndInitilizeShutdown(async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Initilize => Initialize

FWIW, If you are using VS Code (if not, disregard), there's a "Code Spell Checker" extension from "Street Side Software" which is handy.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, Initialize => Initiate to match the other ports.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@mikelehen mikelehen assigned ghost and unassigned mikelehen Aug 5, 2019
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.

The high level concern you're raising here is a valid one but the contortions come from the fact that we want clearPersistence to be able to run after shutdown. You're welcome to propose an alternative, but I think at this point you have to help port it :-).

// auth tokens.
this.credentials.removeChangeListener();
this._clientShutdown = true;
return this.asyncQueue.enqueueAndInitilizeShutdown(async () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, Initialize => Initiate to match the other ports.

@mikelehen
Copy link
Contributor

Fair enough. My naive alternative is basically "Don't shut down the queue" since it's not actually shutting down and we still actively need it (for clearPersistence())". But I don't know what breaks if you don't shut it down, and it's probably not worth re-exploring this, so I'll defer my criticism until a point in time where I feel motivated to explore it myself. :)

@wilhuff
Copy link
Contributor

wilhuff commented Aug 5, 2019

Well, the thing that we want is in a few parts:

  • clearPersistence should be able to run after shutdown
  • shutdown should be able to run after shutdown (and have no effect)
  • anything else should fail
  • callbacks submitted to other APIs should not fail.

So far we've chosen to push the is shutdown check down into the async queue, but another way to go would be to push it up into FirestoreClient.

Copy link
Author

@ghost ghost left a comment

Choose a reason for hiding this comment

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

I actually started the task by doing the check in FirestoreClient, but i could not be sure that is enough to stop all activities going on with the SDK, it might be true but my glossing over code base does not seem to support it. I believe there will be activities going on after shut down, and i am not sure if we can tolerate those or not.

I decided to go into async queue and make the change there to be certain that we can stop all activities in a more controlled manner.

INTERNAL: { delete: () => Promise<void> };
INTERNAL: {
delete: () => Promise<void>;
shutdown: () => Promise<void>;
Copy link
Author

Choose a reason for hiding this comment

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

Done.

* Shuts down this Firestore instance.
*
* After shutdown only the `clearPersistence()` method may be used. Any other method
* will throw an `FirestoreError`.
Copy link
Author

Choose a reason for hiding this comment

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

Done.

* will throw an `FirestoreError`.
*
* To restart after shutdown, simply create a new instance of FirebaseFirestore with
* `Firebase.firestore()`.
Copy link
Author

Choose a reason for hiding this comment

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

Done.

*/
// TODO(b/135755126): make this public.
shutdown: (): Promise<void> => {
(this._config.firebaseApp as _FirebaseApp)._removeServiceInstance(
Copy link
Author

Choose a reason for hiding this comment

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

Done.

*
* Shutdown does not cancel any pending writes and any promises that are awaiting a response
* from the server will not be resolved. The next time you start this instance,
* it will resume attempting to send these writes to the server.
Copy link
Author

Choose a reason for hiding this comment

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

Done. Although i might need to back port this.

// auth tokens.
this.credentials.removeChangeListener();
this._clientShutdown = true;
return this.asyncQueue.enqueueAndInitilizeShutdown(async () => {
Copy link
Author

Choose a reason for hiding this comment

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

Done.

const doStep = (n: number): Promise<number> =>
defer(() => {
const result = completedSteps.push(n);
return result;
Copy link
Author

Choose a reason for hiding this comment

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

Done.

op: () => Promise<T>
): Promise<T> {
this.verifyNotFailed();
// tslint:disable-next-line:no-floating-promises
Copy link
Author

Choose a reason for hiding this comment

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

Done.

@@ -199,6 +203,12 @@ export class AsyncQueue {
// assertion sanity-checks.
private operationInProgress = false;

// Is this AsyncQueue being shut down? If true, this instance will not enqueue
// any new operations, Promises from enqueue requests will not resolve.
get isShuttingDown(): boolean {
Copy link
Author

Choose a reason for hiding this comment

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

Yeah, this is from other ports. The reason is async queue is not actually shutdown..so i picked this name originally, it is leaking the details a bit. But async queue is an internal class used frequently, maybe it's not too bad to indicate how it is implemented on the name.

FirestoreClient has clientShutdown instead, because that is one abstraction up.

this.verifyNotFailed();
if (this._isShuttingDown) {
// Return a Promise resolves right away if it is already shutdown.
return new Promise<T>(resolve => resolve(undefined));
Copy link
Author

Choose a reason for hiding this comment

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

Thanks..much better!

@ghost ghost assigned mikelehen and unassigned ghost Aug 6, 2019
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.

Couple tiny things left...

@@ -447,6 +447,39 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService {
return deferred.promise;
}

/**
* Shuts down this Firestore instance.
Copy link
Contributor

Choose a reason for hiding this comment

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

leading spaces seems off... If you run yarn lint from packages/firestore, it should complain about this (and it's complaining in the CI build too).

@@ -1073,7 +1073,7 @@ apiDescribe('Database', (persistence: boolean) => {
it('can start a new instance after shut down', async () => {
return withTestDoc(persistence, async docRef => {
const firestore = docRef.firestore;
await firestore.INTERNAL.shutdown();
await (firestore as any)._shutdown();
Copy link
Contributor

Choose a reason for hiding this comment

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

any is disallowed by the linter (again, running yarn lint from packages/firestore will complain). So you'll need a lint suppression. I'd recommend trying not to repeat it over and over. So you could create a helper like Brian did for clearPersistence(): 7a15e7e#diff-1422c3d56f022a40f00b40d264351ffdL109

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@mikelehen mikelehen assigned ghost and unassigned mikelehen Aug 6, 2019
@ghost ghost assigned mikelehen and unassigned ghost Aug 6, 2019
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.

LGTM

@mikelehen mikelehen assigned ghost and unassigned mikelehen Aug 6, 2019
Copy link
Member

@Feiyang1 Feiyang1 left a comment

Choose a reason for hiding this comment

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

LGTM for the _removeServiceInstance() piece

@ghost ghost merged commit 80d0846 into master Aug 7, 2019
@firebase firebase locked and limited conversation to collaborators Oct 8, 2019
This pull request was closed.
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.

4 participants