Remove outdated documentation files for companies and migration guide; implement server session management features including server assignment and session status updates; enhance database schema for servers and server sessions with new fields and validation; add auto-assign functionality for unassigned sessions.
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import db from "../../db";
|
||||
import { servers } from "../../db/schema/servers";
|
||||
|
||||
export type ServerType = "stream" | "local";
|
||||
export type ServerLocation = "ru1" | "uae1";
|
||||
export type ServerTier = "demo" | "prod";
|
||||
|
||||
export interface CreateServerParams {
|
||||
localIp: string;
|
||||
hostname: string;
|
||||
type: ServerType;
|
||||
gpuFreeMb: number;
|
||||
branchId?: string;
|
||||
location?: ServerLocation;
|
||||
tier?: ServerTier;
|
||||
}
|
||||
|
||||
export interface UpdateServerParams {
|
||||
localIp?: string;
|
||||
hostname?: string;
|
||||
gpuFreeMb?: number;
|
||||
branchId?: string;
|
||||
location?: ServerLocation;
|
||||
tier?: ServerTier;
|
||||
}
|
||||
|
||||
export interface FindServersFilters {
|
||||
type?: ServerType;
|
||||
location?: ServerLocation;
|
||||
tier?: ServerTier;
|
||||
branchId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сервис для работы с серверами
|
||||
*/
|
||||
export const serverService = {
|
||||
/**
|
||||
* Создать сервер
|
||||
*/
|
||||
async create(params: CreateServerParams) {
|
||||
// Валидация для stream-серверов
|
||||
if (params.type === "stream" && !params.location) {
|
||||
throw new Error("Location is required for stream servers");
|
||||
}
|
||||
|
||||
// Установить tier по умолчанию для stream-серверов
|
||||
const tier =
|
||||
params.type === "stream" && !params.tier ? "demo" : params.tier;
|
||||
|
||||
const [server] = await db
|
||||
.insert(servers)
|
||||
.values({
|
||||
localIp: params.localIp,
|
||||
hostname: params.hostname,
|
||||
type: params.type,
|
||||
gpuFreeMb: params.gpuFreeMb,
|
||||
branchId: params.branchId,
|
||||
location: params.location,
|
||||
tier: tier,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return server;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить все серверы с фильтрацией
|
||||
*/
|
||||
async findAll(filters?: FindServersFilters) {
|
||||
const conditions = [];
|
||||
|
||||
if (filters?.type) {
|
||||
conditions.push(eq(servers.type, filters.type));
|
||||
}
|
||||
|
||||
if (filters?.location) {
|
||||
conditions.push(eq(servers.location, filters.location));
|
||||
}
|
||||
|
||||
if (filters?.tier) {
|
||||
conditions.push(eq(servers.tier, filters.tier));
|
||||
}
|
||||
|
||||
if (filters?.branchId) {
|
||||
conditions.push(eq(servers.branchId, filters.branchId));
|
||||
}
|
||||
|
||||
const allServers = await db.query.servers.findMany({
|
||||
where: conditions.length > 0 ? and(...conditions) : undefined,
|
||||
orderBy: (servers, { asc }) => [asc(servers.hostname)],
|
||||
});
|
||||
|
||||
return allServers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Найти сервер по ID
|
||||
*/
|
||||
async findById(serverId: string) {
|
||||
const server = await db.query.servers.findFirst({
|
||||
where: eq(servers.id, serverId),
|
||||
});
|
||||
|
||||
return server || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Найти сервер по hostname
|
||||
*/
|
||||
async findByHostname(hostname: string) {
|
||||
const server = await db.query.servers.findFirst({
|
||||
where: eq(servers.hostname, hostname),
|
||||
});
|
||||
|
||||
return server || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить серверы по филиалу
|
||||
*/
|
||||
async findByBranchId(branchId: string) {
|
||||
const branchServers = await db.query.servers.findMany({
|
||||
where: eq(servers.branchId, branchId),
|
||||
orderBy: (servers, { asc }) => [asc(servers.hostname)],
|
||||
});
|
||||
|
||||
return branchServers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить доступные stream-серверы
|
||||
*/
|
||||
async findAvailableStreamServers(tier?: ServerTier) {
|
||||
const conditions = [eq(servers.type, "stream")];
|
||||
|
||||
if (tier) {
|
||||
conditions.push(eq(servers.tier, tier));
|
||||
}
|
||||
|
||||
const streamServers = await db.query.servers.findMany({
|
||||
where: and(...conditions),
|
||||
orderBy: (servers, { desc }) => [desc(servers.gpuFreeMb)],
|
||||
});
|
||||
|
||||
return streamServers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить доступные local-серверы
|
||||
*/
|
||||
async findAvailableLocalServers(branchId?: string) {
|
||||
const conditions = [eq(servers.type, "local")];
|
||||
|
||||
if (branchId) {
|
||||
conditions.push(eq(servers.branchId, branchId));
|
||||
}
|
||||
|
||||
const localServers = await db.query.servers.findMany({
|
||||
where: and(...conditions),
|
||||
orderBy: (servers, { desc }) => [desc(servers.gpuFreeMb)],
|
||||
});
|
||||
|
||||
return localServers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Обновить сервер
|
||||
*/
|
||||
async update(serverId: string, params: UpdateServerParams) {
|
||||
const updateData: any = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (params.localIp) {
|
||||
updateData.localIp = params.localIp;
|
||||
}
|
||||
|
||||
if (params.hostname) {
|
||||
updateData.hostname = params.hostname;
|
||||
}
|
||||
|
||||
if (params.gpuFreeMb !== undefined) {
|
||||
updateData.gpuFreeMb = params.gpuFreeMb;
|
||||
}
|
||||
|
||||
if (params.branchId !== undefined) {
|
||||
updateData.branchId = params.branchId;
|
||||
}
|
||||
|
||||
if (params.location !== undefined) {
|
||||
updateData.location = params.location;
|
||||
}
|
||||
|
||||
if (params.tier !== undefined) {
|
||||
updateData.tier = params.tier;
|
||||
}
|
||||
|
||||
const [updatedServer] = await db
|
||||
.update(servers)
|
||||
.set(updateData)
|
||||
.where(eq(servers.id, serverId))
|
||||
.returning();
|
||||
|
||||
return updatedServer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Обновить свободную память GPU
|
||||
*/
|
||||
async updateGpuMemory(serverId: string, gpuFreeMb: number) {
|
||||
const [updatedServer] = await db
|
||||
.update(servers)
|
||||
.set({
|
||||
gpuFreeMb,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(servers.id, serverId))
|
||||
.returning();
|
||||
|
||||
return updatedServer;
|
||||
},
|
||||
|
||||
/**
|
||||
* Удалить сервер
|
||||
*/
|
||||
async delete(serverId: string) {
|
||||
await db.delete(servers).where(eq(servers.id, serverId));
|
||||
},
|
||||
};
|
||||
|
||||
@@ -120,6 +120,80 @@ export const serverSessionService = {
|
||||
return sessions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить все сессии для конкретного сервера
|
||||
*/
|
||||
async findByServerId(
|
||||
serverId: string,
|
||||
filters?: {
|
||||
status?: SessionStatus;
|
||||
mode?: SessionMode;
|
||||
}
|
||||
) {
|
||||
const conditions = [eq(serverSessions.serverId, serverId)];
|
||||
|
||||
if (filters?.status) {
|
||||
conditions.push(eq(serverSessions.status, filters.status));
|
||||
}
|
||||
|
||||
if (filters?.mode) {
|
||||
conditions.push(eq(serverSessions.mode, filters.mode));
|
||||
}
|
||||
|
||||
const sessions = await db.query.serverSessions.findMany({
|
||||
where: and(...conditions),
|
||||
with: {
|
||||
app: true,
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
email: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: (serverSessions, { asc }) => [asc(serverSessions.createdAt)],
|
||||
});
|
||||
|
||||
return sessions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить сессии без назначенного сервера
|
||||
*/
|
||||
async findUnassignedSessions(filters?: {
|
||||
status?: SessionStatus;
|
||||
mode?: SessionMode;
|
||||
}) {
|
||||
const { isNull } = await import("drizzle-orm");
|
||||
const conditions = [isNull(serverSessions.serverId)];
|
||||
|
||||
if (filters?.status) {
|
||||
conditions.push(eq(serverSessions.status, filters.status));
|
||||
}
|
||||
|
||||
if (filters?.mode) {
|
||||
conditions.push(eq(serverSessions.mode, filters.mode));
|
||||
}
|
||||
|
||||
const sessions = await db.query.serverSessions.findMany({
|
||||
where: and(...conditions),
|
||||
with: {
|
||||
app: true,
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
email: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: (serverSessions, { asc }) => [asc(serverSessions.createdAt)],
|
||||
});
|
||||
|
||||
return sessions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Проверить, есть ли у пользователя активная сессия для данного приложения
|
||||
*/
|
||||
@@ -161,9 +235,10 @@ export const serverSessionService = {
|
||||
async create(params: CreateSessionParams) {
|
||||
const { appId, userId, mode, serverId } = params;
|
||||
|
||||
// Выбрать сервер (если не указан)
|
||||
// Для local-сессий выбираем сервер сразу
|
||||
// Для stream-сессий сервер будет назначен динамически при запуске
|
||||
let selectedServerId = serverId;
|
||||
if (!selectedServerId) {
|
||||
if (mode === "local" && !selectedServerId) {
|
||||
selectedServerId = await this.selectAvailableServer(mode);
|
||||
if (!selectedServerId) {
|
||||
throw new Error(`No available ${mode} servers`);
|
||||
@@ -178,7 +253,7 @@ export const serverSessionService = {
|
||||
const [newSession] = await db
|
||||
.insert(serverSessions)
|
||||
.values({
|
||||
serverId: selectedServerId,
|
||||
serverId: selectedServerId, // Может быть null для stream-сессий
|
||||
appId,
|
||||
userId,
|
||||
mode,
|
||||
@@ -263,4 +338,210 @@ export const serverSessionService = {
|
||||
|
||||
return updatedSession;
|
||||
},
|
||||
|
||||
/**
|
||||
* Назначить сервер для сессии
|
||||
* Выбирает сервер с максимальной свободной GPU памятью
|
||||
*/
|
||||
async assignServer(sessionId: string, requiredGpuMb?: number) {
|
||||
const session = await this.findById(sessionId);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
if (session.serverId) {
|
||||
// Сервер уже назначен
|
||||
return session;
|
||||
}
|
||||
|
||||
// Импортируем serverService динамически чтобы избежать циклических зависимостей
|
||||
const { serverService } = await import("../server");
|
||||
|
||||
let selectedServer;
|
||||
|
||||
if (session.mode === "stream") {
|
||||
// Для stream-сессий выбираем сервер с максимальной свободной памятью
|
||||
// Ищем среди всех tier (prod и demo)
|
||||
const availableServers = await serverService.findAvailableStreamServers();
|
||||
|
||||
if (availableServers.length === 0) {
|
||||
throw new Error(
|
||||
"No available stream servers (check that stream servers are registered)"
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 📊 Найдено ${
|
||||
availableServers.length
|
||||
} stream-серверов:`,
|
||||
availableServers
|
||||
.map((s) => `${s.hostname} (${s.tier}, ${s.gpuFreeMb}MB)`)
|
||||
.join(", ")
|
||||
);
|
||||
|
||||
// Фильтруем серверы по доступной GPU памяти
|
||||
// Требуемая память берется из gpuLimitMb приложения или используются все доступные серверы
|
||||
const memoryOkServers = requiredGpuMb
|
||||
? availableServers.filter((s) => {
|
||||
const hasEnough = s.gpuFreeMb >= requiredGpuMb;
|
||||
if (!hasEnough) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ⚠️ Сервер ${s.id} (${
|
||||
s.hostname
|
||||
}) пропущен по памяти: ${s.gpuFreeMb}MB < ${requiredGpuMb}MB`
|
||||
);
|
||||
}
|
||||
return hasEnough;
|
||||
})
|
||||
: availableServers;
|
||||
|
||||
if (memoryOkServers.length === 0) {
|
||||
const maxAvailable = Math.max(
|
||||
...availableServers.map((s) => s.gpuFreeMb)
|
||||
);
|
||||
throw new Error(
|
||||
`No servers with enough GPU memory (required: ${requiredGpuMb}MB, max available: ${maxAvailable}MB)`
|
||||
);
|
||||
}
|
||||
|
||||
// Проверяем количество активных сессий на каждом сервере
|
||||
// Максимум одновременных сессий на один stream-сервер (по умолчанию 3)
|
||||
const MAX_SESSIONS_PER_SERVER = parseInt(
|
||||
process.env.MAX_SESSIONS_PER_STREAM_SERVER || "3",
|
||||
10
|
||||
);
|
||||
const suitableServers = [];
|
||||
|
||||
for (const server of memoryOkServers) {
|
||||
// Подсчитываем активные сессии (starting или started)
|
||||
const activeSessions = await this.findByServerId(server.id, {});
|
||||
const activeCount = activeSessions.filter(
|
||||
(s) => s.status === "starting" || s.status === "started"
|
||||
).length;
|
||||
|
||||
if (activeCount >= MAX_SESSIONS_PER_SERVER) {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ⚠️ Сервер ${server.id} (${
|
||||
server.hostname
|
||||
}) пропущен по загрузке: ${activeCount}/${MAX_SESSIONS_PER_SERVER} активных сессий`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Сервер ${server.id} (${
|
||||
server.hostname
|
||||
}) доступен: ${activeCount}/${MAX_SESSIONS_PER_SERVER} активных сессий`
|
||||
);
|
||||
suitableServers.push(server);
|
||||
}
|
||||
}
|
||||
|
||||
if (suitableServers.length === 0) {
|
||||
throw new Error(
|
||||
`No available servers (all servers have ${MAX_SESSIONS_PER_SERVER} or more active sessions)`
|
||||
);
|
||||
}
|
||||
|
||||
// Берем первый сервер (уже отсортирован по убыванию gpuFreeMb)
|
||||
selectedServer = suitableServers[0];
|
||||
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] ✅ Выбран сервер ${selectedServer.id} (${
|
||||
selectedServer.hostname
|
||||
}) с ${
|
||||
selectedServer.gpuFreeMb
|
||||
}MB свободной памяти для сессии ${sessionId} (требуется: ${
|
||||
requiredGpuMb || "не указано"
|
||||
}MB)`
|
||||
);
|
||||
} else {
|
||||
// Для local-сессий используем существующую логику
|
||||
const serverId = await this.selectAvailableServer(session.mode);
|
||||
if (!serverId) {
|
||||
throw new Error("No available local servers");
|
||||
}
|
||||
selectedServer = await serverService.findById(serverId);
|
||||
}
|
||||
|
||||
if (!selectedServer) {
|
||||
throw new Error("Failed to select server");
|
||||
}
|
||||
|
||||
// Назначаем сервер сессии
|
||||
const [updatedSession] = await db
|
||||
.update(serverSessions)
|
||||
.set({
|
||||
serverId: selectedServer.id,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(serverSessions.id, sessionId))
|
||||
.returning();
|
||||
|
||||
return updatedSession;
|
||||
},
|
||||
|
||||
/**
|
||||
* Автоматически назначить серверы для всех unassigned сессий, готовых к запуску
|
||||
* Вызывается периодически main сервером
|
||||
*/
|
||||
async autoAssignServers() {
|
||||
const now = new Date();
|
||||
|
||||
// Находим все unassigned сессии со статусом "starting" и startAt <= now
|
||||
const { isNull } = await import("drizzle-orm");
|
||||
const unassignedSessions = await db.query.serverSessions.findMany({
|
||||
where: and(
|
||||
isNull(serverSessions.serverId),
|
||||
eq(serverSessions.status, "starting")
|
||||
),
|
||||
with: {
|
||||
app: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Фильтруем сессии, у которых уже наступило время запуска
|
||||
const readySessions = unassignedSessions.filter((session) => {
|
||||
const startAt = new Date(session.startAt);
|
||||
return startAt <= now;
|
||||
});
|
||||
|
||||
const results = {
|
||||
total: readySessions.length,
|
||||
assigned: 0,
|
||||
failed: 0,
|
||||
errors: [] as string[],
|
||||
};
|
||||
|
||||
// Назначаем сервер для каждой готовой сессии
|
||||
for (const session of readySessions) {
|
||||
try {
|
||||
const requiredGpuMb = session.app.gpuLimitMb || undefined;
|
||||
console.log(
|
||||
`[${new Date().toISOString()}] 🔍 Назначение сервера для сессии ${
|
||||
session.id
|
||||
} (приложение: ${session.app.name}, требуется GPU: ${
|
||||
requiredGpuMb || "не указано"
|
||||
}MB)`
|
||||
);
|
||||
|
||||
await this.assignServer(session.id, requiredGpuMb);
|
||||
results.assigned++;
|
||||
} catch (error) {
|
||||
results.failed++;
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
results.errors.push(
|
||||
`Session ${session.id} (${session.app.name}): ${errorMsg}`
|
||||
);
|
||||
|
||||
console.error(
|
||||
`[${new Date().toISOString()}] ❌ Не удалось назначить сервер для сессии ${
|
||||
session.id
|
||||
}:`,
|
||||
errorMsg
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user