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

feat(ai): Set a rate limit for vector store rebuild #9404

Merged
merged 25 commits into from
Nov 25, 2024

Conversation

miya
Copy link
Member

@miya miya commented Nov 14, 2024

Task

  • #156796 [RAG] rebuild 時 file をアップロードする際に rate limit を考慮した実装にできる
    • #157022 POST /_api/v3/openai/rebuild-vector-store に対して rate limit を設定できる

@miya miya requested a review from yuki-takei November 14, 2024 04:21
@miya miya self-assigned this Nov 14, 2024
Copy link

changeset-bot bot commented Nov 14, 2024

⚠️ No Changeset found

Latest commit: 0914f31

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@@ -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;
Copy link
Member Author

Choose a reason for hiding this comment

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

test で利用するために export

Copy link
Member

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 から外に出してください

Copy link
Member Author

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(
Copy link
Member Author

Choose a reason for hiding this comment

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

test で利用するために export

Copy link
Member

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 から外に出してください

Copy link
Member Author

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);
Copy link
Member Author

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 になってしまっていたので修正

@miya miya changed the title feat: Set a rate limit for vector store rebuild feat(ai): Set a rate limit for vector store rebuild Nov 17, 2024
@@ -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;
Copy link
Member

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(
Copy link
Member

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 から外に出してください

Copy link
Member

@yuki-takei yuki-takei Nov 18, 2024

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 の時)

Copy link
Member Author

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;
};
Copy link
Member Author

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);
Copy link
Member Author

@miya miya Nov 21, 2024

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 として保存されれ、期待する通りに動かなかったためこれも却下になった。

参考

https://qiita.com/kihara-takahiro/items/4518902225304f4e15b6#%E5%B0%8F%E6%95%B0%E7%82%B9%E3%81%AE%E5%8F%96%E3%82%8A%E6%89%B1%E3%81%84%E6%B3%A8%E6%84%8F

After (最終的に)

rateLimiter.points = maxRequests;
const rateLimiterRes = await rateLimiter.consume(key, 1);

RateLimiterMongo 初期化時には point の閾値を設定せず、request される度に更新するように変更した。point 消費は 1 (整数) で固定するように変更した。

@miya miya requested a review from yuki-takei November 21, 2024 11:51
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');
Copy link
Member Author

@miya miya Nov 21, 2024

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);
Copy link
Member Author

@miya miya Nov 21, 2024

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);
Copy link
Member Author

@miya miya Nov 21, 2024

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');
Copy link
Member Author

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');
Copy link
Member Author

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);
Copy link
Member Author

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 になっていることを期待

Copy link
Member

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 を都度上書きするような実装で問題はでないのかという心配がある。

Copy link
Member Author

@miya miya Nov 24, 2024

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;
}
Copy link
Member Author

@miya miya Nov 24, 2024

Choose a reason for hiding this comment

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

  • key ごとに独立したインスタンスを作成できるようにした

@miya miya requested a review from yuki-takei November 24, 2024 11:40
mergify bot added a commit that referenced this pull request Nov 25, 2024
@mergify mergify bot merged commit a120c07 into master Nov 25, 2024
22 checks passed
@mergify mergify bot deleted the feat/157512-set-a-rate-limit-for-vector-store-rebuild branch November 25, 2024 09:27
@github-actions github-actions bot mentioned this pull request Nov 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Development

Successfully merging this pull request may close these issues.

2 participants