refactor: remove deployment failure classifier and related tests

This commit is contained in:
Mauricio Siu
2026-08-13 15:03:35 -06:00
parent a4425cfb1d
commit 171c70492d
7 changed files with 11 additions and 601 deletions
@@ -233,10 +233,3 @@ export const apiFindAllByType = z.object({
"volumeBackup",
]),
});
export const apiFindFailedDeployments = z.object({
dateFrom: z.string().datetime().optional(),
dateTo: z.string().datetime().optional(),
projectId: z.string().optional(),
environmentId: z.string().optional(),
});
-1
View File
@@ -96,7 +96,6 @@ export * from "./utils/builders/utils";
export * from "./utils/cluster/upload";
export * from "./utils/crons/enterprise";
export * from "./utils/databases/rebuild";
export * from "./utils/deployment-failure-classifier";
export * from "./utils/docker/collision";
export * from "./utils/docker/compose";
export * from "./utils/docker/compose/configs";
+1 -180
View File
@@ -10,14 +10,12 @@ import {
type apiCreateDeploymentSchedule,
type apiCreateDeploymentServer,
type apiCreateDeploymentVolumeBackup,
type apiFindFailedDeployments,
applications,
compose,
deployments,
environments,
projects,
} from "@dokploy/server/db/schema";
import { classifyFailure } from "@dokploy/server/utils/deployment-failure-classifier";
import { removeDirectoryIfExistsContent } from "@dokploy/server/utils/filesystem/directory";
import {
execAsync,
@@ -25,7 +23,7 @@ import {
} from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { format } from "date-fns";
import { and, desc, eq, gte, inArray, lte, or, sql } from "drizzle-orm";
import { and, desc, eq, inArray, or, sql } from "drizzle-orm";
import type { z } from "zod";
import {
type Application,
@@ -944,183 +942,6 @@ export const findAllDeploymentsCentralized = async (
});
};
// Kept separate from getApplicationIdsInOrg/getComposeIdsInOrg to avoid touching that path.
type FailedDeploymentIdFilters = { projectId?: string; environmentId?: string };
async function getApplicationIdsForFailedDeployments(
orgId: string,
accessedServices: string[] | null,
filters: FailedDeploymentIdFilters,
): Promise<string[]> {
const conditions = [
eq(projects.organizationId, orgId),
...(accessedServices !== null
? [inArray(applications.applicationId, accessedServices)]
: []),
...(filters.projectId ? [eq(projects.projectId, filters.projectId)] : []),
...(filters.environmentId
? [eq(environments.environmentId, filters.environmentId)]
: []),
];
const rows = await db
.select({ applicationId: applications.applicationId })
.from(applications)
.innerJoin(
environments,
eq(applications.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(and(...conditions));
return rows.map((r) => r.applicationId);
}
async function getComposeIdsForFailedDeployments(
orgId: string,
accessedServices: string[] | null,
filters: FailedDeploymentIdFilters,
): Promise<string[]> {
const conditions = [
eq(projects.organizationId, orgId),
...(accessedServices !== null
? [inArray(compose.composeId, accessedServices)]
: []),
...(filters.projectId ? [eq(projects.projectId, filters.projectId)] : []),
...(filters.environmentId
? [eq(environments.environmentId, filters.environmentId)]
: []),
];
const rows = await db
.select({ composeId: compose.composeId })
.from(compose)
.innerJoin(
environments,
eq(compose.environmentId, environments.environmentId),
)
.innerJoin(projects, eq(environments.projectId, projects.projectId))
.where(and(...conditions));
return rows.map((r) => r.composeId);
}
const readLogTail = async (
logPath: string | null,
serverId: string | null | undefined,
) => {
if (!logPath) return "";
const command = `tail -c 4096 "${logPath}" 2>/dev/null || echo ""`;
const { stdout } = serverId
? await execAsyncRemote(serverId, command)
: await execAsync(command);
return stdout;
};
// Caps simultaneous SSH connections when classifying a batch of failures.
const LOG_TAIL_CONCURRENCY = 8;
async function mapWithConcurrency<T, R>(
items: T[],
limit: number,
fn: (item: T) => Promise<R>,
): Promise<R[]> {
const results = new Array<R>(items.length);
let nextIndex = 0;
const worker = async () => {
while (nextIndex < items.length) {
const current = nextIndex++;
results[current] = await fn(items[current] as T);
}
};
await Promise.all(
Array.from({ length: Math.min(limit, items.length) }, worker),
);
return results;
}
export type FailedDeploymentFilters = z.infer<typeof apiFindFailedDeployments>;
// Same org-wide scope as findAllDeploymentsCentralized, narrowed to failed deployments.
export const findFailedDeploymentsCentralized = async (
orgId: string,
accessedServices: string[] | null,
filters: FailedDeploymentFilters = {},
) => {
if (accessedServices !== null && accessedServices.length === 0) {
return [];
}
const idFilters: FailedDeploymentIdFilters = {
projectId: filters.projectId,
environmentId: filters.environmentId,
};
const [appIds, compIds] = await Promise.all([
getApplicationIdsForFailedDeployments(orgId, accessedServices, idFilters),
getComposeIdsForFailedDeployments(orgId, accessedServices, idFilters),
]);
if (appIds.length === 0 && compIds.length === 0) {
return [];
}
// createdAt is stored as text via toISOString(); normalize filters the same way.
const dateFrom = filters.dateFrom
? new Date(filters.dateFrom).toISOString()
: undefined;
const dateTo = filters.dateTo
? new Date(filters.dateTo).toISOString()
: undefined;
const serviceConditions = [
...(appIds.length > 0 ? [inArray(deployments.applicationId, appIds)] : []),
...(compIds.length > 0 ? [inArray(deployments.composeId, compIds)] : []),
];
const conditions = [
eq(deployments.status, "error"),
serviceConditions.length === 1
? serviceConditions[0]
: or(...serviceConditions),
...(dateFrom ? [gte(deployments.createdAt, dateFrom)] : []),
...(dateTo ? [lte(deployments.createdAt, dateTo)] : []),
];
const failed = await db.query.deployments.findMany({
where: and(...conditions),
orderBy: desc(deployments.createdAt),
with: centralizedDeploymentsWith,
limit: 200,
});
// Skip a server again once it's timed out, so one dead host doesn't cost N SSH timeouts.
const deadServers = new Set<string>();
return mapWithConcurrency(
failed,
LOG_TAIL_CONCURRENCY,
async (deployment) => {
// Same buildServerId-then-serverId precedence used app-wide (application.ts, drop.ts, etc.)
const serverId =
deployment.buildServer?.serverId ||
deployment.application?.buildServer?.serverId ||
deployment.server?.serverId ||
deployment.application?.server?.serverId ||
deployment.compose?.server?.serverId ||
null;
let logTail = "";
if (!serverId || !deadServers.has(serverId)) {
try {
logTail = await readLogTail(deployment.logPath, serverId);
} catch {
if (serverId) deadServers.add(serverId);
}
}
const combined = `${deployment.errorMessage ?? ""}\n${logTail}`;
const classification = classifyFailure(combined);
// Tail end only: the actual failure line is almost always near the end of the log.
const rawError = combined.trim().slice(-1000) || null;
return { ...deployment, classification, rawError };
},
);
};
export const updateDeployment = async (
deploymentId: string,
deploymentData: Partial<Deployment>,
@@ -1,69 +0,0 @@
export interface FailurePattern {
id: string;
label: string;
pattern: RegExp;
}
export const FAILURE_PATTERNS: FailurePattern[] = [
{
id: "inotify-limit",
label: "Inotify limit reached",
pattern: /max_user_instances|max_user_watches|inotify watch limit reached/i,
},
{
id: "oom",
label: "Out of memory (OOM)",
pattern: /\boom[-_ ]?kill(er|ed)?\b|killed process|out of memory/i,
},
{
id: "address-pool-exhausted",
label: "Docker network address pool exhausted",
pattern:
/could not find an available[,]? non-overlapping|available.*address pool/i,
},
{
id: "network-ip-exhausted",
label: "Network out of IP addresses",
pattern:
/could not find an available ip|no available ip addresses|task allocation failure/i,
},
{
id: "network-attach-timeout",
label: "Network attach timed out",
pattern:
/attaching to network failed|failed to set up container networking/i,
},
{
id: "disk-full",
label: "Disk space exhausted",
pattern: /no space left on device/i,
},
{
id: "conntrack-full",
label: "Connection tracking table full",
pattern: /nf_conntrack:?\s*table full/i,
},
{
id: "fd-limit",
label: "File descriptor limit reached",
pattern: /too many open files/i,
},
{
id: "pid-limit",
label: "Process/PID limit reached",
pattern:
/pids\.max|cannot allocate memory|fork:.*resource temporarily unavailable/i,
},
];
export const classifyFailure = (
text: string | null | undefined,
): { id: string; label: string } | null => {
if (!text) return null;
for (const entry of FAILURE_PATTERNS) {
if (entry.pattern.test(text)) {
return { id: entry.id, label: entry.label };
}
}
return null;
};