Add application job enqueue limits guard on all message queue drivers (#23570)
## Context
Applications trigger logic functions from several paths (cron, database
events, HTTP routes, install/connect hooks). Without a cap, a single app
can flood the `logic-function-queue` and starve it. This adds enqueue
limits scoped to that queue, mirroring the existing per-application API
rate limiting.
## What changed
`JobEnqueueThrottlerGuard` reuses the `ThrottlerService` token bucket
(same primitive as the API rate limiter) with two tiers:
- **Per application installation**
(`enqueue:throttler:application:{applicationId}`) - lower ceiling,
`APPLICATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT` (default 500).
- **Per application registration**
(`enqueue:throttler:application-registration:{applicationRegistrationId}`)
- higher ceiling shared across all workspaces that installed the same
app, `APPLICATION_REGISTRATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT` (default
2000).
Both share `APPLICATION_JOB_ENQUEUE_RATE_LIMITING_TTL_IN_MS` (default
60s). Both buckets are checked before either is debited, so a rejection
on one tier never burns quota on the other.
**Per-queue guarding.** A `ThrottledMessageQueueDriver` decorator wraps
the concrete driver (BullMQ or Sync) in the `QUEUE_DRIVER` provider and
passes the `queueName` to the guard. The guard only acts on queues in
`GUARDED_ENQUEUE_QUEUES` (currently just `logic-function-queue`); every
other queue is untouched.
**Required application context.** The guard reads a dedicated
`applicationJobEnqueueContextStorage` (AsyncLocalStorage) carrying `{
applicationId, applicationRegistrationId }`, and throws if a
guarded-queue enqueue runs without both. Every logic-function-queue
enqueue site wraps its `add`/`bulkAdd` in
`withApplicationJobEnqueueContext`:
- cron trigger
- database-event trigger (groups logic functions by application, one
batch per application)
- server route trigger
- application post-install hook
- connection-provider on-connect hook
When the limit is reached the guard records a
`JobEnqueueApplicationRateLimited` metric and throws
`ThrottlerException` (mapped to 429 by the existing handlers).
## Files
- `message-queue/guards/job-enqueue-throttler.guard.ts` - the guard
(new)
- `message-queue/storage/application-job-enqueue-context.storage.ts` -
dedicated enqueue context (new)
- `message-queue/constants/guarded-enqueue-queues.constant.ts` -
guarded-queue set (new)
- `message-queue/drivers/throttled-message-queue.driver.ts` - decorator
driver wrapping any driver (new)
- `message-queue/message-queue-core.module.ts` - wires the guard into
the driver provider
- `twenty-config/config-variables.ts` - three tunable `RATE_LIMITING`
config variables
- `metrics/types/metrics-keys.type.ts` -
`JobEnqueueApplicationRateLimited` key
- the 5 enqueue sites above - inject the enqueue context
## Notes / trade-offs
- A logic function whose application has no `applicationRegistrationId`
is skipped at the trigger paths (the install hook throws), matching how
the server route trigger already treats "not linked to a registration".
- `addCron` is left ungated (idempotent upsert).
- Default limits are placeholders and tunable per instance.
## Testing
- `JobEnqueueThrottlerGuard` unit tests (7 cases): non-guarded queue
skip, throw on missing/partial context, two-tier throttling with
distinct limits, per-item token consumption on bulk, no partial debit
when either tier is exhausted.
- Updated `connection-provider-oauth-flow.service.spec.ts` for the new
cache key.
- `npx nx typecheck twenty-server` passes; oxlint + oxfmt clean on
changed files.
This commit is contained in:
+1
@@ -572,6 +572,7 @@ export class ApplicationInstallService {
|
||||
},
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+21
-1
@@ -5,6 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { findActiveFlatApplicationById } from 'src/engine/core-modules/application/utils/find-active-flat-application-by-id.util';
|
||||
import { CronTriggerDeduplicationService } from 'src/engine/core-modules/cron/services/cron-trigger-deduplication.service';
|
||||
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
@@ -12,6 +13,7 @@ import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decora
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { ApplicationJobEnqueueThrottlerService } from 'src/engine/core-modules/message-queue/services/application-job-enqueue-throttler.service';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import {
|
||||
@@ -34,6 +36,7 @@ export class CronTriggerCronJob {
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly exceptionHandlerService: ExceptionHandlerService,
|
||||
private readonly cronTriggerDeduplicationService: CronTriggerDeduplicationService,
|
||||
private readonly applicationJobEnqueueThrottlerService: ApplicationJobEnqueueThrottlerService,
|
||||
) {}
|
||||
|
||||
@Process(CronTriggerCronJob.name)
|
||||
@@ -50,9 +53,10 @@ export class CronTriggerCronJob {
|
||||
|
||||
for (const activeWorkspace of activeWorkspaces) {
|
||||
try {
|
||||
const { flatLogicFunctionMaps } =
|
||||
const { flatLogicFunctionMaps, flatApplicationMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(activeWorkspace.id, [
|
||||
'flatLogicFunctionMaps',
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
const logicFunctions = Object.values(
|
||||
@@ -85,6 +89,22 @@ export class CronTriggerCronJob {
|
||||
continue;
|
||||
}
|
||||
|
||||
const application = findActiveFlatApplicationById(
|
||||
flatApplicationMaps,
|
||||
logicFunction.applicationId,
|
||||
);
|
||||
const applicationRegistrationId =
|
||||
application?.applicationRegistrationId;
|
||||
|
||||
if (!isDefined(applicationRegistrationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.applicationJobEnqueueThrottlerService.throttleOrThrow({
|
||||
applicationId: logicFunction.applicationId,
|
||||
applicationRegistrationId,
|
||||
});
|
||||
|
||||
await this.messageQueueService.add<LogicFunctionTriggerJobData>(
|
||||
LogicFunctionTriggerJob.name,
|
||||
{
|
||||
|
||||
+55
-11
@@ -2,10 +2,12 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import type { ObjectRecordEvent } from 'twenty-shared/database-events';
|
||||
|
||||
import { findActiveFlatApplicationById } from 'src/engine/core-modules/application/utils/find-active-flat-application-by-id.util';
|
||||
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
|
||||
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
|
||||
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
|
||||
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
|
||||
import { ApplicationJobEnqueueThrottlerService } from 'src/engine/core-modules/message-queue/services/application-job-enqueue-throttler.service';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { transformEventBatchToEventPayloads } from 'src/engine/core-modules/logic-function/logic-function-trigger/triggers/database-event/utils/transform-event-batch-to-event-payloads';
|
||||
import {
|
||||
@@ -21,14 +23,15 @@ export class CallDatabaseEventTriggerJobsJob {
|
||||
@InjectMessageQueue(MessageQueue.logicFunctionQueue)
|
||||
private readonly messageQueueService: MessageQueueService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly applicationJobEnqueueThrottlerService: ApplicationJobEnqueueThrottlerService,
|
||||
) {}
|
||||
|
||||
@Process(CallDatabaseEventTriggerJobsJob.name)
|
||||
async handle(workspaceEventBatch: WorkspaceEventBatch<ObjectRecordEvent>) {
|
||||
const { flatLogicFunctionMaps } =
|
||||
const { flatLogicFunctionMaps, flatApplicationMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(
|
||||
workspaceEventBatch.workspaceId,
|
||||
['flatLogicFunctionMaps'],
|
||||
['flatLogicFunctionMaps', 'flatApplicationMaps'],
|
||||
);
|
||||
|
||||
const logicFunctionsWithDatabaseEventTrigger = Object.values(
|
||||
@@ -51,16 +54,57 @@ export class CallDatabaseEventTriggerJobsJob {
|
||||
}),
|
||||
);
|
||||
|
||||
const logicFunctionPayloads = transformEventBatchToEventPayloads({
|
||||
logicFunctions: logicFunctionsToTrigger,
|
||||
workspaceEventBatch,
|
||||
});
|
||||
const logicFunctionsByApplicationId = new Map<
|
||||
string,
|
||||
typeof logicFunctionsToTrigger
|
||||
>();
|
||||
|
||||
await this.messageQueueService.bulkAdd<LogicFunctionTriggerJobData>(
|
||||
LogicFunctionTriggerJob.name,
|
||||
logicFunctionPayloads,
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
for (const logicFunction of logicFunctionsToTrigger) {
|
||||
const applicationLogicFunctions =
|
||||
logicFunctionsByApplicationId.get(logicFunction.applicationId) ?? [];
|
||||
|
||||
applicationLogicFunctions.push(logicFunction);
|
||||
logicFunctionsByApplicationId.set(
|
||||
logicFunction.applicationId,
|
||||
applicationLogicFunctions,
|
||||
);
|
||||
}
|
||||
|
||||
for (const [
|
||||
applicationId,
|
||||
logicFunctions,
|
||||
] of logicFunctionsByApplicationId) {
|
||||
const application = findActiveFlatApplicationById(
|
||||
flatApplicationMaps,
|
||||
applicationId,
|
||||
);
|
||||
const applicationRegistrationId = application?.applicationRegistrationId;
|
||||
|
||||
if (!isDefined(applicationRegistrationId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const logicFunctionPayloads = transformEventBatchToEventPayloads({
|
||||
logicFunctions,
|
||||
workspaceEventBatch,
|
||||
});
|
||||
|
||||
if (logicFunctionPayloads.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.applicationJobEnqueueThrottlerService.throttleOrThrow({
|
||||
applicationId,
|
||||
applicationRegistrationId,
|
||||
jobCount: logicFunctionPayloads.length,
|
||||
});
|
||||
|
||||
await this.messageQueueService.bulkAdd<LogicFunctionTriggerJobData>(
|
||||
LogicFunctionTriggerJob.name,
|
||||
logicFunctionPayloads,
|
||||
{ retryLimit: 3 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private shouldTriggerJob({
|
||||
|
||||
+16
-1
@@ -20,9 +20,11 @@ import {
|
||||
ConfigurableModuleClass,
|
||||
type OPTIONS_TYPE,
|
||||
} from 'src/engine/core-modules/message-queue/message-queue.module-definition';
|
||||
import { ApplicationJobEnqueueThrottlerService } from 'src/engine/core-modules/message-queue/services/application-job-enqueue-throttler.service';
|
||||
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
|
||||
import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||
|
||||
@Global()
|
||||
@Module({})
|
||||
@@ -43,13 +45,20 @@ export class MessageQueueCoreModule extends ConfigurableModuleClass {
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
imports: [
|
||||
...(dynamicModule.imports ?? []),
|
||||
MetricsModule,
|
||||
ThrottlerModule,
|
||||
],
|
||||
providers: [
|
||||
...(dynamicModule.providers ?? []),
|
||||
ApplicationJobEnqueueThrottlerService,
|
||||
driverProvider,
|
||||
...queueProviders,
|
||||
],
|
||||
exports: [
|
||||
...(dynamicModule.exports ?? []),
|
||||
ApplicationJobEnqueueThrottlerService,
|
||||
...Object.values(MessageQueue).map((queueName) =>
|
||||
getQueueToken(queueName),
|
||||
),
|
||||
@@ -78,14 +87,20 @@ export class MessageQueueCoreModule extends ConfigurableModuleClass {
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
imports: [...(dynamicModule.imports ?? []), MetricsModule],
|
||||
imports: [
|
||||
...(dynamicModule.imports ?? []),
|
||||
MetricsModule,
|
||||
ThrottlerModule,
|
||||
],
|
||||
providers: [
|
||||
...(dynamicModule.providers ?? []),
|
||||
ApplicationJobEnqueueThrottlerService,
|
||||
driverProvider,
|
||||
...queueProviders,
|
||||
],
|
||||
exports: [
|
||||
...(dynamicModule.exports ?? []),
|
||||
ApplicationJobEnqueueThrottlerService,
|
||||
...Object.values(MessageQueue).map((queueName) =>
|
||||
getQueueToken(queueName),
|
||||
),
|
||||
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { ApplicationJobEnqueueThrottlerService } from 'src/engine/core-modules/message-queue/services/application-job-enqueue-throttler.service';
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import { ThrottlerException } from 'src/engine/core-modules/throttler/throttler.exception';
|
||||
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
const CONFIG = {
|
||||
APPLICATION_JOB_ENQUEUE_RATE_LIMITING_TTL_IN_MS: 60_000,
|
||||
APPLICATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT: 500,
|
||||
APPLICATION_REGISTRATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT: 2000,
|
||||
} as const;
|
||||
|
||||
const context = {
|
||||
applicationId: 'application-id',
|
||||
applicationRegistrationId: 'registration-id',
|
||||
};
|
||||
|
||||
const APPLICATION_KEY = 'enqueue:throttler:application:application-id';
|
||||
const REGISTRATION_KEY =
|
||||
'enqueue:throttler:application-registration:registration-id';
|
||||
|
||||
describe('ApplicationJobEnqueueThrottlerService', () => {
|
||||
let service: ApplicationJobEnqueueThrottlerService;
|
||||
let throttlerService: {
|
||||
getAvailableTokensCount: jest.Mock;
|
||||
consumeTokens: jest.Mock;
|
||||
};
|
||||
let metricsService: { incrementCounterForEvent: jest.Mock };
|
||||
|
||||
const mockAvailableTokens = (byKey: Record<string, number>) => {
|
||||
throttlerService.getAvailableTokensCount.mockImplementation(
|
||||
async (key: string) => byKey[key] ?? 0,
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
throttlerService = {
|
||||
getAvailableTokensCount: jest.fn().mockResolvedValue(1000),
|
||||
consumeTokens: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
metricsService = {
|
||||
incrementCounterForEvent: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ApplicationJobEnqueueThrottlerService,
|
||||
{ provide: ThrottlerService, useValue: throttlerService },
|
||||
{ provide: MetricsService, useValue: metricsService },
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
get: (key: keyof typeof CONFIG) => CONFIG[key],
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get(ApplicationJobEnqueueThrottlerService);
|
||||
});
|
||||
|
||||
it('debits both application and registration buckets with distinct limits', async () => {
|
||||
await service.throttleOrThrow(context);
|
||||
|
||||
expect(throttlerService.consumeTokens).toHaveBeenCalledWith(
|
||||
APPLICATION_KEY,
|
||||
1,
|
||||
CONFIG.APPLICATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT,
|
||||
CONFIG.APPLICATION_JOB_ENQUEUE_RATE_LIMITING_TTL_IN_MS,
|
||||
);
|
||||
expect(throttlerService.consumeTokens).toHaveBeenCalledWith(
|
||||
REGISTRATION_KEY,
|
||||
1,
|
||||
CONFIG.APPLICATION_REGISTRATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT,
|
||||
CONFIG.APPLICATION_JOB_ENQUEUE_RATE_LIMITING_TTL_IN_MS,
|
||||
);
|
||||
});
|
||||
|
||||
it('consumes one token per job for bulk enqueue', async () => {
|
||||
await service.throttleOrThrow({ ...context, jobCount: 5 });
|
||||
|
||||
expect(throttlerService.consumeTokens).toHaveBeenCalledWith(
|
||||
APPLICATION_KEY,
|
||||
5,
|
||||
expect.any(Number),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not debit any bucket when the registration bucket is exhausted', async () => {
|
||||
mockAvailableTokens({
|
||||
[APPLICATION_KEY]: 1000,
|
||||
[REGISTRATION_KEY]: 0,
|
||||
});
|
||||
|
||||
await expect(service.throttleOrThrow(context)).rejects.toThrow(
|
||||
ThrottlerException,
|
||||
);
|
||||
|
||||
expect(throttlerService.consumeTokens).not.toHaveBeenCalled();
|
||||
expect(metricsService.incrementCounterForEvent).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
key: MetricsKeys.JobEnqueueApplicationRateLimited,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not debit any bucket when the application bucket is exhausted', async () => {
|
||||
mockAvailableTokens({
|
||||
[APPLICATION_KEY]: 0,
|
||||
[REGISTRATION_KEY]: 1000,
|
||||
});
|
||||
|
||||
await expect(service.throttleOrThrow(context)).rejects.toThrow(
|
||||
ThrottlerException,
|
||||
);
|
||||
|
||||
expect(throttlerService.consumeTokens).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { MetricsService } from 'src/engine/core-modules/metrics/metrics.service';
|
||||
import { MetricsKeys } from 'src/engine/core-modules/metrics/types/metrics-keys.type';
|
||||
import {
|
||||
ThrottlerException,
|
||||
ThrottlerExceptionCode,
|
||||
} from 'src/engine/core-modules/throttler/throttler.exception';
|
||||
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationJobEnqueueThrottlerService {
|
||||
constructor(
|
||||
private readonly throttlerService: ThrottlerService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly metricsService: MetricsService,
|
||||
) {}
|
||||
|
||||
async throttleOrThrow({
|
||||
applicationId,
|
||||
applicationRegistrationId,
|
||||
jobCount = 1,
|
||||
}: {
|
||||
applicationId: string;
|
||||
applicationRegistrationId: string;
|
||||
jobCount?: number;
|
||||
}): Promise<void> {
|
||||
const timeWindow = this.twentyConfigService.get(
|
||||
'APPLICATION_JOB_ENQUEUE_RATE_LIMITING_TTL_IN_MS',
|
||||
);
|
||||
const applicationKey = `enqueue:throttler:application:${applicationId}`;
|
||||
const applicationLimit = this.twentyConfigService.get(
|
||||
'APPLICATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT',
|
||||
);
|
||||
const registrationKey = `enqueue:throttler:application-registration:${applicationRegistrationId}`;
|
||||
const registrationLimit = this.twentyConfigService.get(
|
||||
'APPLICATION_REGISTRATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT',
|
||||
);
|
||||
|
||||
const [applicationTokens, registrationTokens] = await Promise.all([
|
||||
this.throttlerService.getAvailableTokensCount(
|
||||
applicationKey,
|
||||
applicationLimit,
|
||||
timeWindow,
|
||||
),
|
||||
this.throttlerService.getAvailableTokensCount(
|
||||
registrationKey,
|
||||
registrationLimit,
|
||||
timeWindow,
|
||||
),
|
||||
]);
|
||||
|
||||
if (applicationTokens < jobCount || registrationTokens < jobCount) {
|
||||
await this.metricsService.incrementCounterForEvent({
|
||||
key: MetricsKeys.JobEnqueueApplicationRateLimited,
|
||||
shouldStoreInCache: false,
|
||||
attributes: {
|
||||
application_id: applicationId,
|
||||
application_registration_id: applicationRegistrationId,
|
||||
},
|
||||
});
|
||||
|
||||
throw new ThrottlerException(
|
||||
'Application job enqueue limit reached',
|
||||
ThrottlerExceptionCode.LIMIT_REACHED,
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
this.throttlerService.consumeTokens(
|
||||
applicationKey,
|
||||
jobCount,
|
||||
applicationLimit,
|
||||
timeWindow,
|
||||
),
|
||||
this.throttlerService.consumeTokens(
|
||||
registrationKey,
|
||||
jobCount,
|
||||
registrationLimit,
|
||||
timeWindow,
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ export enum MetricsKeys {
|
||||
SignUpSuccess = 'sign-up/success',
|
||||
CommonApiQueryRateLimited = 'common-api-query/rate-limited',
|
||||
CommonApiApplicationQueryRateLimited = 'common-api-query/application-rate-limited',
|
||||
JobEnqueueApplicationRateLimited = 'job/enqueue-application-rate-limited',
|
||||
JobCompleted = 'job/completed',
|
||||
JobFailed = 'job/failed',
|
||||
JobStalled = 'job/stalled',
|
||||
|
||||
@@ -1521,6 +1521,33 @@ export class ConfigVariables {
|
||||
@CastToPositiveNumber()
|
||||
APPLICATION_API_RATE_LIMITING_LIMIT = 500;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.RATE_LIMITING,
|
||||
description:
|
||||
'Time-to-live for application job enqueue rate limiting in milliseconds',
|
||||
type: ConfigVariableType.NUMBER,
|
||||
})
|
||||
@CastToPositiveNumber()
|
||||
APPLICATION_JOB_ENQUEUE_RATE_LIMITING_TTL_IN_MS = 60_000;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.RATE_LIMITING,
|
||||
description:
|
||||
'Maximum number of jobs a single application installation can enqueue in the rate limiting window',
|
||||
type: ConfigVariableType.NUMBER,
|
||||
})
|
||||
@CastToPositiveNumber()
|
||||
APPLICATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT = 500;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.RATE_LIMITING,
|
||||
description:
|
||||
'Maximum number of jobs enqueued per application registration across all workspaces in the rate limiting window',
|
||||
type: ConfigVariableType.NUMBER,
|
||||
})
|
||||
@CastToPositiveNumber()
|
||||
APPLICATION_REGISTRATION_JOB_ENQUEUE_RATE_LIMITING_LIMIT = 2000;
|
||||
|
||||
@CastToPositiveNumber()
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.RATE_LIMITING,
|
||||
|
||||
Reference in New Issue
Block a user