fix: verify swarm task convergence before marking db deploys done

This commit is contained in:
Mauricio Siu
2026-09-01 01:56:58 -06:00
parent d7ea9cf9bc
commit 5624dff5c1
7 changed files with 82 additions and 6 deletions
+5 -1
View File
@@ -7,7 +7,10 @@ import {
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildLibsql } from "@dokploy/server/utils/databases/libsql";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import {
pullImage,
waitForSwarmServiceConvergence,
} from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
@@ -149,6 +152,7 @@ export const deployLibsql = async (
}
await buildLibsql(libsql);
await waitForSwarmServiceConvergence(libsql.appName, libsql.serverId);
await updateLibsqlById(libsqlId, {
applicationStatus: "done",
});
+5 -1
View File
@@ -7,7 +7,10 @@ import {
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildMariadb } from "@dokploy/server/utils/databases/mariadb";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import {
pullImage,
waitForSwarmServiceConvergence,
} from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
@@ -154,6 +157,7 @@ export const deployMariadb = async (
}
await buildMariadb(mariadb);
await waitForSwarmServiceConvergence(mariadb.appName, mariadb.serverId);
await updateMariadbById(mariadbId, {
applicationStatus: "done",
});
+5 -1
View File
@@ -8,7 +8,10 @@ import {
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildMongo } from "@dokploy/server/utils/databases/mongo";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import {
pullImage,
waitForSwarmServiceConvergence,
} from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
@@ -169,6 +172,7 @@ export const deployMongo = async (
}
await buildMongo(mongo);
await waitForSwarmServiceConvergence(mongo.appName, mongo.serverId);
await updateMongoById(mongoId, {
applicationStatus: "done",
});
+5 -1
View File
@@ -7,7 +7,10 @@ import {
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildMysql } from "@dokploy/server/utils/databases/mysql";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import {
pullImage,
waitForSwarmServiceConvergence,
} from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
@@ -152,6 +155,7 @@ export const deployMySql = async (
}
await buildMysql(mysql);
await waitForSwarmServiceConvergence(mysql.appName, mysql.serverId);
await updateMySqlById(mysqlId, {
applicationStatus: "done",
});
+6 -1
View File
@@ -7,7 +7,10 @@ import {
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildPostgres } from "@dokploy/server/utils/databases/postgres";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import {
pullImage,
waitForSwarmServiceConvergence,
} from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq, getTableColumns } from "drizzle-orm";
@@ -165,6 +168,8 @@ export const deployPostgres = async (
await buildPostgres(postgres);
await waitForSwarmServiceConvergence(postgres.appName, postgres.serverId);
await updatePostgresById(postgresId, {
applicationStatus: "done",
});
+5 -1
View File
@@ -6,7 +6,10 @@ import {
} from "@dokploy/server/db/schema";
import { generatePassword } from "@dokploy/server/templates";
import { buildRedis } from "@dokploy/server/utils/databases/redis";
import { pullImage } from "@dokploy/server/utils/docker/utils";
import {
pullImage,
waitForSwarmServiceConvergence,
} from "@dokploy/server/utils/docker/utils";
import { execAsyncRemote } from "@dokploy/server/utils/process/execAsync";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
@@ -119,6 +122,7 @@ export const deployRedis = async (
}
await buildRedis(redis);
await waitForSwarmServiceConvergence(redis.appName, redis.serverId);
await updateRedisById(redisId, {
applicationStatus: "done",
});
+51
View File
@@ -934,6 +934,57 @@ const getSwarmServiceContainerId = async (
}
};
export class ServiceConvergenceError extends Error {}
export const waitForSwarmServiceConvergence = async (
appName: string,
serverId?: string | null,
options?: { timeoutMs?: number; intervalMs?: number },
): Promise<void> => {
const timeoutMs = options?.timeoutMs ?? 45_000;
const intervalMs = options?.intervalMs ?? 2_000;
const remoteDocker = await getRemoteDocker(serverId);
const service = remoteDocker.getService(appName);
const deadline = Date.now() + timeoutMs;
let lastState = "unknown";
while (true) {
const info = await service.inspect();
const desiredTasksCount = info.Spec?.Mode?.Replicated?.Replicas ?? 1;
const tasks = await remoteDocker.listTasks({
filters: JSON.stringify({ service: [appName] }),
});
const currentTasks = tasks.filter(
(task) => task.DesiredState === "running",
);
const runningTasksCount = currentTasks.filter(
(task) => task.Status?.State === "running",
).length;
if (runningTasksCount >= desiredTasksCount) {
return;
}
const failedTask = currentTasks.find((task) =>
["failed", "rejected"].includes(task.Status?.State ?? ""),
);
lastState =
failedTask?.Status?.Err ??
failedTask?.Status?.State ??
currentTasks[0]?.Status?.State ??
lastState;
if (Date.now() >= deadline) {
throw new ServiceConvergenceError(
`Service ${appName} did not converge within ${timeoutMs}ms: ${runningTasksCount}/${desiredTasksCount} tasks running (last state: ${lastState})`,
);
}
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}
};
export const checkPostgresHealth = async (): Promise<ServiceHealthStatus> => {
const serviceCheck = await checkSwarmServiceRunning("dokploy-postgres");
if (serviceCheck.status === "unhealthy") {