mirror of
https://github.com/andwati/dokploy.git
synced 2026-10-18 03:02:56 +00:00
refactor: remove deployment failure classifier and related tests
This commit is contained in:
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user