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:
2025-10-06 15:59:55 +05:00
parent 9e4bc7b0f8
commit a49129f643
16 changed files with 2332 additions and 483 deletions
+232
View File
@@ -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));
},
};
+284 -3
View File
@@ -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;
},
};