Adds container cleaning - doesn't clean populated container on reboot

This commit is contained in:
Gabriel Matte 2024-11-12 01:22:54 -05:00
parent 3c2bcb4ed4
commit 806935e48c
4 changed files with 114 additions and 65 deletions

View file

@ -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 = {}) {

View file

@ -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;

View file

@ -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) {

View file

@ -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;