chore: checkpoint before performance tuning
This commit is contained in:
+94
-18
@@ -18,7 +18,7 @@ export interface ContainerStatus {
|
||||
ports: string;
|
||||
}
|
||||
|
||||
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "codex-queue"] as const;
|
||||
const rebuildableServices = ["backend-core", "frontend", "provider-gateway", "todo-note", "codex-queue", "project-manager"] as const;
|
||||
export type RebuildableService = typeof rebuildableServices[number];
|
||||
|
||||
export function isRebuildableService(value: string | undefined): value is RebuildableService {
|
||||
@@ -112,10 +112,29 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
|
||||
UNIDESK_HOST_SSH_HOST: config.sshForwarding.host,
|
||||
UNIDESK_HOST_SSH_PORT: String(config.sshForwarding.port),
|
||||
UNIDESK_HOST_SSH_USER: config.sshForwarding.user,
|
||||
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_ENABLED") || "true",
|
||||
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy",
|
||||
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TARGET_TYPE") || "private",
|
||||
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_USER_ID") || "645275593",
|
||||
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_GROUP_ID"),
|
||||
UNIDESK_TODO_NOTE_REMINDER_LEAD_MINUTES: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_LEAD_MINUTES") || "10",
|
||||
UNIDESK_TODO_NOTE_REMINDER_SCAN_INTERVAL_MS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_SCAN_INTERVAL_MS") || "30000",
|
||||
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_TIMEOUT_MS") || "15000",
|
||||
UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_TODO_NOTE_REMINDER_CLAUDEQQ_SEND_ATTEMPTS") || "3",
|
||||
UNIDESK_CODEX_QUEUE_MINIMAX_API_KEY: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_API_KEY"),
|
||||
UNIDESK_CODEX_QUEUE_MINIMAX_MODEL: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_MODEL") || "MiniMax-M2.7",
|
||||
UNIDESK_CODEX_QUEUE_MINIMAX_API_BASE: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_API_BASE") || "https://api.minimaxi.com/v1",
|
||||
UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS: runtimeSecret("UNIDESK_CODEX_QUEUE_MINIMAX_JUDGE_TIMEOUT_MS") || "60000",
|
||||
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_ENABLED") || "true",
|
||||
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy",
|
||||
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE") || "private",
|
||||
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_USER_ID") || "645275593",
|
||||
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID"),
|
||||
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_MAX_RESPONSE_CHARS") || "12000",
|
||||
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_TIMEOUT_MS") || "15000",
|
||||
UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS: runtimeSecret("UNIDESK_CODEX_QUEUE_NOTIFY_CLAUDEQQ_SEND_ATTEMPTS") || "3",
|
||||
OPENAI_API_KEY: runtimeSecret("OPENAI_API_KEY"),
|
||||
CRS_OAI_KEY: runtimeSecret("CRS_OAI_KEY"),
|
||||
};
|
||||
writeFileSync(envFile, Object.entries(lines).map(([key, value]) => `${key}=${envValue(value)}`).join("\n") + "\n", "utf8");
|
||||
return { envFile, logDir, logPrefix };
|
||||
@@ -136,17 +155,17 @@ export function startStack(config: UniDeskConfig): unknown {
|
||||
if (occupiedPorts.length > 0 && containers.length === 0) {
|
||||
throw new Error(`Fixed UniDesk port is occupied before start: ${occupiedPorts.map((p) => `${p.name}:${p.port}`).join(", ")}`);
|
||||
}
|
||||
const downCommand = [...compose, "down", "--remove-orphans"];
|
||||
const upCommand = [...compose, "up", "-d", "--build"];
|
||||
const command = ["bash", "-lc", `set -euo pipefail; ${shellJoin(downCommand)}; ${shellJoin(upCommand)}`];
|
||||
const job = startJob("server_start", command, "Build and start UniDesk database, core, frontend, provider gateway, and managed microservice containers");
|
||||
const upCommand = [...compose, "up", "-d", "--build", "--remove-orphans"];
|
||||
const command = ["bash", "-lc", composeLockedScript(`set -euo pipefail; ${shellJoin(upCommand)}`)];
|
||||
const job = startJob("server_start", command, "Build and start UniDesk services without tearing down running queue containers");
|
||||
return { job, runtimeEnv, command, ports: fixedPorts(config) };
|
||||
}
|
||||
|
||||
export function stopStack(config: UniDeskConfig): unknown {
|
||||
const runtimeEnv = writeComposeEnv(config, false);
|
||||
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
|
||||
const command = [...compose, "down", "--remove-orphans"];
|
||||
const downCommand = [...compose, "down", "--remove-orphans"];
|
||||
const command = ["bash", "-lc", composeLockedScript(`set -euo pipefail; ${shellJoin(downCommand)}`)];
|
||||
const job = startJob("server_stop", command, "Stop all UniDesk Docker services managed by the fixed compose project");
|
||||
return { job, runtimeEnv, command, portsBeforeStop: fixedPorts(config) };
|
||||
}
|
||||
@@ -158,7 +177,7 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic
|
||||
const listServiceContainersCommand = [
|
||||
"docker",
|
||||
"ps",
|
||||
"-aq",
|
||||
"-q",
|
||||
"--filter",
|
||||
`label=com.docker.compose.project=${config.docker.projectName}`,
|
||||
"--filter",
|
||||
@@ -166,29 +185,65 @@ export function rebuildService(config: UniDeskConfig, service: RebuildableServic
|
||||
"--filter",
|
||||
"label=com.docker.compose.oneoff=False",
|
||||
];
|
||||
const upCommand = [...compose, "up", "-d", "--no-deps", service];
|
||||
const upCommand = [...compose, "up", "-d", "--no-deps", "--force-recreate", service];
|
||||
const restoreCommand = [...compose, "up", "-d", "--no-deps", service];
|
||||
const listAllServiceContainersCommand = [...listServiceContainersCommand];
|
||||
listAllServiceContainersCommand[2] = "-a";
|
||||
const lockPath = composeLockPath();
|
||||
const watchdogLog = rootPath(".state", "jobs", "compose-rebuild-watchdog.log");
|
||||
const watchdogInnerScript = [
|
||||
"set -euo pipefail",
|
||||
"sleep 20",
|
||||
`cid=$(${shellJoin(listServiceContainersCommand)} || true)`,
|
||||
`if [ -z "$cid" ]; then echo "$(date -Is) compose_rebuild_watchdog_restore service=${service}" >> ${shellQuote(watchdogLog)}; ${shellJoin(restoreCommand)} >> ${shellQuote(watchdogLog)} 2>&1 || true; fi`,
|
||||
].join("\n");
|
||||
const watchdogScript = `set -euo pipefail; ${shellJoin(["flock", "-w", "300", lockPath, "bash", "-lc", watchdogInnerScript])} || true`;
|
||||
const validateScript = [
|
||||
"ready=0",
|
||||
"for attempt in $(seq 1 60); do",
|
||||
`cid=$(${shellJoin(listServiceContainersCommand)} || true)`,
|
||||
"if [ -n \"$cid\" ]; then",
|
||||
"health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' $cid 2>/dev/null | head -1 || true)",
|
||||
`echo "service_container_probe service=${service} attempt=$attempt cid=$cid health=$health"`,
|
||||
"if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi",
|
||||
"else",
|
||||
`echo "service_container_probe service=${service} attempt=$attempt cid=missing"`,
|
||||
"fi",
|
||||
"sleep 1",
|
||||
"done",
|
||||
"if [ \"$ready\" != \"1\" ]; then",
|
||||
`echo "service_container_not_ready service=${service}" >&2`,
|
||||
`${shellJoin(listAllServiceContainersCommand)} --format '{{.ID}} {{.Names}} {{.Status}}' >&2 || true`,
|
||||
"exit 1",
|
||||
"fi",
|
||||
].join("\n");
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
`echo ${shellJoin(["rebuild_service", service, "build_first_then_replace_container"])}`,
|
||||
`echo ${shellJoin(["rebuild_service", service, "build_first_then_force_recreate_with_validation"])}`,
|
||||
shellJoin(buildCommand),
|
||||
`ids=$(${shellJoin(listServiceContainersCommand)})`,
|
||||
`if [ -n "$ids" ]; then echo "remove_existing_compose_service_containers service=${service} ids=$ids"; docker rm -f $ids; else echo "no_existing_compose_service_containers service=${service}"; fi`,
|
||||
`nohup bash -lc ${shellQuote(watchdogScript)} >/dev/null 2>&1 &`,
|
||||
shellJoin(upCommand),
|
||||
].join("; ");
|
||||
const command = ["bash", "-lc", script];
|
||||
const job = startJob("server_rebuild", command, `Rebuild and replace UniDesk ${service} without Docker Compose v1 recreate`);
|
||||
validateScript,
|
||||
].join("\n");
|
||||
const command = ["bash", "-lc", composeLockedScript(script)];
|
||||
const jobRunner = service === "codex-queue" ? { runner: "docker" as const, dockerImage: "unidesk-codex-queue:latest" } : {};
|
||||
const job = startJob("server_rebuild", command, `Rebuild and validate UniDesk ${service} with serialized Docker Compose mutation`, jobRunner);
|
||||
return {
|
||||
job,
|
||||
runtimeEnv,
|
||||
service,
|
||||
command,
|
||||
strategy: {
|
||||
buildBeforeRemove: true,
|
||||
removeScope: {
|
||||
buildBeforeReplace: true,
|
||||
replaceScope: {
|
||||
projectLabel: config.docker.projectName,
|
||||
serviceLabel: service,
|
||||
},
|
||||
noDeps: true,
|
||||
forceRecreate: true,
|
||||
composeMutationLock: rootPath(".state", "locks", "server-compose.lock"),
|
||||
jobRunner: service === "codex-queue" ? "docker" : "local",
|
||||
postUpValidation: true,
|
||||
namedVolumesPreserved: true,
|
||||
},
|
||||
};
|
||||
@@ -202,7 +257,27 @@ function fixedPorts(config: UniDeskConfig): Array<{ name: string; port: number;
|
||||
}
|
||||
|
||||
function shellJoin(args: string[]): string {
|
||||
return args.map((arg) => `'${arg.replace(/'/g, `'\\''`)}'`).join(" ");
|
||||
return args.map(shellQuote).join(" ");
|
||||
}
|
||||
|
||||
function shellQuote(arg: string): string {
|
||||
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function composeLockedScript(innerScript: string): string {
|
||||
const lockPath = composeLockPath();
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
`mkdir -p ${shellQuote(rootPath(".state", "locks"))}`,
|
||||
`echo ${shellJoin(["compose_lock_wait", lockPath])}`,
|
||||
shellJoin(["flock", lockPath, "bash", "-lc", innerScript]),
|
||||
].join("; ");
|
||||
}
|
||||
|
||||
function composeLockPath(): string {
|
||||
const lockDir = rootPath(".state", "locks");
|
||||
mkdirSync(lockDir, { recursive: true });
|
||||
return join(lockDir, "server-compose.lock");
|
||||
}
|
||||
|
||||
function isPortListening(port: number): boolean {
|
||||
@@ -278,6 +353,7 @@ export async function stackStatus(config: UniDeskConfig): Promise<unknown> {
|
||||
{ name: "backend-core", containerPort: config.network.core.containerPort, hostPort: null },
|
||||
{ name: "database", containerPort: config.network.database.containerPort, hostPort: null },
|
||||
{ name: "codex-queue", containerPort: 4222, hostPort: null },
|
||||
{ name: "project-manager", containerPort: 4233, hostPort: null },
|
||||
],
|
||||
containers: dockerContainers(config),
|
||||
health: {
|
||||
@@ -315,7 +391,7 @@ export function stackLogs(config: UniDeskConfig, tailBytes: number): unknown {
|
||||
const currentFiles = allFiles.filter((path) => basename(path).startsWith(runtimeEnv.logPrefix));
|
||||
const selectedFiles = (currentFiles.length > 0 ? currentFiles : allFiles.slice(-12)).slice(-12);
|
||||
const files = selectedFiles.map((path) => ({ path, name: basename(path), tail: tailFile(path, tailBytes) }));
|
||||
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "codex-queue-backend"];
|
||||
const containerNames = ["unidesk-database", "unidesk-backend-core", "unidesk-frontend", "unidesk-provider-gateway-main", "todo-note-backend", "codex-queue-backend", "project-manager-backend"];
|
||||
const docker = containerNames.map((name) => {
|
||||
const result = runCommand(["docker", "logs", "--tail", "40", name], repoRoot);
|
||||
return { name, exitCode: result.exitCode, stdoutTail: result.stdout.slice(-tailBytes), stderrTail: result.stderr.slice(-tailBytes) };
|
||||
|
||||
Reference in New Issue
Block a user