diff --git a/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts b/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts index f408511a8..e8b146151 100644 --- a/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts +++ b/apps/webapp/app/presenters/v3/EnvironmentQueuePresenter.server.ts @@ -1,3 +1,4 @@ +import { env } from "~/env.server"; import { type AuthenticatedEnvironment } from "~/services/apiAuth.server"; import { marqs } from "~/v3/marqs/index.server"; import { engine } from "~/v3/runEngine.server"; @@ -9,6 +10,7 @@ export type Environment = { concurrencyLimit: number; burstFactor: number; runsEnabled: boolean; + queueSizeLimit: number | null; }; export class EnvironmentQueuePresenter extends BasePresenter { @@ -30,6 +32,8 @@ export class EnvironmentQueuePresenter extends BasePresenter { }, select: { runsEnabled: true, + maximumDevQueueSize: true, + maximumDeployedQueueSize: true, }, }); @@ -37,12 +41,18 @@ export class EnvironmentQueuePresenter extends BasePresenter { throw new Error("Organization not found"); } + const queueSizeLimit = + environment.type === "DEVELOPMENT" + ? (organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE ?? null) + : (organization.maximumDeployedQueueSize ?? env.MAXIMUM_DEPLOYED_QUEUE_SIZE ?? null); + return { running, queued, concurrencyLimit: environment.maximumConcurrencyLimit, burstFactor: environment.concurrencyLimitBurstFactor.toNumber(), runsEnabled: environment.type === "DEVELOPMENT" || organization.runsEnabled, + queueSizeLimit, }; } } diff --git a/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts b/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts index 11b66d6c0..e5e09ad5b 100644 --- a/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts +++ b/apps/webapp/app/presenters/v3/LimitsPresenter.server.ts @@ -12,6 +12,7 @@ import { BasePresenter } from "./basePresenter.server"; import { singleton } from "~/utils/singleton"; import { logger } from "~/services/logger.server"; import { CheckScheduleService } from "~/v3/services/checkSchedule.server"; +import { engine } from "~/v3/runEngine.server"; // Create a singleton Redis client for rate limit queries const rateLimitRedisClient = singleton("rateLimitQueryRedisClient", () => @@ -66,8 +67,7 @@ export type LimitsResult = { logRetentionDays: QuotaInfo | null; realtimeConnections: QuotaInfo | null; batchProcessingConcurrency: QuotaInfo; - devQueueSize: QuotaInfo; - deployedQueueSize: QuotaInfo; + queueSize: QuotaInfo; }; features: { hasStagingEnvironment: FeatureInfo; @@ -167,6 +167,32 @@ export class LimitsPresenter extends BasePresenter { batchRateLimitConfig ); + // Get current queue size for this environment + const runtimeEnv = await this._replica.runtimeEnvironment.findFirst({ + where: { id: environmentId }, + select: { + id: true, + type: true, + organizationId: true, + projectId: true, + maximumConcurrencyLimit: true, + concurrencyLimitBurstFactor: true, + }, + }); + + let currentQueueSize = 0; + if (runtimeEnv) { + const engineEnv = { + id: runtimeEnv.id, + type: runtimeEnv.type, + maximumConcurrencyLimit: runtimeEnv.maximumConcurrencyLimit, + concurrencyLimitBurstFactor: runtimeEnv.concurrencyLimitBurstFactor, + organization: { id: runtimeEnv.organizationId }, + project: { id: runtimeEnv.projectId }, + }; + currentQueueSize = (await engine.lengthOfEnvQueue(engineEnv)) ?? 0; + } + // Get plan-level limits const schedulesLimit = limits?.schedules?.number ?? null; const teamMembersLimit = limits?.teamMembers?.number ?? null; @@ -282,19 +308,24 @@ export class LimitsPresenter extends BasePresenter { canExceed: true, isUpgradable: true, }, - devQueueSize: { - name: "Dev queue size", - description: "Maximum pending runs in development environments", - limit: organization.maximumDevQueueSize ?? null, - currentUsage: 0, // Would need to query Redis for this - source: organization.maximumDevQueueSize ? "override" : "default", - }, - deployedQueueSize: { - name: "Deployed queue size", - description: "Maximum pending runs in deployed environments", - limit: organization.maximumDeployedQueueSize ?? null, - currentUsage: 0, // Would need to query Redis for this - source: organization.maximumDeployedQueueSize ? "override" : "default", + queueSize: { + name: "Max queue size", + description: "Maximum pending runs in this environment", + limit: + runtimeEnv?.type === "DEVELOPMENT" + ? (organization.maximumDevQueueSize ?? env.MAXIMUM_DEV_QUEUE_SIZE ?? null) + : (organization.maximumDeployedQueueSize ?? env.MAXIMUM_DEPLOYED_QUEUE_SIZE ?? null), + currentUsage: currentQueueSize, + // "plan" = org has a value (typically set by billing sync) + // "default" = no org value, using env var fallback + source: + runtimeEnv?.type === "DEVELOPMENT" + ? organization.maximumDevQueueSize + ? "plan" + : "default" + : organization.maximumDeployedQueueSize + ? "plan" + : "default", }, }, features: { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx index dfaffe993..c979f096d 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx @@ -507,9 +507,8 @@ function QuotasSection({ // Include batch processing concurrency quotaRows.push(quotas.batchProcessingConcurrency); - // Add queue size quotas if set - if (quotas.devQueueSize.limit !== null) quotaRows.push(quotas.devQueueSize); - if (quotas.deployedQueueSize.limit !== null) quotaRows.push(quotas.deployedQueueSize); + // Add queue size quota if set + if (quotas.queueSize.limit !== null) quotaRows.push(quotas.queueSize); return (