chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { EnvironmentPauseSource, type PrismaClientOrTransaction } from "@trigger.dev/database";
|
||||
import { prisma } from "~/db.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { getManualPauseEnvironmentResult } from "~/v3/services/billingLimit/manualPauseEnvironmentGuard.server";
|
||||
import { updateEnvConcurrencyLimits } from "../runQueue.server";
|
||||
import { WithRunEngine } from "./baseService.server";
|
||||
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
||||
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
|
||||
|
||||
export type PauseStatus = "paused" | "resumed";
|
||||
|
||||
export type PauseEnvironmentResult =
|
||||
| {
|
||||
success: true;
|
||||
state: PauseStatus;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
export class PauseEnvironmentService extends WithRunEngine {
|
||||
constructor(protected readonly _prisma: PrismaClientOrTransaction = prisma) {
|
||||
super({ prisma });
|
||||
}
|
||||
|
||||
public async call(
|
||||
environment: AuthenticatedEnvironment,
|
||||
action: PauseStatus
|
||||
): Promise<PauseEnvironmentResult> {
|
||||
try {
|
||||
const org = await this._prisma.organization.findFirst({
|
||||
where: {
|
||||
id: environment.organizationId,
|
||||
},
|
||||
select: {
|
||||
runsEnabled: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!org) {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const previousPauseState = await this._prisma.runtimeEnvironment.findFirst({
|
||||
where: { id: environment.id },
|
||||
select: {
|
||||
paused: true,
|
||||
pauseSource: true,
|
||||
},
|
||||
});
|
||||
|
||||
const manualPauseGuard = getManualPauseEnvironmentResult(
|
||||
action,
|
||||
previousPauseState?.pauseSource
|
||||
);
|
||||
if (!manualPauseGuard.proceed) {
|
||||
if (manualPauseGuard.success) {
|
||||
return {
|
||||
success: true,
|
||||
state: manualPauseGuard.state,
|
||||
};
|
||||
}
|
||||
// Expected, user-actionable guard result, not an error: return it as a failure
|
||||
// result so it doesn't reach Sentry via the catch below.
|
||||
return {
|
||||
success: false,
|
||||
error: manualPauseGuard.error,
|
||||
};
|
||||
}
|
||||
|
||||
if (!org.runsEnabled && action === "resumed") {
|
||||
throw new Error(
|
||||
"Runs are disabled for this organization. Your free plan has probably been exceeded. If not please contact support."
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "resumed") {
|
||||
const resumed = await this._prisma.runtimeEnvironment.updateMany({
|
||||
where: {
|
||||
id: environment.id,
|
||||
// NOT on a nullable field excludes NULL rows in Prisma, which made
|
||||
// user-paused envs (pauseSource null) unresumable.
|
||||
OR: [
|
||||
{ pauseSource: null },
|
||||
{ NOT: { pauseSource: EnvironmentPauseSource.BILLING_LIMIT } },
|
||||
],
|
||||
},
|
||||
data: {
|
||||
paused: false,
|
||||
pauseSource: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (resumed.count === 0) {
|
||||
// Raced into the paused state after the guard read above: expected,
|
||||
// return as a failure result rather than throwing to Sentry.
|
||||
return {
|
||||
success: false,
|
||||
error:
|
||||
"This environment is paused because your organization reached its billing limit. Resolve the limit on the billing limits settings page to resume.",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
await this._prisma.runtimeEnvironment.update({
|
||||
where: { id: environment.id },
|
||||
data: { paused: true },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === "paused") {
|
||||
logger.debug("PauseEnvironmentService: pausing environment", {
|
||||
environmentId: environment.id,
|
||||
});
|
||||
await updateEnvConcurrencyLimits(environment, 0);
|
||||
} else {
|
||||
logger.debug("PauseEnvironmentService: resuming environment", {
|
||||
environmentId: environment.id,
|
||||
});
|
||||
await updateEnvConcurrencyLimits(environment);
|
||||
}
|
||||
} catch (error) {
|
||||
await this._prisma.runtimeEnvironment.update({
|
||||
where: { id: environment.id },
|
||||
data: {
|
||||
paused: previousPauseState?.paused ?? action === "resumed",
|
||||
pauseSource: previousPauseState?.pauseSource ?? null,
|
||||
},
|
||||
});
|
||||
// Rollback still wrote the env row; drop any cached copy before rethrowing.
|
||||
controlPlaneResolver.invalidateEnvironment(environment.id);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// The env's `paused` state changed in the control-plane; drop any cached copy.
|
||||
controlPlaneResolver.invalidateEnvironment(environment.id);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
state: action,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("PauseEnvironmentService: error pausing environment", {
|
||||
action,
|
||||
environmentId: environment.id,
|
||||
error,
|
||||
});
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user