feat(dashboard): Display environment queue length limits on queues and limits page

This commit is contained in:
Eric Allam
2026-01-30 19:01:34 +00:00
parent b221719c09
commit a68ef306e8
4 changed files with 92 additions and 20 deletions
@@ -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,
};
}
}
@@ -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: {
@@ -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 (
<div className="flex flex-col gap-3">
@@ -68,6 +68,7 @@ import { EnvironmentQueuePresenter } from "~/presenters/v3/EnvironmentQueuePrese
import { QueueListPresenter } from "~/presenters/v3/QueueListPresenter.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { formatNumberCompact } from "~/utils/numberFormatter";
import {
concurrencyPath,
docsPath,
@@ -345,7 +346,27 @@ export default function Page() {
<BigNumber
title="Queued"
value={environment.queued}
suffix={env.paused && environment.queued > 0 ? "paused" : undefined}
suffix={
environment.queueSizeLimit ? (
<span className="flex items-center gap-1">
<span className="text-text-dimmed">/</span>
<span
className={getQueueUsageColorClass(
environment.queued,
environment.queueSizeLimit
)}
>
{formatNumberCompact(environment.queueSizeLimit)}
</span>
<InfoIconTooltip
content="Maximum pending runs in this environment"
contentClassName="max-w-xs"
/>
</span>
) : env.paused && environment.queued > 0 ? (
"paused"
) : undefined
}
animate
accessory={
<div className="flex items-start gap-1">
@@ -364,7 +385,10 @@ export default function Page() {
/>
</div>
}
valueClassName={env.paused ? "text-warning" : undefined}
valueClassName={
getQueueUsageColorClass(environment.queued, environment.queueSizeLimit) ??
(env.paused ? "text-warning" : undefined)
}
compactThreshold={1000000}
/>
<BigNumber
@@ -1118,3 +1142,11 @@ function BurstFactorTooltip({
/>
);
}
function getQueueUsageColorClass(current: number, limit: number | null): string | undefined {
if (!limit) return undefined;
const percentage = current / limit;
if (percentage >= 1) return "text-error";
if (percentage >= 0.9) return "text-warning";
return undefined;
}