Skip to content

Commit 44e7bee

Browse files
authored
refactor(Task): extract RateLimitClock from Task static state (#361) (#628)
* refactor(Task): extract RateLimitClock from Task static state * test(Task): hardening ratelimit spec
1 parent e0dd61a commit 44e7bee

5 files changed

Lines changed: 177 additions & 40 deletions

File tree

src/core/task/RateLimitClock.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export class RateLimitClock {
2+
private lastRequestTime?: number
3+
4+
getLastRequestTime(): number | undefined {
5+
return this.lastRequestTime
6+
}
7+
8+
recordRequest(): void {
9+
this.lastRequestTime = performance.now()
10+
}
11+
}
12+
13+
export function createRateLimitClock(): RateLimitClock {
14+
return new RateLimitClock()
15+
}

src/core/task/Task.ts

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { v7 as uuidv7 } from "uuid"
66
import EventEmitter from "events"
77

88
import { AskIgnoredError } from "./AskIgnoredError"
9+
import { RateLimitClock, createRateLimitClock } from "./RateLimitClock"
910

1011
import { Anthropic } from "@anthropic-ai/sdk"
1112
import OpenAI from "openai"
@@ -158,6 +159,7 @@ export interface TaskOptions extends CreateTaskOptions {
158159
workspacePath?: string
159160
/** Initial status for the task's history item (e.g., "active" for child tasks) */
160161
initialStatus?: "active" | "delegated" | "completed"
162+
rateLimitClock?: RateLimitClock
161163
}
162164

163165
export class Task extends EventEmitter<TaskEvents> implements TaskLike {
@@ -284,17 +286,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
284286
// API
285287
apiConfiguration: ProviderSettings
286288
api: ApiHandler
287-
private static lastGlobalApiRequestTime?: number
289+
private rateLimitClock: RateLimitClock
288290
private autoApprovalHandler: AutoApprovalHandler
289291

290-
/**
291-
* Reset the global API request timestamp. This should only be used for testing.
292-
* @internal
293-
*/
294-
static resetGlobalApiRequestTime(): void {
295-
Task.lastGlobalApiRequestTime = undefined
296-
}
297-
298292
toolRepetitionDetector: ToolRepetitionDetector
299293
rooIgnoreController?: RooIgnoreController
300294
rooProtectedController?: RooProtectedController
@@ -437,6 +431,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
437431
initialTodos,
438432
workspacePath,
439433
initialStatus,
434+
rateLimitClock,
440435
}: TaskOptions) {
441436
super()
442437

@@ -486,6 +481,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
486481

487482
this.apiConfiguration = apiConfiguration
488483
this.api = buildApiHandler(this.apiConfiguration)
484+
this.rateLimitClock = rateLimitClock ?? createRateLimitClock()
489485
this.autoApprovalHandler = new AutoApprovalHandler()
490486

491487
this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
@@ -2455,12 +2451,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
24552451
// This prevents the UI from showing an "API Request..." spinner while we are
24562452
// intentionally waiting due to the rate limit slider.
24572453
//
2458-
// NOTE: We also set Task.lastGlobalApiRequestTime here to reserve this slot
2459-
// before we build environment details (which can take time).
2460-
// This ensures subsequent requests (including subtasks) still honour the
2454+
// NOTE: We also record the request time here to reserve this slot before
2455+
// we build environment details (which can take time). This ensures
2456+
// subsequent requests (including subtasks) still honour the
24612457
// provider rate-limit window.
24622458
await this.maybeWaitForProviderRateLimit(currentItem.retryAttempt ?? 0)
2463-
Task.lastGlobalApiRequestTime = performance.now()
2459+
this.rateLimitClock.recordRequest()
24642460

24652461
await this.say(
24662462
"api_req_started",
@@ -3854,12 +3850,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
38543850
const rateLimitSeconds =
38553851
state?.apiConfiguration?.rateLimitSeconds ?? this.apiConfiguration?.rateLimitSeconds ?? 0
38563852

3857-
if (rateLimitSeconds <= 0 || !Task.lastGlobalApiRequestTime) {
3853+
const lastRequestTime = this.rateLimitClock.getLastRequestTime()
3854+
if (rateLimitSeconds <= 0 || !lastRequestTime) {
38583855
return
38593856
}
38603857

38613858
const now = performance.now()
3862-
const timeSinceLastRequest = now - Task.lastGlobalApiRequestTime
3859+
const timeSinceLastRequest = now - lastRequestTime
38633860
const rateLimitDelay = Math.ceil(
38643861
Math.min(rateLimitSeconds, Math.max(0, rateLimitSeconds * 1000 - timeSinceLastRequest) / 1000),
38653862
)
@@ -3907,7 +3904,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
39073904
// timestamp earlier to include the environment details build. We still set it
39083905
// here for direct callers (tests) and for the case where we didn't rate-limit
39093906
// in the caller.
3910-
Task.lastGlobalApiRequestTime = performance.now()
3907+
this.rateLimitClock.recordRequest()
39113908

39123909
const systemPrompt = await this.getSystemPrompt()
39133910
const { contextTokens } = this.getTokenUsage()
@@ -4282,8 +4279,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
42824279
// Respect provider rate limit window
42834280
let rateLimitDelay = 0
42844281
const rateLimit = (state?.apiConfiguration ?? this.apiConfiguration)?.rateLimitSeconds || 0
4285-
if (Task.lastGlobalApiRequestTime && rateLimit > 0) {
4286-
const elapsed = performance.now() - Task.lastGlobalApiRequestTime
4282+
const lastRequestTime = this.rateLimitClock.getLastRequestTime()
4283+
if (lastRequestTime && rateLimit > 0) {
4284+
const elapsed = performance.now() - lastRequestTime
42874285
rateLimitDelay = Math.ceil(Math.min(rateLimit, Math.max(0, rateLimit * 1000 - elapsed) / 1000))
42884286
}
42894287

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { RateLimitClock, createRateLimitClock } from "../RateLimitClock"
2+
3+
describe("RateLimitClock", () => {
4+
it("returns undefined when no request has been recorded", () => {
5+
const clock = createRateLimitClock()
6+
expect(clock.getLastRequestTime()).toBeUndefined()
7+
})
8+
9+
it("records a request and returns a timestamp", () => {
10+
const clock = createRateLimitClock()
11+
clock.recordRequest()
12+
const time = clock.getLastRequestTime()
13+
expect(time).toBeDefined()
14+
expect(time).toBeGreaterThan(0)
15+
})
16+
17+
it("updates timestamp on subsequent calls", () => {
18+
const clock = createRateLimitClock()
19+
clock.recordRequest()
20+
const first = clock.getLastRequestTime()!
21+
clock.recordRequest()
22+
const second = clock.getLastRequestTime()!
23+
expect(second).toBeGreaterThanOrEqual(first)
24+
})
25+
26+
it("isolates state between different clocks", () => {
27+
const clock1 = createRateLimitClock()
28+
const clock2 = createRateLimitClock()
29+
30+
clock1.recordRequest()
31+
32+
expect(clock1.getLastRequestTime()).toBeDefined()
33+
expect(clock2.getLastRequestTime()).toBeUndefined()
34+
})
35+
})

0 commit comments

Comments
 (0)