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

Add Poll class, fix some tests, throttle polling. #6141

Merged
merged 142 commits into from
Apr 9, 2019
Merged

Add Poll class, fix some tests, throttle polling. #6141

merged 142 commits into from
Apr 9, 2019

Conversation

afshin
Copy link
Member

@afshin afshin commented Mar 26, 2019

Fixes #3929

  • Adds a Poll class to @jupyterlab/coreutils to abstract polling and exponentially backing off in case of failure.
  • Adds Poll class tests.
  • Cleans up doc strings in the running extension.
  • Updates different ad hoc polls to use the Poll class:
    • @jupyterlab/services:KernelManager#models
    • @jupyterlab/services:KernelManager#specs
    • @jupyterlab/services:SessionManager#models
    • @jupyterlab/services:SessionManager#specs
    • @jupyterlab/services:TerminalManager#models
    • @jupyterlab/statusbar:MemoryUsage#metrics
  • Switches kernel manager and session manager to async.
  • A future enhancement of the Poll class that is possible is adding Poll[Symbol.asyncIterator], Poll#return(), and Poll#next() methods to make the poll an async iterator.
  • Updates tests and switches some previously asynchronous tests to synchronous (as long as the semantics are not modified).
  • Re-enables dialog tests in dialog.spec.tsx file that were accidentally disabled and modernizes them to jest API.
  • Cleans up Dialog class API and adds an instance tracker: Dialog.tracker

State diagram of the possible state transitions between poll phases

poll state diagram

@jupyterlab-dev-mode
Copy link

Thanks for making a pull request to JupyterLab!

To try out this branch on binder, follow this link: Binder

@afshin afshin changed the title [WIP] Add Poll class, fix tests, throttle polling. Add Poll class, fix tests, throttle polling. Mar 26, 2019
@afshin afshin requested a review from jasongrout March 27, 2019 20:09
@afshin afshin added the api-change A change that should be accompanied by a major version increase label Mar 28, 2019
@afshin afshin changed the title Add Poll class, fix tests, throttle polling. [WIP] Add Poll class, fix tests, throttle polling. Mar 29, 2019
@afshin
Copy link
Member Author

afshin commented Mar 29, 2019

The broken tests are correct. I will remove the [WIP] tag when ready for review.

@afshin afshin changed the title [WIP] Add Poll class, fix tests, throttle polling. Add Poll class, fix some tests, throttle polling. Mar 30, 2019
@afshin afshin changed the title Add Poll class, fix some tests, throttle polling. [WIP] Add Poll class, fix some tests, throttle polling. Apr 1, 2019
@jasongrout
Copy link
Contributor

@afshin - I notice this is still WIP. Do you want me to review it yet for the prerelease this week?

@afshin
Copy link
Member Author

afshin commented Apr 2, 2019

@jasongrout Sorry! I put it back into WIP because some things needed refactoring. This will be ready before our meeting tomorrow. Is that enough time for you to review?

@afshin afshin changed the title [WIP] Add Poll class, fix some tests, throttle polling. Add Poll class, fix some tests, throttle polling. Apr 2, 2019
@afshin
Copy link
Member Author

afshin commented Apr 2, 2019

@jasongrout Assuming tests pass, this is ready for review. I created an issue to refactor the file browser model, which is the only poll I left out, because it is more deeply embedded in the file browser and it will not require an API change so we can do it later.

@jasongrout
Copy link
Contributor

Thanks. I'm looking at this now.

@jasongrout
Copy link
Contributor

I have a PR to submit to this PR with some slight (but I feel important) modifications to the math of the jitter algorithm. I also looked a bit into jitter algorithms. https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ was one of the few interesting results I found in a quick search, and discussed more at https://trac.torproject.org/projects/tor/ticket/23816#no2. Using the terminology from the blog post, we essentially have a variant of their "Equal Jitter" approach. Perhaps it would be better to use something like "Decorrelated Jitter", which essentially puts a static lower bound on the polling interval, but just increases the max exponentially, and picks a uniform random value between the min and max. I like this approach because it tends to really spread clients out quickly, rather than still grouping them around the exponential values. What do you think about using that algorithm instead? Then "no jitter" would just be basically a setTimeout with a single value.

@afshin
Copy link
Member Author

afshin commented Apr 4, 2019

That sounds good to me, the decorrelated jitter sounds like a good behavior to me. Please post a PR if you're working on one. If not, I can implement it tomorrow.

@afshin
Copy link
Member Author

afshin commented Apr 4, 2019

Also, one change I recently made that I want to undo is that instead of await this.ready; I think it should do:

if (!this.isReady) {
  await this.ready;
}

or something like that. I'm not sure whether to add Poll#isReady or simply check for this.state.phase === 'instantiated'. But the point is to avoid an additional frame request happening every single time a client calls one of the public methods.

@jasongrout
Copy link
Contributor

I'm not sure whether to add Poll#isReady or simply check for this.state.phase === 'instantiated'.

I've been puzzling through the state transitions for a while this afternoon. I think checking the state phase won't work because the phase could be any number of things after the initialization. I like the synchronous .isReady and have used that elsewhere.

@afshin
Copy link
Member Author

afshin commented Apr 4, 2019

Any phase after instantiated can be arbitrarily interrupted without causing a break in the promise chain since its tick will be resolved with the new tick interrupting it (or if unnecessary for a new started or refreshed tick, it'll simply ignore). The only uninterruptible phase is instantiated, so the implementation of isReady would look like:

get isReady(): boolean {
  return this.state.phase !== 'instantiated';
}

Because any other phase can

@afshin
Copy link
Member Author

afshin commented Apr 4, 2019

In reality, instantiated could be interrupted too without breaking the chain, it's just that it would break the contract of what the when promise actually means, so it needs to respected by checking for it in the public methods.

@afshin
Copy link
Member Author

afshin commented Apr 5, 2019

Hm, I think we should talk about jitter, I'm a little unsure about what we want.

@jasongrout
Copy link
Contributor

I opened https://github.com/afshin/jupyterlab/pull/24 for some changes to the kernel manager logic. I'm continuing to work my way through the files in the PR, and I may add more to that PR.

let xsrfToken = getCookie('_xsrf');
if (xsrfToken !== void 0) {
}
if (typeof document !== 'undefined' && document && document.cookie) {
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 elaborate on why you took out the else here?

Copy link
Member Author

@afshin afshin Apr 8, 2019

Choose a reason for hiding this comment

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

This was causing reconnects (after stopping the server and starting again) to fail. I didn't definitively understand why but it seems reasonable if we have both the cookie and the header to use both and it turned out to eliminate a class of failures after reconnect.

@jasongrout
Copy link
Contributor

One more PR with changes to the terminal manager: https://github.com/afshin/jupyterlab/pull/25

Update terminal manager to be consistent with kernel/session manager async logic
@jasongrout
Copy link
Contributor

For a future PR, I noticed in using the API it might be nice if .refresh() returns the .tick promise, so you can just do await poll.refresh() instead of await poll.refresh(); await poll.tick().

@@ -124,7 +126,7 @@ describe('terminal', () => {
let called = false;
session = await manager.startNew();
manager.runningChanged.connect((sender, args) => {
expect(session.isDisposed).to.equal(false);
expect(session.isDisposed).to.equal(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Note to self: this may need to be changed back to false after my recent manager updates.

@@ -203,7 +221,7 @@ describe('kernel/manager', () => {
const kernel = await manager.startNew();
const emission = testEmission(manager.runningChanged, {
test: () => {
expect(kernel.isDisposed).to.equal(false);
expect(kernel.isDisposed).to.equal(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Note to self: this may need to be changed back to false after my recent manager updates.

@afshin
Copy link
Member Author

afshin commented Apr 8, 2019

For a future PR, I noticed in using the API it might be nice if .refresh() returns the .tick promise, so you can just do await poll.refresh() instead of await poll.refresh(); await poll.tick().

We could potentially do something like:

async refresh(passthrough = false): Promise<void> {
  if (this.isDisposed) {
    return;
  }

  if (this.state.phase === 'constructed') {
    await this.ready;
  }

  if (this.state.phase !== 'refreshed') {
    this.schedule({
      interval: Private.IMMEDIATE,
      payload: null,
      phase: 'refreshed',
      timestamp: new Date().getTime()
    });
  }

  if (passthrough) {
    await this.tick;
  }
}

But I'm not sure. The idea was to keep the API super simple and predictable: you always get the tick you created, the refreshed tick. But it is a little counter-intuitive if all you care about is the result of the refresh roundtrip to a server and not just the instruction to refresh.

@jasongrout
Copy link
Contributor

But I'm not sure.

I'm not sure either. I'm happy to leave it as-is, with a simple consistent api, and see if that's okay, or if we want to make it slightly more convenient later.

@afshin
Copy link
Member Author

afshin commented Apr 8, 2019

The spot you flagged did indeed fail.

Feel free to push directly into afshin:api-requests branch if you want, because I won't be able to merge a PR until tomorrow otherwise.

Screen Shot 2019-04-08 at 11 58 56 PM

@afshin
Copy link
Member Author

afshin commented Apr 9, 2019

I apparently neglected to git push the doc string commit on my other machine, so I added it now.

@jasongrout
Copy link
Contributor

And the tests all pass! Thanks again for all your work on this one, @afshin!

@jasongrout jasongrout merged commit 730333f into jupyterlab:master Apr 9, 2019
@ian-r-rose
Copy link
Member

Nice!

@lock lock bot added the status:resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion. label Aug 7, 2019
@lock lock bot locked as resolved and limited conversation to collaborators Aug 7, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-change A change that should be accompanied by a major version increase pkg:apputils pkg:coreutils pkg:running pkg:services status:resolved-locked Closed issues are locked after 30 days inactivity. Please open a new issue for related discussion. tag:Pre-Release tag:Testing
Projects
None yet
Development

Successfully merging this pull request may close these issues.

jupyterlab polls the API a bit too much
3 participants