-
Notifications
You must be signed in to change notification settings - Fork 220
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
feat(ai): Set a rate limit for vector store rebuild #9404
feat(ai): Set a rate limit for vector store rebuild #9404
Conversation
|
…ld-api' into feat/157512-set-a-rate-limit-for-vector-store-rebuild
@@ -19,7 +19,7 @@ const logger = loggerFactory('growi:middleware:api-rate-limit'); | |||
// API_RATE_LIMIT_010_FOO_METHODS=GET,POST | |||
// API_RATE_LIMIT_010_FOO_MAX_REQUESTS=10 | |||
|
|||
const POINTS_THRESHOLD = 100; | |||
export const POINTS_THRESHOLD = 100; |
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 で利用するために export
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.
このままだと結局 index.tx によって外部に export されてしまうので、factory.ts から外に出してください
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.
修正しました
@@ -37,9 +37,9 @@ const keysWithRegExp = Object.keys(configWithRegExp).map(key => new RegExp(`^${k | |||
const valuesWithRegExp = Object.values(configWithRegExp); | |||
|
|||
|
|||
const _consumePoints = async( | |||
export const _consumePoints = 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.
test で利用するために export
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.
こちらも同様、このままだと結局 index.tx によって外部に export されてしまうので、factory.ts から外に出してください
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.
修正しました
const consumePoints = (POINTS_THRESHOLD + 0.0001) / maxRequests; | ||
await rateLimiter.consume(key, consumePoints); | ||
const consumePoints = POINTS_THRESHOLD / maxRequests; | ||
const rateLimiterRes = await rateLimiter.consume(key, consumePoints); |
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.
maxRequest より 1 回少ない request 数で rate limit になってしまっていたので修正
@@ -19,7 +19,7 @@ const logger = loggerFactory('growi:middleware:api-rate-limit'); | |||
// API_RATE_LIMIT_010_FOO_METHODS=GET,POST | |||
// API_RATE_LIMIT_010_FOO_MAX_REQUESTS=10 | |||
|
|||
const POINTS_THRESHOLD = 100; | |||
export const POINTS_THRESHOLD = 100; |
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.
このままだと結局 index.tx によって外部に export されてしまうので、factory.ts から外に出してください
@@ -37,9 +37,9 @@ const keysWithRegExp = Object.keys(configWithRegExp).map(key => new RegExp(`^${k | |||
const valuesWithRegExp = Object.values(configWithRegExp); | |||
|
|||
|
|||
const _consumePoints = async( | |||
export const _consumePoints = 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.
こちらも同様、このままだと結局 index.tx によって外部に export されてしまうので、factory.ts から外に出してください
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.
- 意味のあるテストにしないといけない
- unit test からテスト用 MongoDB と連携させた rate-limiter-flexible をテストする integration test へ
- 境界値テストにしないといけない
- 意味がある境界 = 500, 501 の間 (maxRequests=500 の時)
- @faker-js/faker も使う
- 意味がある境界 = n, n+1 の間 (maxRequests=n の時)
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.
修正しました
rateLimiter.points = maxRequests; | ||
const rateLimiterRes = await rateLimiter.consume(key, 1); | ||
return rateLimiterRes; | ||
}; |
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.
features dir 外から import できないように外部モジュール化した
} | ||
|
||
rateLimiter.points = maxRequests; | ||
const rateLimiterRes = await rateLimiter.consume(key, 1); |
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.
機能改修はここのみ
Before
// because the maximum request is reduced by 1 if it is divisible by
// https://github.com/weseek/growi/pull/6225
const consumePoints = (POINTS_THRESHOLD + 0.0001) / maxRequests;
await rateLimiter.consume(key, consumePoints);
maxRequest: 1 の場合 2 回目の request で rate limit error べきだが、 (100 + 0.0001) / 1 = 100.0001 になるので1回目の request で rate limit error になる。
Before (この PR の当初の改修)
const consumePoints = POINTS_THRESHOLD / maxRequests;
const rateLimiterRes = await rateLimiter.consume(key, consumePoints);
maxRequest: 500 の場合 100 / 500 = 0.2 になる。0.2 * 500 で 100 (閾値) に達するので良いと思っていた。しかし 1回の request で consumedPoints (消費ポイント) が 0.2 として保存される 。2回目の request で consumedPoints が 0.4 として保存される。3回目の request で consumedPoints が 0.6000000000000001 として保存されれ、期待する通りに動かなかったためこれも却下になった。
参考
After (最終的に)
rateLimiter.points = maxRequests;
const rateLimiterRes = await rateLimiter.consume(key, 1);
RateLimiterMongo 初期化時には point の閾値を設定せず、request される度に更新するように変更した。point 消費は 1 (整数) で固定するように変更した。
const testRateLimitErrorWhenExceedingMaxRequests = async(method: string, key: string, maxRequests: number): Promise<void> => { | ||
// dynamic import is used because rateLimiterMongo needs to be initialized after connecting to DB | ||
// Issue: https://github.com/animir/node-rate-limiter-flexible/issues/216 | ||
const { rateLimiter } = await import('./rate-limiter-mongo-client'); |
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.
DB との connection が確立する前に RateLimiterMongo が初期化されると issue 記載のエラーになるため、RateLimiterMongo を初期化するモジュールを dynamic import している (issue の解法とは異なる)。
Issue comment
animir/node-rate-limiter-flexible#216 (comment)
my solution to this problem is to simply wait for the database to connect before creating a RateLimitMongo instance
const res = await consumePoints(rateLimiter, method, key, { method, maxRequests }); | ||
if (count === maxRequests) { | ||
// Expect consumedPoints to be equal to maxRequest when maxRequest is reached | ||
expect(res?.consumedPoints).toBe(maxRequests); |
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.
maxRequest 回目で consumedPoints (消費ポイント) が maxRequest と等しくなることを期待
// Expect consumedPoints to be equal to maxRequest when maxRequest is reached | ||
expect(res?.consumedPoints).toBe(maxRequests); | ||
// Expect remainingPoints to be 0 when maxRequest is reached | ||
expect(res?.remainingPoints).toBe(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.
maxRequest 回目で remainingPoints (残ポイント) が 0 になることを期待
expect(res?.remainingPoints).toBe(0); | ||
} | ||
if (count > maxRequests) { | ||
throw new Error('Exception occurred'); |
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.
maxRequest を超過しても rate limit error にならなかった場合 error を throw する
} | ||
catch (err) { | ||
// Expect not to exceed maxRequest | ||
expect(err.message).not.toBe('Exception occurred'); |
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.
maxRequest 超過時に rate limit error が throw されていることを期待
// Expect not to exceed maxRequest | ||
expect(err.message).not.toBe('Exception occurred'); | ||
// Expect rate limit error at maxRequest + 1 | ||
expect(count).toBe(maxRequests + 1); |
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.
(maxRequest + 1) 回目の時点で rate limit error になっていることを期待
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.
factory で RateLimiterMongo をインスタンス化することで 1 instance per endpoint になる設計だったのだが、この PR では rate-limiter-mongo-client.ts を用意していることにより singleton になっている。
テストでは問題ないのかもしれないが、実環境では多くのリクエストが同時にやってくる中、points を都度上書きするような実装で問題はでないのかという心配がある。
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.
factory で RateLimiterMongo をインスタンス化することで 1 instance per endpoint になる設計だったのだが
1 instance per endpoint (key) になるような実装に修正しました
実環境では多くのリクエストが同時にやってくる中、points を都度上書きするような実装で問題はでないのかという心配がある。
確かに調べてみるとこの実装には race condition というリスクがあるみたいでした。
this.rateLimiters.set(key, rateLimiter); | ||
|
||
return rateLimiter; | ||
} |
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.
- key ごとに独立したインスタンスを作成できるようにした
Task