From 806935e48cbe0b67a258bbc6e1b7896375509460 Mon Sep 17 00:00:00 2001 From: Gabriel Matte Date: Tue, 12 Nov 2024 01:22:54 -0500 Subject: [PATCH] Adds container cleaning - doesn't clean populated container on reboot --- server/controllers/rooms.js | 16 +++- server/models/room.js | 24 ++++-- server/roomsProviders/base-provider.js | 35 ++++++-- server/roomsProviders/docker-provider.js | 104 +++++++++++++---------- 4 files changed, 114 insertions(+), 65 deletions(-) diff --git a/server/controllers/rooms.js b/server/controllers/rooms.js index 30aca60..03916a0 100644 --- a/server/controllers/rooms.js +++ b/server/controllers/rooms.js @@ -6,7 +6,8 @@ const DockerRoomProvider = require('../roomsProviders/docker-provider.js'); //const KubernetesRoomProvider = require('../roomsProviders/kubernetes-provider'); const NB_CODE_CHARS = 6; -const DEFAULT_HOST = "172.18.0.5:4500" // must be room ip not name +const NB_MS_UPDATE_ROOM = 1000; +const NB_MS_CLEANUP = 30000; class RoomsController { constructor(options = {}, roomRepository) { @@ -16,7 +17,7 @@ class RoomsController { roomRepository ); this.roomRepository = roomRepository; - this.setupCleanup(); + this.setupTasks(); } createProvider(type, options, repository) { @@ -37,10 +38,17 @@ class RoomsController { } } - setupCleanup() { + async setupTasks(){ + await this.provider.syncInstantiatedRooms(); + // Update rooms + setInterval(() => { + this.provider.updateRoomsInfo().catch(console.error); + }, NB_MS_UPDATE_ROOM); + + // Cleanup rooms setInterval(() => { this.provider.cleanup().catch(console.error); - }, 30000); + }, NB_MS_CLEANUP); } async createRoom(options = {}) { diff --git a/server/models/room.js b/server/models/room.js index b9a5c0e..23de986 100644 --- a/server/models/room.js +++ b/server/models/room.js @@ -1,9 +1,15 @@ class Room { - constructor(id, name, host, nbStudents = 0) { // Default nbStudents to 0 + constructor(id, name, host, nbStudents = 0,) { // Default nbStudents to 0 this.id = id; this.name = name; + + if (!host.startsWith('http://') && !host.startsWith('https://')) { + host = 'http://' + host; + } this.host = host; + this.nbStudents = nbStudents; + this.mustBeCleaned = false; } } @@ -47,15 +53,21 @@ class RoomRepository { return await this.collection.find().toArray(); } - async update(room) { + async update(room,roomId = null) { await this.init(); + + const searchId = roomId ?? room.id; + const result = await this.collection.updateOne( - { id: room.id }, - { $set: room } + { id: searchId }, + { $set: room }, + { upsert: false } ); - + if (result.modifiedCount === 0) { - console.warn(`Room with id ${room.id} was not updated because it was not found.`); + if (result.matchedCount > 0) { + return true; // Document exists but no changes needed + } return false; } return true; diff --git a/server/roomsProviders/base-provider.js b/server/roomsProviders/base-provider.js index 05a602f..5ddef90 100644 --- a/server/roomsProviders/base-provider.js +++ b/server/roomsProviders/base-provider.js @@ -5,10 +5,14 @@ * @typedef {import('../../types/room').BaseProviderConfig} BaseProviderConfig */ +const MIN_NB_SECONDS_BEFORE_CLEANUP = process.env.MIN_NB_SECONDS_BEFORE_CLEANUP || 60 + class BaseRoomProvider { constructor(config = {}, roomRepository) { this.config = config; this.roomRepository = roomRepository; + + this.quiz_docker_image = process.env.QUIZROOM_IMAGE || "evaluetonsavoir-quizroom"; } async createRoom(roomId, options) { @@ -31,19 +35,32 @@ class BaseRoomProvider { throw new Error("Method not implemented"); } - async updateRoomInfo(roomId, info) { - let room = await this.getRoomInfo(roomId); + async syncInstantiatedRooms(){ + throw new Error("Method not implemented"); + } - if (!room) return false; + async updateRoomsInfo() { + const rooms = await this.roomRepository.getAll(); + for(var room of rooms){ + const url = `${room.host}/health`; + try { + const response = await fetch(url); + + if (!response.ok) { + room.mustBeCleaned = true; + await this.roomRepository.update(room); + continue; + } + + const json = await response.json(); + room.nbStudents = json.connections; + room.mustBeCleaned = room.nbStudents === 0 && json.uptime >MIN_NB_SECONDS_BEFORE_CLEANUP; - for (let key of Object.keys(room)) { - if (info[key] !== undefined) { - room[key] = info[key]; + await this.roomRepository.update(room); + } catch (error) { + console.error(`Error updating room ${room.id}:`, error); } } - - const result = await this.roomRepository.update(room); - return result != null; } async getRoomInfo(roomId) { diff --git a/server/roomsProviders/docker-provider.js b/server/roomsProviders/docker-provider.js index 37d01c8..f0d0739 100644 --- a/server/roomsProviders/docker-provider.js +++ b/server/roomsProviders/docker-provider.js @@ -5,26 +5,53 @@ const BaseRoomProvider = require("./base-provider.js"); class DockerRoomProvider extends BaseRoomProvider { constructor(config, roomRepository) { super(config, roomRepository); - this.docker = new Docker({ socketPath: "/var/run/docker.sock" }); + const dockerSocket = process.env.DOCKER_SOCKET || "/var/run/docker.sock"; + + this.docker = new Docker({ socketPath: dockerSocket }); + } + + async syncInstantiatedRooms(){ + let containers = await this.docker.listContainers(); + containers = containers.filter(container => container.Image === this.quiz_docker_image); + + const containerIds = new Set(containers.map(container => container.Id)); + + for (let container of containers) { + const container_name = container.Names[0].slice(1); + if (!container_name.startsWith("room_")) { + console.warn(`Container ${container_name} does not follow the room naming convention, removing it.`); + const curContainer = this.docker.getContainer(container.Id); + await curContainer.stop(); + await curContainer.remove(); + containerIds.delete(container.Id); + console.warn(`Container ${container_name} removed.`); + } + else{ + console.warn(`Found orphan container : ${container_name}`); + const roomId = container_name.slice(5); + const room = await this.roomRepository.get(roomId); + + if (!room) { + console.warn(`container not our rooms database`); + const containerInfo = await this.docker.getContainer(container.Id).inspect(); + const containerIP = containerInfo.NetworkSettings.Networks.evaluetonsavoir_quiz_network.IPAddress; + const host = `${containerIP}:4500`; + console.warn(`Creating room ${roomId} in our database - host : ${host}`); + return await this.roomRepository.create(new Room(roomId, container_name, host)); + } + + console.warn(`room ${roomId} already in our database`); + } + } } async createRoom(roomId, options) { const container_name = `room_${roomId}`; const containerConfig = { - Image: 'evaluetonsavoir-quizroom', + Image: this.quiz_docker_image, name: container_name, - ExposedPorts: { - "4500/tcp": {} - }, HostConfig: { - PortBindings: { - "4500/tcp": [ - { - HostPort: "" - } - ] - }, NetworkMode: "evaluetonsavoir_quiz_network" }, Env: [...options.env || [], `ROOM_ID=${roomId}`] @@ -36,13 +63,14 @@ class DockerRoomProvider extends BaseRoomProvider { const containerInfo = await container.inspect(); const containerIP = containerInfo.NetworkSettings.Networks.evaluetonsavoir_quiz_network.IPAddress; - const host = `${containerIP}:4500`; + const host = `${containerIP}:${this.quiz_docker_port ?? "4500"}`; return await this.roomRepository.create(new Room(roomId, container_name, host, 0)); } async deleteRoom(roomId) { const container_name = `room_${roomId}`; + await this.roomRepository.delete(roomId); try { const container = this.docker.getContainer(container_name); @@ -61,8 +89,7 @@ class DockerRoomProvider extends BaseRoomProvider { throw new Error("Failed to delete room"); } } - - await this.roomRepository.delete(roomId); + console.log(`Room ${roomId} deleted from repository.`); } @@ -117,44 +144,29 @@ class DockerRoomProvider extends BaseRoomProvider { async cleanup() { const rooms = await this.roomRepository.getAll(); - const roomIds = new Set(rooms.map(room => room.id)); - - const containers = await this.docker.listContainers({ all: true }); - const containerIds = new Set(); - - for (const containerInfo of containers) { - const containerName = containerInfo.Names[0].replace("/", ""); - if (containerName.startsWith("room_")) { - const roomId = containerName.split("_")[1]; - containerIds.add(roomId); - - if (!roomIds.has(roomId)) { - try { - const container = this.docker.getContainer(containerInfo.Id); - await container.stop(); - await container.remove(); - console.log(`Loose container ${containerName} deleted.`); - } catch (error) { - console.error(`Failed to delete loose container ${containerName}:`, error); - } - } - } - } - - for (const room of rooms) { - if (!containerIds.has(room.id)) { + for (let room of rooms) { + if (room.mustBeCleaned) { try { - await this.roomRepository.delete(room.id); - console.log(`Orphan room ${room.id} deleted from repository.`); + await this.deleteRoom(room.id); } catch (error) { - console.error(`Failed to delete orphan room ${room.id} from repository:`, error); + console.error(`Error cleaning up room ${room.id}:`, error); } } } - console.log("Cleanup of loose containers and orphan rooms completed."); - } + let containers = await this.docker.listContainers(); + containers = containers.filter(container => container.Image === this.quiz_docker_image); + const roomIds = rooms.map(room => room.id); + for (let container of containers) { + if (!roomIds.includes(container.Names[0].slice(6))) { + const curContainer = this.docker.getContainer(container.Id); + await curContainer.stop(); + await curContainer.remove(); + console.warn(`Orphan container ${container.Names[0]} removed.`); + } + } + } } module.exports = DockerRoomProvider;