Improve workflow throttling logic (#16260)

- if >5000 workflows per hour, new ones should failed
- if >100 workflow per min, new ones should be set as not started.
Except manual trigger
- when enqueued, we check if there a not started workflows that may be
queued. If yes, we call the associated job

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
Thomas Trompette
2025-12-03 10:01:36 +01:00
committed by GitHub
parent 288db78abd
commit a0f196e871
25 changed files with 538 additions and 386 deletions
@@ -20,7 +20,7 @@ export enum MetricsKeys {
WorkflowRunCompleted = 'workflow-run/completed',
WorkflowRunFailed = 'workflow-run/failed',
WorkflowRunStopped = 'workflow-run/stopped',
WorkflowRunFailedThrottled = 'workflow-run/failed/throttled',
WorkflowRunThrottled = 'workflow-run/throttled',
WorkflowRunFailedToEnqueue = 'workflow-run/failed/to-enqueue',
AIToolExecutionFailed = 'ai-tool-execution/failed',
AIToolExecutionSucceeded = 'ai-tool-execution/succeeded',
@@ -20,18 +20,14 @@ export class ThrottlerService {
tokensToConsume: number,
maxTokens: number,
timeWindow: number,
): Promise<void> {
): Promise<number> {
const now = Date.now();
const refillRate = maxTokens / timeWindow;
const { tokens, lastRefillAt } = (await this.cacheStorage.get<{
tokens: number;
lastRefillAt: number;
}>(key)) || { tokens: maxTokens, lastRefillAt: now };
const refillAmount = Math.floor((now - lastRefillAt) * refillRate);
const availableTokens = Math.min(tokens + refillAmount, maxTokens);
const availableTokens = await this.getAvailableTokensCount(
key,
maxTokens,
timeWindow,
now,
);
if (availableTokens < tokensToConsume) {
throw new ThrottlerException(
@@ -48,5 +44,49 @@ export class ThrottlerService {
},
timeWindow * 2,
);
return availableTokens - tokensToConsume;
}
async consumeTokens(
key: string,
tokensToConsume: number,
maxTokens: number,
timeWindow: number,
) {
const now = Date.now();
const availableTokens = await this.getAvailableTokensCount(
key,
maxTokens,
timeWindow,
now,
);
await this.cacheStorage.set(
key,
{
tokens: availableTokens - tokensToConsume,
lastRefillAt: now,
},
timeWindow * 2,
);
}
async getAvailableTokensCount(
key: string,
maxTokens: number,
timeWindow: number,
now = Date.now(),
): Promise<number> {
const refillRate = maxTokens / timeWindow;
const { tokens, lastRefillAt } = (await this.cacheStorage.get<{
tokens: number;
lastRefillAt: number;
}>(key)) || { tokens: maxTokens, lastRefillAt: now };
const refillAmount = Math.floor((now - lastRefillAt) * refillRate);
return Math.min(tokens + refillAmount, maxTokens);
}
}
@@ -1219,19 +1219,39 @@ export class ConfigVariables {
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.RATE_LIMITING,
description: 'Throttle limit for workflow execution',
description:
'Throttle limit for workflow execution. Remaining will not be enqueued immediately.',
type: ConfigVariableType.NUMBER,
})
@CastToPositiveNumber()
WORKFLOW_EXEC_THROTTLE_LIMIT = 100;
WORKFLOW_EXEC_SOFT_THROTTLE_LIMIT = 100;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.RATE_LIMITING,
description: 'Time-to-live for workflow execution throttle in milliseconds',
description:
'Time-to-live for workflow execution throttle in milliseconds. Remaining will not be enqueued immediately.',
type: ConfigVariableType.NUMBER,
})
@CastToPositiveNumber()
WORKFLOW_EXEC_THROTTLE_TTL = 60_000;
WORKFLOW_EXEC_SOFT_THROTTLE_TTL = 60_000;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.RATE_LIMITING,
description:
'Throttle limit for workflow execution. Remaining will be marked as failed.',
type: ConfigVariableType.NUMBER,
})
@CastToPositiveNumber()
WORKFLOW_EXEC_HARD_THROTTLE_LIMIT = 5000;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.RATE_LIMITING,
description:
'Time-to-live for workflow execution throttle in milliseconds. Remaining will be marked as failed.',
type: ConfigVariableType.NUMBER,
})
@CastToPositiveNumber()
WORKFLOW_EXEC_HARD_THROTTLE_TTL = 3_600_000; // 1 hour;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.CAPTCHA_CONFIG,