Skip to content

Commit 62eba0d

Browse files
k1b1t0k1b1t0ncduy0303DanielJames0302
authored
Integrate execution collaboration (#19)
* Add POST API for execution service * Rewrite execution service to running code only * WIP: latest changes before merging dev * Modify docker-compose.yml * Merge collaboration service and change execution-service * feat(collaboration-service-frontend): - Integrate collaboration service backend with into Code Editor panel on /practice/:id pages. - Add placeholder panels for Question, Communication, Code Execution. * fix(code-editor-panel): dynamic loading monaco editor on client * Error handling when worker can't connect to PistonAPI * Add Agora Token generator * Add CI workflow for execution service * Integrate collaboration and execution, todo: broadcast to FE, and send code from FE * Integrate code execution to collab FE * Fix rabbitMQ startup error * Add sync error output when execution dies * Fix lock file * Clean execution service * Clean execution service and add .env example * Add .env example * Update README to include installation instructions for Piston language packages on Linux/Mac and Windows * Fix error message --------- Co-authored-by: k1b1t0 <[email protected]> Co-authored-by: Nguyen Cao Duy <[email protected]> Co-authored-by: TramMinhMan <[email protected]>
1 parent 0fd9080 commit 62eba0d

File tree

21 files changed

+766
-468
lines changed

21 files changed

+766
-468
lines changed

README.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,22 @@ docker exec mongodb mongorestore --username admin --password password /data/back
109109
- The teaching team should be given access to the repositories as we may require viewing the history of the repository in case of any disputes or disagreements.
110110

111111
## Note for execution service
112-
- After run docker compose-up, run the next commands in terminal
112+
- After run docker compose-up, run the next commands in terminal to install language packages for Piston
113+
114+
### Linux/Mac (Bash)
113115
```bash
114116
chmod +x init-piston-languages.sh
115-
# Install language packages for Piston
116-
./init-piston-languages.sh
117+
./init-piston-languages.sh
118+
```
119+
120+
### Windows (PowerShell)
121+
```powershell
122+
powershell -ExecutionPolicy Bypass -File .\init-piston-languages.ps1
123+
```
124+
125+
Alternatively, if your PowerShell execution policy allows it:
126+
```powershell
127+
.\init-piston-languages.ps1
117128
```
118129
- APIs
119130
```bash

backend/collaboration-service/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"packageManager": "[email protected]",
1616
"dependencies": {
1717
"@y/websocket-server": "^0.1.1",
18+
"amqplib": "^0.10.9",
1819
"cors": "^2.8.5",
1920
"express": "^5.1.0",
2021
"jsonwebtoken": "^9.0.2",
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import express from 'express'
2+
import amqp from 'amqplib'
3+
import roomManager from '../websocket/roomManager.js'
4+
5+
const router = express.Router();
6+
7+
const RABBITMQ_URL = 'amqp://user:password@rabbitmq';
8+
const QUEUE_NAME = 'execution_jobs';
9+
10+
const EXECUTION_TIMEOUT_MS = 30000 // 30s
11+
const activeTimers = new Map()
12+
13+
let mqChannel = null
14+
15+
16+
export async function initRabbitMQ() {
17+
try {
18+
const connection = await amqp.connect(RABBITMQ_URL)
19+
mqChannel = await connection.createChannel()
20+
21+
await mqChannel.assertQueue(QUEUE_NAME, {durable: true})
22+
23+
console.log("RabbitMQ Producer connected")
24+
} catch (error) {
25+
console.log("Producer can't connect to RabbitMQ")
26+
console.log(error.message)
27+
process.exit(1)
28+
}
29+
}
30+
31+
async function sendJob(job) {
32+
let connection
33+
try {
34+
mqChannel.sendToQueue(
35+
QUEUE_NAME,
36+
Buffer.from(JSON.stringify(job)),
37+
{persistent: true}
38+
)
39+
40+
console.log('>>> Sent job: ', job.room_id)
41+
} catch (error) {
42+
console.log ("Error while sending job: ", error.message)
43+
}
44+
}
45+
46+
/**
47+
* SUBMIT
48+
* POST /api/v1/code/submit-code
49+
* get code from FE and push a job to MQ
50+
*/
51+
router.post("/submit-code", async (req, res) => {
52+
try {
53+
// export job
54+
const {room_id, language, source_code} = req.body
55+
const job = {
56+
room_id: room_id,
57+
language: language,
58+
source_code: source_code
59+
}
60+
61+
console.log(">>> Got code ", job)
62+
63+
// delete old timers
64+
if (activeTimers.has(room_id)) {
65+
clearTimeout(activeTimers.get(room_id))
66+
activeTimers.delete(room_id)
67+
}
68+
69+
const newTimer = setTimeout(() => {
70+
console.log('>>> Job timeout set for ', room_id)
71+
72+
activeTimers.delete(room_id)
73+
74+
const timeoutMessage = {
75+
type: "code-execution-result",
76+
data: {
77+
isError: true,
78+
output: "Execution timed out. Please try again"
79+
}
80+
}
81+
82+
roomManager.broadcastToRoom(room_id, timeoutMessage)
83+
}, EXECUTION_TIMEOUT_MS)
84+
85+
activeTimers.set(room_id, newTimer)
86+
87+
// send job to MQ
88+
await sendJob(job)
89+
res.status(202).json({
90+
message: "Job accepted"
91+
})
92+
93+
} catch (error) {
94+
console.error('Error during submission process:', error.message)
95+
res.status(500).json({
96+
error: 'Internal Server Error'
97+
})
98+
}
99+
})
100+
101+
/**
102+
* CALLBACK
103+
* POST /api/v1/code/execute-callback
104+
* get result and push to FE
105+
*/
106+
router.post("/execute-callback", async (req, res) => {
107+
try {
108+
// get result
109+
const {room_id, isError, output} = req.body
110+
111+
// delete the timeout or ignore if the job is timeout
112+
if (activeTimers.has(room_id)) {
113+
clearTimeout(activeTimers.get(room_id))
114+
activeTimers.delete(room_id)
115+
} else {
116+
console.log(`>>> Received STALE callback for ${room_id}`)
117+
return res.status(200).json({message: 'Job already timeout'})
118+
}
119+
120+
// send result to FE
121+
const message = {
122+
type: "code-execution-result",
123+
data: {
124+
isError: isError,
125+
output: output
126+
}
127+
}
128+
129+
roomManager.broadcastToRoom(room_id, message)
130+
131+
console.log('>>> Callback, broadcasted to room')
132+
console.log('room_id: ', room_id)
133+
console.log('isError: ', isError)
134+
console.log('output:', output)
135+
136+
res.status(200).json({
137+
message: 'Got result and broadcasted to room'
138+
})
139+
140+
} catch (error) {
141+
console.error('Error during submission process:', error.message)
142+
res.status(500).json({
143+
error: 'Internal Server Error'
144+
})
145+
}
146+
})
147+
148+
export default router

backend/collaboration-service/src/http/httpServer.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import cors from "cors";
33
import http from "http";
44
import config from "../config.js";
55
import roomRoutes from "./roomRoutes.js";
6+
import codeExecutionRoutes from "./codeExecutionRoutes.js"
67

78
class HttpServer {
89
constructor() {
@@ -29,6 +30,7 @@ class HttpServer {
2930
* Set up API routes
3031
*/
3132
setupRoutes() {
33+
this.app.use("/api/v1/code", codeExecutionRoutes)
3234
this.app.use("/api/v1/rooms", roomRoutes);
3335

3436
this.app.use((req, res) => {

backend/collaboration-service/src/server.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import db from "./db.js";
22
import httpServer from "./http/httpServer.js";
33
import webSocketServer from "./websocket/websocketServer.js";
4+
import {initRabbitMQ} from "./http/codeExecutionRoutes.js";
45
import { setupRoomCreationConsumer } from "./consumers/roomCreationConsumer.js";
56

67
async function startServer() {
78
try {
89
// Step 1: Connect to MongoDB
910
await db.connect();
1011

11-
// Step 2: Start HTTP server
12+
// Step 2: Connect to RabbitMQ
13+
await initRabbitMQ();
14+
15+
// Step 3: Start HTTP server
1216
await httpServer.start();
1317

14-
// Step 3: Start WebSocket server
18+
// Step 4: Start WebSocket server
1519
await webSocketServer.start();
1620

1721
// Step 4: Start Kafka consumer for room creation

backend/execution-service/.env example

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RABBITMQ_URL="amqp://user:password@rabbitmq"
2+
QUEUE_NAME="execution_jobs"
3+
CALLBACK_URL="http://collaboration-service:8004/api/v1/code/execute-callback"
4+
PISTON_URL="http://piston-api:2000/api/v2/execute"

backend/execution-service/package-lock.json

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/execution-service/package.json

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,21 @@
22
"name": "execution-service",
33
"version": "1.0.0",
44
"description": "",
5-
"main": "src/api.js",
5+
"main": "src/worker.js",
66
"scripts": {
7-
"start:api": "node src/api.js",
8-
"start:worker": "node src/worker.js",
9-
"dev:api": "nodemon src/api.js",
7+
"start": "node src/worker.js",
108
"dev:worker": "nodemon src/worker.js",
11-
"dev": "concurrently \"npm:dev:api\" \"npm:dev:worker\"",
129
"test": "echo \"Error: no test specified\" && exit 1"
1310
},
1411
"keywords": [],
1512
"author": "",
1613
"license": "ISC",
1714
"packageManager": "[email protected]",
1815
"dependencies": {
19-
"axios": "^1.12.2",
20-
"express": "^5.1.0",
21-
"mongoose": "^8.19.1"
16+
"amqplib": "^0.10.9",
17+
"axios": "^1.12.2"
2218
},
2319
"devDependencies": {
24-
"concurrently": "^9.2.1",
2520
"nodemon": "^3.1.10"
2621
}
2722
}

backend/execution-service/src/api.js

Lines changed: 0 additions & 72 deletions
This file was deleted.

0 commit comments

Comments
 (0)