Skip to content
This repository has been archived by the owner on Sep 13, 2022. It is now read-only.

Add throttler #246

Merged
merged 23 commits into from
Apr 12, 2018
Merged

Add throttler #246

merged 23 commits into from
Apr 12, 2018

Conversation

black-adder
Copy link
Contributor

Throttler is a rate limiter that frequently fetches credits for each operation and rate limits said operation if it has insufficient credits. This PR only adds the throttler, the actual rate limiting will be done in a subsequent PR.

Signed-off-by: Won Jun Jang wjang@uber.com

Signed-off-by: Won Jun Jang <wjang@uber.com>
@ghost ghost assigned black-adder Apr 2, 2018
@ghost ghost added the review label Apr 2, 2018
Signed-off-by: Won Jun Jang <wjang@uber.com>
if (!this._credits.has(operation)) {
this._credits.set(operation, 0);
// If seen for the first time, async fetch credits
this._refreshCredits();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

In golang, when we see the operation for the first time, we have the option of allowing service owners to synchronously fetch credits for the first call; this is useful for cases like yab. In node however, I spoke with @tiffon and he said it's not advisable (TBF, it's not advisable in golang either but I think necessary).

I think this is an ok compromise.

Signed-off-by: Won Jun Jang <wjang@uber.com>
@@ -24,6 +24,9 @@ declare type LogData = {
declare type Process = {
serviceName: string,
tags: Array<Tag>,
// N.B. uuid uniquely identifies this instance of the client. It is only used by the client and is not
// propagated as part of the real jaeger thrift process.
uuid?: string,
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 made this optional to not break all the things. Should not be a breaking change.

Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
"coveralls": "npm run cover && cat ./coverage/lcov.info | coveralls",
"flow": "flow",
"format": "prettier --write '**/*.{js,json,md}'",
"format": "prettier --write '**/*.{js,json,md}' '!src/jaeger-idl/**'",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

added a rule to skip prettifying a git submodule

Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
@coveralls
Copy link

coveralls commented Apr 3, 2018

Coverage Status

Coverage increased (+0.08%) to 96.911% when pulling 00df710 on add_throttler into aa4bfb8 on master.

Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
Copy link
Member

@tiffon tiffon left a comment

Choose a reason for hiding this comment

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

Looks great. Some questions and comments.

import ConfigServer from '../lib/config_server';
import LocalMetricFactory from '../lib/metrics/local/metric_factory.js';
import LocalBackend from '../lib/metrics/local/backend.js';
import RemoteThrottler from '../../src/throttler/remote_thottler';
Copy link
Member

Choose a reason for hiding this comment

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

Typo in filename, missing the "r" in throttler

Copy link
Contributor Author

Choose a reason for hiding this comment

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

that's a mighty sharp eye you have there

this._logger.error(`Error in fetching sampling strategy: ${err}.`);
this._metrics.samplerQueryFailure.increment(1);
});
let success: Function = body => {
Copy link
Member

Choose a reason for hiding this comment

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

Using an arrow function here can be avoided if the method is defined with an arrow function. Then you can just use the method as the callback.

_parseSamplingServerResponse = (body: string) => {
  // ...
}


isAllowed(operation: string): boolean {
if (operation in this._credits) {
// TODO if credits is 0, is this false? Do I need to do an explicit check on undefined?
Copy link
Member

Choose a reason for hiding this comment

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

If this._credits.a === 0 then 'a' in this._credits is still true.

If null comes up as a value and you need to refresh if the value is null then you can check for

this._credits[operation] != `null`

Note the != instead of !== means the check will fail for both undefined and null

Copy link
Member

Choose a reason for hiding this comment

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

Or is that the point of initialDelayMs? Seems like it won't prevent the above scenario, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the credits can't ever be set to null so the current check will suffice

// N.B. The -1 assignment is necessary, otherwise we need to type credits as ?number which
// becomes a bit of a headache in this function because flow will throw errors on credits
// being null even if I explicitly check it's not before continuing.
let credits: number = operation in this._credits ? this._credits[operation] : 0;
Copy link
Member

Choose a reason for hiding this comment

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

This can be a const. And, this is vulnerable to the scenario where operation is in _credits but not a number, so then becomes NaN on line 114. An alternative is

const credits = this._credits[operation] || 0;

if (credits < MINIMUM_CREDITS) {
return false;
}
this._credits[operation] = credits - MINIMUM_CREDITS;
Copy link
Member

Choose a reason for hiding this comment

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

Looks like the minimum credit is doubling as the unit value? Maybe can be renamed to CREDIT_UNIT or UNIT_CREDIT or something?

}
this._credits[operation] = 0;
// If seen for the first time, async fetch credits
this._refreshCredits();
Copy link
Member

Choose a reason for hiding this comment

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

You might consider debouncing this some moderate amount of time so the wouldn't result in N calls to /credits (a possibility on initialization):

throttler.isAllowed('op-0');
throttler.isAllowed('op-1');
// ...
throttler.isAllowed('op-N');


operations.forEach(operation => {
url = url + `&operation=${encodeURIComponent(operation)}`;
});
Copy link
Member

Choose a reason for hiding this comment

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

String concatenation is pretty slow in JS. An alternate is

const ops = operations.map(encodeURIComponent).join('&operation=')
const url = `/credits?service=${serviceName}&uuid=${uuid}&operation=${ops}`

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 led me astray: https://stackoverflow.com/questions/13859543/fast-way-to-concatenate-strings-in-nodejs-javascript

Is that link incorrect? I changed over to using join

Copy link
Member

Choose a reason for hiding this comment

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

Seems to vary based on the strings, themselves. But, they're all fairly close in most scenarios.


_afterInitialDelay(): void {
this._refreshCredits();
this._refreshIntervalHandle = setInterval(this._refreshCredits.bind(this), this._refreshIntervalMs);
Copy link
Member

Choose a reason for hiding this comment

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

You might consider setting this._initialDelayTimeoutHandle to null as it is no longer valid.

}

_isAllowed(operation: string): boolean {
// N.B. The -1 assignment is necessary, otherwise we need to type credits as ?number which
Copy link
Member

Choose a reason for hiding this comment

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

What -1 assignment?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sorry old comment

this._host = options.host || DEFAULT_THROTTLER_HOST;
this._port = options.port || DEFAULT_THROTTLER_PORT;

this._credits = Object.create(null);
Copy link
Member

Choose a reason for hiding this comment

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

What's the advantage of Object.create(null) vs {} in this case?

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 copied over legacy code that did this. Since we're using the keys of the object, won't Object.keys({}) return things like _proto which is something we don't want right? I'll check

Copy link
Member

Choose a reason for hiding this comment

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

It should be fine:

The object of which the enumerable's own properties are to be returned.

MDN Object.keys

Signed-off-by: Won Jun Jang <wjang@uber.com>
@black-adder
Copy link
Contributor Author

addressed some comments, looking into debouncing and other comments

return;
}
const keys = Object.keys(this._credits);
if (keys.length == 0) {
Copy link
Member

Choose a reason for hiding this comment

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

I recommend using === instead of == for all cases except comparisons with null or undefined.

Ref: Loose equality using ==

let success: Function = body => {
this._parseCreditResponse(body);
};
let error: Function = err => {
Copy link
Member

Choose a reason for hiding this comment

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

I'd recommend any let that doesn't change be switched to a const.

Signed-off-by: Won Jun Jang <wjang@uber.com>
throttler = new RemoteThrottler(serviceName);
throttler.setProcess({ uuid: null });
throttler._refreshCredits();
});
Copy link
Member

Choose a reason for hiding this comment

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

Should there be an assert, here?

Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
Copy link
Member

@tiffon tiffon left a comment

Choose a reason for hiding this comment

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

Some suggestions, mostly with regard to tests.

@@ -120,5 +122,13 @@ export default class Metrics {
});

this.baggageTruncate = this._factory.createCounter('baggage-trucate');

this.throttlerUpdateSuccess = this._factory.createCounter('throttler-update', {
Copy link
Member

Choose a reason for hiding this comment

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

I don't know the params for this factory, but should this string be something like throttler-update-success and the next one throttler-update-failure?

this._logger.error(`Error in fetching sampling strategy: ${err}.`);
this._metrics.samplerQueryFailure.increment(1);
});
let success: Function = body => {
Copy link
Member

Choose a reason for hiding this comment

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

I recommend using const on values that don't change.

export default class DefaultThrottler {
_throttleAll: boolean;

constructor(throttleAll: ?boolean) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this should be throttleAll?: boolean

if (operation in this._credits) {
return this._isAllowed(operation);
}
// Credits for the operation will be asynchronously fetched
Copy link
Member

Choose a reason for hiding this comment

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

Just curious, is relying on the interval consistent with the Go and Java throttlers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yup

Copy link
Member

Choose a reason for hiding this comment

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

👍

}
}

close(callback: ?Function): void {
Copy link
Member

Choose a reason for hiding this comment

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

I believe this should be callback?: Function, or callback?: () => void

this._strategies = Object.create(null);
this._credits = Object.create(null);
Copy link
Member

Choose a reason for hiding this comment

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

I think all of these Object.create(null) should just be {}...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

bah, these are all over the code base. I'll add a subsequent PR to change all these at once

Copy link
Member

Choose a reason for hiding this comment

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

👍

throttler.setProcess({ uuid: uuid });
throttler._credits[operation] = 0;
server.addCredits(serviceName, [{ operation: operation, credits: 5 }]);
throttler._onCreditsUpdate = _throttler => {
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you can avoid overwriting the private(-ish) property by using the options.onCreditUpdate. This would avoid relying on the private property and breaking the public API would break the test.

let creditUpdatedHook;

beforeEach(() => {
  creditUpdatedHook = sinon.spy();
  throttler = new RemoteThrottler(serviceName, {
    // ...
    onCreditUpdate: (...args) => creditUpdatedHook(...args),
  });
});

it('....', () => {
  assertTrue(creditUpdatedHook.called);
});

it('...', done => {
  creditUpdatedHook = () => {
    // ...
    done();
  }
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

great refactoring, will do

Copy link
Member

Choose a reason for hiding this comment

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

👍

throttler._refreshCredits();
});

it('emit failure metric on failing to query for credits', done => {
Copy link
Member

Choose a reason for hiding this comment

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

For the failure scenarios, you might consider also ensuring the credit update callback does not fire.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

what would be the best way to do this? the callback happens asynchronously, do I overwrite the done function to assert that the callback is not called? Might increase how long our tests take substantially.

Copy link
Member

Choose a reason for hiding this comment

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

I think I generally use setTimeout(..., 0), so something like:


  it('emit failure metric when server returns an invalid response', done => {
    throttler.setProcess({ uuid: uuid });
    throttler._credits[operation] = 0;
    metrics.throttlerUpdateFailure.increment = function() {
      assert.equal(logger._errorMsgs.length, 1, `errors=${logger._errorMsgs}`);
      setTimeout(() => {
        // assert
        done();
      }, 0)
    };
    throttler._refreshCredits();
  });

Copy link
Contributor Author

Choose a reason for hiding this comment

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

stupid setTimeout issue on travis strikes again. I think asserting that the logs were added are good enough for these tests.

Copy link
Member

Choose a reason for hiding this comment

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

That is really unfortunate. Sort of mild in this case, but in general it's undermining our test-coverage...

Signed-off-by: Won Jun Jang <wjang@uber.com>
throttler = new RemoteThrottler(serviceName);
throttler.setProcess({ uuid: uuid });
throttler._refreshCredits();
assert.equal(Object.keys(throttler._credits).length, 0);
Copy link
Member

Choose a reason for hiding this comment

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

Does this assert verify the fetch doesn't happen?

Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
@@ -24,6 +24,9 @@ declare type LogData = {
declare type Process = {
serviceName: string,
tags: Array<Tag>,
// N.B. uuid uniquely identifies this instance of the client. It is only used by the client and is not
Copy link
Member

Choose a reason for hiding this comment

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

Is the UUID used by the client and the agent?

throttler._refreshCredits();
});

it('emit failure metric on failing to query for credits', done => {
Copy link
Member

Choose a reason for hiding this comment

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

That is really unfortunate. Sort of mild in this case, but in general it's undermining our test-coverage...

Copy link
Member

@tiffon tiffon left a comment

Choose a reason for hiding this comment

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

Looks great, two nits

if (operation in this._credits) {
return this._isAllowed(operation);
}
// Credits for the operation will be asynchronously fetched
Copy link
Member

Choose a reason for hiding this comment

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

👍


close(callback?: Function): void {
clearTimeout(this._initialDelayTimeoutHandle);
clearInterval(this._refreshIntervalHandle);
Copy link
Member

Choose a reason for hiding this comment

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

There is an edge case of a credits fetch finishing after close is called.

black-adder and others added 2 commits April 12, 2018 12:00
Signed-off-by: Won Jun Jang <wjang@uber.com>
@black-adder black-adder merged commit 1608b0d into master Apr 12, 2018
@ghost ghost removed the review label Apr 12, 2018
@black-adder black-adder deleted the add_throttler branch April 12, 2018 16:26
Iuriy-Budnikov pushed a commit to agile-pm/jaeger-client-node that referenced this pull request Sep 25, 2021
closes jaegertracing#242

Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants