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

grpc-js: waitForReady called multiple times #1352

Closed
badsyntax opened this issue Apr 12, 2020 · 6 comments · Fixed by #1368
Closed

grpc-js: waitForReady called multiple times #1352

badsyntax opened this issue Apr 12, 2020 · 6 comments · Fixed by #1368
Assignees

Comments

@badsyntax
Copy link
Contributor

badsyntax commented Apr 12, 2020

Problem description

When using client.waitForReady, the ready callback is called multiple times (3 times in this case). Is this is the expected behaviour? I expected it to be called only once. Perhaps I'm misunderstanding the API.

Reproduction steps

I'm constructing the gRPC client (which extends from Client.ts).

import * as grpc from '@grpc/grpc-js';
import { GradleTasksClient as GrpcClient } from './java-gradle-tasks-grpc/src/main/proto/gradle_tasks_grpc_pb';

const grpcClient = new GrpcClient(
  'localhost:8887',
  grpc.credentials.createInsecure()
);
const deadline = new Date();
deadline.setSeconds(deadline.getSeconds() + 1);
grpcClient.waitForReady(deadline, (err: Error | undefined) => {
  console.log('is ready');
});

Environment

  • OS name, version and architecture: macos Catalina 10.15.3 x86_64
  • Node version: v12.16.1
  • Node installation method: nvm
  • Package name and version: @grpc/grpc-js@0.7.9

Screencast

grpc-js-waitforready

@murgatroid99
Copy link
Member

That does look like a bug, but I don't know what could be causing that to happen.

@murgatroid99 murgatroid99 self-assigned this Apr 13, 2020
@murgatroid99
Copy link
Member

In your screencast, you are running that code in a function connectToServer. Are you sure you're not just calling that function multiple times?

@badsyntax
Copy link
Contributor Author

badsyntax commented Apr 17, 2020

Hi. Yes i've checked, that method is called only once, and the ready callback is called 3 times.

I'm using a pretty standard gRPC Java server on the backend.

I've done some digging and it looks like there's some recursion happening which results in multiple watcherObjects being queued which results in the waitForReady callback being called multiple times.

It starts here: (note the callback (checkState) will re-run this code when the state changes - this is the recursion):

within client.waitForReady:

this[CHANNEL_SYMBOL].watchConnectivityState(
newState,
deadline,
checkState
);

channel.watchConnectivityState will queue the first watcher:

const watcherObject = {
currentState,
callback,
timer: setTimeout(() => {
this.removeConnectivityStateWatcher(watcherObject);
callback(
new Error('Deadline passed without connectivity state change')
);
}, deadlineDate.getTime() - now.getTime()),
};
this.connectivityStateWatchers.push(watcherObject);

Now when channel.updateState is called, the watcher callback method is called, see:

for (const watcherObject of watchersCopy) {
if (newState !== watcherObject.currentState) {
watcherObject.callback();
clearTimeout(watcherObject.timer);
this.removeConnectivityStateWatcher(watcherObject);
}
}

watcherObject.callback is the original checkState callback defined in client.waitForReady, so now we're back to the beginning, and another watcher object is queued.

So I think the fix is to remove the watcher before calling the callback.

So change this code:

watcherObject.callback();
clearTimeout(watcherObject.timer);
this.removeConnectivityStateWatcher(watcherObject);

To this:

        clearTimeout(watcherObject.timer);
        this.removeConnectivityStateWatcher(watcherObject);
        watcherObject.callback();

I've tested this change and seems to work, the waitForReady callback is only called once, but I'm really not familiar with this code and I could be completely misunderstanding all of this. Also not sure the side-effects of making this change.

@badsyntax badsyntax reopened this Apr 17, 2020
@murgatroid99
Copy link
Member

I think I do see a situation that could cause what you're seeing: if the state transition that triggers the updateState call is a transition to the IDLE state, the getConnectivity state in the callback will synchronously kick the channel out of the idle state, calling updateState to be called again, so the watcherObject's callback will be called again before it is removed, increasing the number of eventual watchConnectivityState callbacks.

I'd be happy to make the change you described, or to accept a PR that makes that change.

@badsyntax
Copy link
Contributor Author

Cool yes I think that makes sense. Am happy to make a PR for this, but will only get to doing it sometime tomorrow (BST).

badsyntax pushed a commit to badsyntax/grpc-node that referenced this issue Apr 18, 2020
…#1352

In the case where a new watcher is synchronously added to the watcher queue via the
watcher callback, this can result in the callback being called multiple times.

To support this case, the watcher needs to be move removed from the queue
before calling the watcher callback.
badsyntax pushed a commit to badsyntax/grpc-node that referenced this issue Apr 18, 2020
badsyntax added a commit to badsyntax/grpc-node that referenced this issue Apr 18, 2020
…#1352

In the case where a new watcher is synchronously added to the watcher queue via the
watcher callback, this can result in the callback being called multiple times.

To support this case, the watcher needs to be move removed from the queue
before calling the watcher callback.
badsyntax added a commit to badsyntax/grpc-node that referenced this issue Apr 18, 2020
badsyntax added a commit to badsyntax/grpc-node that referenced this issue Apr 18, 2020
…ixes grpc#1352

In the case where a new watcher is synchronously added to the watcher queue via the
watcher callback, this can result in the callback being called multiple times.

To support this case, the watcher needs to be move removed from the queue
before calling the watcher callback.
badsyntax added a commit to badsyntax/grpc-node that referenced this issue Apr 18, 2020
badsyntax added a commit to badsyntax/grpc-node that referenced this issue Apr 18, 2020
@badsyntax
Copy link
Contributor Author

I've added a PR with #1368 but it doesn't look like the builds are running. Should I close the PR and re-open?

badsyntax added a commit to badsyntax/grpc-node that referenced this issue Apr 20, 2020
No need to add a service to the server to test the client.
badsyntax added a commit to badsyntax/grpc-node that referenced this issue Apr 20, 2020
No need to add a service to the server to test the client.
murgatroid99 added a commit that referenced this issue Apr 20, 2020
grpc-js: Client waitForReady callback fix.  Fixes #1352
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants