-
Notifications
You must be signed in to change notification settings - Fork 133
Conversation
Signed-off-by: Won Jun Jang <wjang@uber.com>
Signed-off-by: Won Jun Jang <wjang@uber.com>
src/throttler/remote_thottler.js
Outdated
if (!this._credits.has(operation)) { | ||
this._credits.set(operation, 0); | ||
// If seen for the first time, async fetch credits | ||
this._refreshCredits(); |
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.
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.
src/_flow/jaeger-thrift.js
Outdated
@@ -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, |
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 made this optional to not break all the things. Should not be a breaking change.
"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/**'", |
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.
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>
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>
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.
Looks great. Some questions and comments.
test/throttler/remote_throttler.js
Outdated
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'; |
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.
Typo in filename, missing the "r" in throttler
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.
that's a mighty sharp eye you have there
src/samplers/remote_sampler.js
Outdated
this._logger.error(`Error in fetching sampling strategy: ${err}.`); | ||
this._metrics.samplerQueryFailure.increment(1); | ||
}); | ||
let success: Function = body => { |
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.
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) => {
// ...
}
src/throttler/remote_thottler.js
Outdated
|
||
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? |
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.
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
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.
Or is that the point of initialDelayMs
? Seems like it won't prevent the above scenario, though.
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 credits can't ever be set to null so the current check will suffice
src/throttler/remote_thottler.js
Outdated
// 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; |
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 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;
src/throttler/remote_thottler.js
Outdated
if (credits < MINIMUM_CREDITS) { | ||
return false; | ||
} | ||
this._credits[operation] = credits - MINIMUM_CREDITS; |
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.
Looks like the minimum credit is doubling as the unit value? Maybe can be renamed to CREDIT_UNIT
or UNIT_CREDIT
or something?
src/throttler/remote_thottler.js
Outdated
} | ||
this._credits[operation] = 0; | ||
// If seen for the first time, async fetch credits | ||
this._refreshCredits(); |
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 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');
src/throttler/remote_thottler.js
Outdated
|
||
operations.forEach(operation => { | ||
url = url + `&operation=${encodeURIComponent(operation)}`; | ||
}); |
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.
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}`
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 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
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.
Seems to vary based on the strings, themselves. But, they're all fairly close in most scenarios.
src/throttler/remote_thottler.js
Outdated
|
||
_afterInitialDelay(): void { | ||
this._refreshCredits(); | ||
this._refreshIntervalHandle = setInterval(this._refreshCredits.bind(this), this._refreshIntervalMs); |
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 might consider setting this._initialDelayTimeoutHandle
to null
as it is no longer valid.
src/throttler/remote_thottler.js
Outdated
} | ||
|
||
_isAllowed(operation: string): boolean { | ||
// N.B. The -1 assignment is necessary, otherwise we need to type credits as ?number which |
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.
What -1
assignment?
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.
sorry old comment
src/throttler/remote_thottler.js
Outdated
this._host = options.host || DEFAULT_THROTTLER_HOST; | ||
this._port = options.port || DEFAULT_THROTTLER_PORT; | ||
|
||
this._credits = Object.create(null); |
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.
What's the advantage of Object.create(null)
vs {}
in this case?
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 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
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.
It should be fine:
The object of which the enumerable's own properties are to be returned.
Signed-off-by: Won Jun Jang <wjang@uber.com>
addressed some comments, looking into debouncing and other comments |
src/throttler/remote_throttler.js
Outdated
return; | ||
} | ||
const keys = Object.keys(this._credits); | ||
if (keys.length == 0) { |
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 recommend using ===
instead of ==
for all cases except comparisons with null
or undefined
.
src/throttler/remote_throttler.js
Outdated
let success: Function = body => { | ||
this._parseCreditResponse(body); | ||
}; | ||
let error: Function = err => { |
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'd recommend any let
that doesn't change be switched to a const
.
Signed-off-by: Won Jun Jang <wjang@uber.com>
test/throttler/remote_throttler.js
Outdated
throttler = new RemoteThrottler(serviceName); | ||
throttler.setProcess({ uuid: null }); | ||
throttler._refreshCredits(); | ||
}); |
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.
Should there be an assert, here?
Signed-off-by: Won Jun Jang <wjang@uber.com>
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.
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', { |
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 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
?
src/samplers/remote_sampler.js
Outdated
this._logger.error(`Error in fetching sampling strategy: ${err}.`); | ||
this._metrics.samplerQueryFailure.increment(1); | ||
}); | ||
let success: Function = body => { |
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 recommend using const
on values that don't change.
src/throttler/default_throttler.js
Outdated
export default class DefaultThrottler { | ||
_throttleAll: boolean; | ||
|
||
constructor(throttleAll: ?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 think this should be throttleAll?: boolean
if (operation in this._credits) { | ||
return this._isAllowed(operation); | ||
} | ||
// Credits for the operation will be asynchronously fetched |
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.
Just curious, is relying on the interval consistent with the Go and Java throttlers?
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.
yup
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.
👍
src/throttler/remote_throttler.js
Outdated
} | ||
} | ||
|
||
close(callback: ?Function): 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 believe this should be callback?: Function
, or callback?: () => void
test/lib/config_server.js
Outdated
this._strategies = Object.create(null); | ||
this._credits = Object.create(null); |
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 think all of these Object.create(null)
should just be {}
...
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.
bah, these are all over the code base. I'll add a subsequent PR to change all these at once
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.
👍
test/throttler/remote_throttler.js
Outdated
throttler.setProcess({ uuid: uuid }); | ||
throttler._credits[operation] = 0; | ||
server.addCredits(serviceName, [{ operation: operation, credits: 5 }]); | ||
throttler._onCreditsUpdate = _throttler => { |
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.
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();
}
});
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.
great refactoring, will do
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.
👍
throttler._refreshCredits(); | ||
}); | ||
|
||
it('emit failure metric on failing to query for credits', 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.
For the failure scenarios, you might consider also ensuring the credit update callback does not fire.
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.
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.
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 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();
});
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.
stupid setTimeout issue on travis strikes again. I think asserting that the logs were added are good enough for these tests.
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.
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>
test/throttler/remote_throttler.js
Outdated
throttler = new RemoteThrottler(serviceName); | ||
throttler.setProcess({ uuid: uuid }); | ||
throttler._refreshCredits(); | ||
assert.equal(Object.keys(throttler._credits).length, 0); |
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.
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>
src/_flow/jaeger-thrift.js
Outdated
@@ -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 |
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.
Is the UUID used by the client and the agent?
throttler._refreshCredits(); | ||
}); | ||
|
||
it('emit failure metric on failing to query for credits', 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.
That is really unfortunate. Sort of mild in this case, but in general it's undermining our test-coverage...
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.
Looks great, two nits
if (operation in this._credits) { | ||
return this._isAllowed(operation); | ||
} | ||
// Credits for the operation will be asynchronously fetched |
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.
👍
|
||
close(callback?: Function): void { | ||
clearTimeout(this._initialDelayTimeoutHandle); | ||
clearInterval(this._refreshIntervalHandle); |
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.
There is an edge case of a credits fetch finishing after close is called.
Signed-off-by: Won Jun Jang <wjang@uber.com>
closes jaegertracing#242 Signed-off-by: Olivier Albertini <olivier.albertini@montreal.ca>
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