-
Notifications
You must be signed in to change notification settings - Fork 923
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
Conversation
…se/firebase-js-sdk into wuandy/PortPublicShutdown
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.
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.
packages/firebase/index.d.ts
Outdated
INTERNAL: { delete: () => Promise<void> }; | ||
INTERNAL: { | ||
delete: () => Promise<void>; | ||
shutdown: () => Promise<void>; |
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.
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:
- Name the method with a leading underscore to indicate it's "private", e.g.
_shutdown()
- 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).
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.
Done.
return this.INTERNAL.delete(); | ||
}, | ||
|
||
isShutdown: (): boolean => { |
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 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
get app(): FirebaseApp { |
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.
Done.
}, | ||
|
||
isShutdown: (): boolean => { | ||
return this._firestoreClient!.clientShutdown; |
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 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
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.
Done.
*/ | ||
// TODO(b/135755126): make this public. | ||
shutdown: (): Promise<void> => { | ||
(this._config.firebaseApp as _FirebaseApp)._removeServiceInstance( |
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.app
?
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.
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 { |
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.
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.
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.
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. |
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 should probably be "If you have persistence enabled, the next time you start ..."
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.
Done. Although i might need to back port this.
op: () => Promise<T> | ||
): Promise<T> { | ||
this.verifyNotFailed(); | ||
// tslint:disable-next-line:no-floating-promises |
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 shouldn't be necessary.
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.
Done.
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.
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)); |
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.
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:
- Make this method
async
so you can useawait
and the compiler will automatically make all code paths returnPromise
(i.e. it'll generate aPromise.resolve(undefined)
for the no-op case. - 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).
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.
Thanks..much better!
const doStep = (n: number): Promise<number> => | ||
defer(() => { | ||
const result = completedSteps.push(n); | ||
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.
Since you're not using the result, you could drop this return and change this to return Promise<void>
.
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.
Done.
// auth tokens. | ||
this.credentials.removeChangeListener(); | ||
this._clientShutdown = true; | ||
return this.asyncQueue.enqueueAndInitilizeShutdown(async () => { |
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.
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.
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.
Actually, Initialize
=> Initiate
to match the other ports.
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.
Done.
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.
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 () => { |
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.
Actually, Initialize
=> Initiate
to match the other ports.
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. :) |
Well, the thing that we want is in a few parts:
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. |
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.
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.
packages/firebase/index.d.ts
Outdated
INTERNAL: { delete: () => Promise<void> }; | ||
INTERNAL: { | ||
delete: () => Promise<void>; | ||
shutdown: () => Promise<void>; |
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.
Done.
* Shuts down this Firestore instance. | ||
* | ||
* After shutdown only the `clearPersistence()` method may be used. Any other method | ||
* will throw an `FirestoreError`. |
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.
Done.
* will throw an `FirestoreError`. | ||
* | ||
* To restart after shutdown, simply create a new instance of FirebaseFirestore with | ||
* `Firebase.firestore()`. |
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.
Done.
*/ | ||
// TODO(b/135755126): make this public. | ||
shutdown: (): Promise<void> => { | ||
(this._config.firebaseApp as _FirebaseApp)._removeServiceInstance( |
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.
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. |
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.
Done. Although i might need to back port this.
// auth tokens. | ||
this.credentials.removeChangeListener(); | ||
this._clientShutdown = true; | ||
return this.asyncQueue.enqueueAndInitilizeShutdown(async () => { |
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.
Done.
const doStep = (n: number): Promise<number> => | ||
defer(() => { | ||
const result = completedSteps.push(n); | ||
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.
Done.
op: () => Promise<T> | ||
): Promise<T> { | ||
this.verifyNotFailed(); | ||
// tslint:disable-next-line:no-floating-promises |
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.
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 { |
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.
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)); |
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.
Thanks..much better!
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.
Couple tiny things left...
@@ -447,6 +447,39 @@ export class Firestore implements firestore.FirebaseFirestore, FirebaseService { | |||
return deferred.promise; | |||
} | |||
|
|||
/** | |||
* Shuts down this Firestore instance. |
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.
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(); |
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.
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
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.
Done.
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.
LGTM
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.
LGTM for the _removeServiceInstance()
piece
No description provided.