feat(dashboard): Display environment queue length limits on queues and limits page
This commit is contained in:
@@ -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: {
|
||||
|
||||
+2
-3
@@ -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">
|
||||
|
||||
+34
-2
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user