Skip to content

Commit db7d90b

Browse files
authored
Merge branch 'dev' into question-service-enhancement
2 parents 34baf83 + 9001545 commit db7d90b

File tree

15 files changed

+6367
-104
lines changed

15 files changed

+6367
-104
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const router = express.Router();
77
const RABBITMQ_URL = 'amqp://user:password@rabbitmq';
88
const QUEUE_NAME = 'execution_jobs';
99

10-
const EXECUTION_TIMEOUT_MS = 30000 // 30s
10+
const EXECUTION_TIMEOUT_MS = 60000 // 60s
1111
const activeTimers = new Map()
1212

1313
let mqChannel = null
@@ -59,6 +59,9 @@ router.post("/submit-code", async (req, res) => {
5959
}
6060

6161
console.log(">>> Got code ", job)
62+
roomManager.broadcastToRoom(room_id, {
63+
type: "code-execution-started"
64+
})
6265

6366
// delete old timers
6467
if (activeTimers.has(room_id)) {

backend/collaboration-service/src/models/roomModel.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,21 @@ const roomSchema = new mongoose.Schema(
1818
},
1919
programmingLanguage: {
2020
type: String,
21-
enum: ["cpp", "java", "javascript", "python"],
21+
enum: [
22+
"c",
23+
"cpp",
24+
"csharp",
25+
"go",
26+
"java",
27+
"javascript",
28+
"kotlin",
29+
"php",
30+
"python",
31+
"ruby",
32+
"rust",
33+
"swift",
34+
"typescript",
35+
],
2236
default: "python",
2337
required: true,
2438
},

backend/execution-service/src/worker.js

Lines changed: 60 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@ const PISTON_URL = process.env.PISTON_URL
88

99
const MAX_RETRIES = 3
1010
const RETRY_DELAY_MS = 2000
11-
const PISTON_CALL_DELAY_MS = 10000
11+
const AXIOS_CONNECTION_TIMEOUT_MS = 2000
12+
const PISTON_EXECUTION_TIMEOUT_MS = 40000 // 40s
1213

1314
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
1415

15-
async function callPistonAPI(language, source_code) {
16+
async function callPistonAPI(language, source_code, timeout_ms) {
1617
try {
1718
const payload = {
1819
language: language,
@@ -21,10 +22,12 @@ async function callPistonAPI(language, source_code) {
2122
{
2223
content: source_code
2324
}
24-
]
25+
],
26+
run_timeout: 10000,
27+
compile_timeout: 30000
2528
}
2629
const response = await axios.post(PISTON_URL, payload, {
27-
timeout: PISTON_CALL_DELAY_MS
30+
timeout: timeout_ms
2831
})
2932
return response.data
3033
} catch (error) {
@@ -55,29 +58,13 @@ async function resultCallback(result) {
5558
* @returns result: {room_id, isError, output}
5659
*/
5760
async function processSubmission(submit) {
58-
// If fail to connect Piston API for 3 times, then the submit is failed
5961
let result = {}
6062
result.room_id = submit.room_id
61-
for (let i = 1; i <= MAX_RETRIES; i++) {
62-
try {
63-
const response = await callPistonAPI(submit.language, submit.source_code)
64-
65-
// set isError
66-
if (response.run.status) { // runtime error
67-
result.isError = true
68-
// get readable message or output
69-
result.output = response.run.message || response.run.output
70-
} else if (response.run.code !== 0) { // runcode != 0 error
71-
result.isError = true
72-
result.output = response.run.output
73-
} else { // successfully run the code
74-
result.isError = false
75-
result.output = response.run.output
76-
}
77-
7863

79-
console.log(">>> Finish job", result)
80-
return result
64+
// first 2 time: check for connection
65+
for (let i = 1; i <= MAX_RETRIES - 1; i++) {
66+
try {
67+
const response = await callPistonAPI(submit.language, submit.source_code, AXIOS_CONNECTION_TIMEOUT_MS)
8168
} catch (error) {
8269
if (error.response) {
8370
const statusCode = error.response.status
@@ -88,26 +75,62 @@ async function processSubmission(submit) {
8875
console.log(">>> Finish job (error)", result)
8976
return result
9077
} else {
91-
// rerun
78+
// piston temp error
9279
console.log(">>> Piston api temporary error: ", statusCode)
9380
}
94-
} else {
95-
// rerun
81+
} else if (axios.isAxiosError(error) && error.code !== 'ECONNREFUSED') {
82+
// timeout / no connection
9683
console.log(">>> Piston api doesn't respond\n", error.message)
84+
} else {
85+
// ECONNREFUSED
86+
console.log(">>> Piston api is dead (ECONNREFUSED)\n", error.message)
9787
}
98-
if (i === MAX_RETRIES) {
99-
break
88+
89+
// next try
90+
const delay = RETRY_DELAY_MS * i;
91+
console.log(`>>> Wait ${delay}ms before reconnect to Piston api`)
92+
await sleep(delay)
93+
continue
94+
}
95+
// if no error, quit try loop
96+
break
97+
}
98+
99+
// final try: timeout 40s
100+
try {
101+
const response = await callPistonAPI(submit.language, submit.source_code, PISTON_EXECUTION_TIMEOUT_MS)
102+
103+
// set isError
104+
if (response.run.status) { // runtime error
105+
result.isError = true
106+
// get readable message or output
107+
result.output = response.run.message || response.run.output
108+
} else if (response.run.code !== 0) { // runcode != 0 error
109+
result.isError = true
110+
result.output = response.run.output
111+
} else { // successfully run the code
112+
result.isError = false
113+
result.output = response.run.output
114+
}
115+
116+
117+
console.log(">>> Finish job", result)
118+
return result
119+
} catch (error) {
120+
if (error.response) {
121+
const statusCode = error.response.status
122+
if (statusCode >= 400 && statusCode < 500) {
123+
result.isError = true
124+
result.output = error.response.data.message
125+
return result
100126
}
101127
}
102-
const delay = RETRY_DELAY_MS * i;
103-
console.log(`>>> Wait ${delay}ms before reconnect to Piston api`)
104-
await sleep(delay)
128+
// failed (after 3 tries)
129+
result.isError = true
130+
result.output = "Code execution service is unavailable. Please try again later."
131+
console.log('>>> Failed job ', result)
132+
return result
105133
}
106-
// failed (after 3 tries)
107-
result.isError = true
108-
result.output = "Code execution service is unavailable. Please try again later."
109-
console.log('>>> Failed job ', result)
110-
return result
111134
}
112135

113136
async function startWorker() {

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ services:
202202
ports:
203203
- '2000:2000'
204204
privileged: true
205+
environment:
206+
- PISTON_COMPILE_TIMEOUT=30000
207+
- PISTON_RUN_TIMEOUT=10000
205208
volumes:
206209
- piston_packages:/piston/packages
207210
networks:

frontend/Dockerfile.frontend

Lines changed: 44 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,66 @@
1+
# ---------- deps ----------
12
FROM node:20-alpine AS deps
23
RUN apk add --no-cache libc6-compat
34
WORKDIR /app
45

5-
# Install pnpm
6-
RUN npm install -g pnpm
6+
RUN corepack enable && corepack prepare [email protected] --activate
77

8-
# Copy package files
9-
COPY package.json ./
8+
RUN echo "node-linker=hoisted" > .npmrc
109

11-
# Install dependencies
12-
RUN pnpm install --no-frozen-lockfile
10+
COPY package.json pnpm-lock.yaml ./
1311

12+
# Pre-fetch store
13+
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
14+
pnpm fetch
15+
16+
# ---------- builder ----------
1417
FROM node:20-alpine AS builder
18+
RUN apk add --no-cache libc6-compat
1519
WORKDIR /app
1620

17-
# Install pnpm
18-
RUN npm install -g pnpm
21+
RUN corepack enable && corepack prepare pnpm@9.12.1 --activate
22+
RUN echo "node-linker=hoisted" > .npmrc
1923

20-
# Copy dependencies from deps stage
21-
COPY --from=deps /app/node_modules ./node_modules
22-
COPY --from=deps /app/package.json ./package.json
24+
ARG USER_SERVICE_URL
25+
ARG MATCHING_SERVICE_URL
26+
ARG QUESTION_SERVICE_URL
27+
ARG COLLABORATION_SERVICE_URL
2328

24-
# Copy source code
25-
COPY . .
29+
ENV NEXT_PUBLIC_USER_SERVICE_URL=$USER_SERVICE_URL \
30+
NEXT_PUBLIC_MATCHING_SERVICE_URL=$MATCHING_SERVICE_URL \
31+
NEXT_PUBLIC_QUESTION_SERVICE_URL=$QUESTION_SERVICE_URL \
32+
NEXT_PUBLIC_COLLABORATION_SERVICE_URL=$COLLABORATION_SERVICE_URL \
33+
NEXT_TELEMETRY_DISABLED=1 \
34+
NEXT_DISABLE_SOURCEMAPS=1
2635

27-
# Set environment to production for build
28-
ENV NODE_ENV=production
29-
ENV NEXT_TELEMETRY_DISABLED=1
36+
COPY package.json pnpm-lock.yaml ./
37+
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
38+
pnpm install --frozen-lockfile --prod=false
3039

31-
# Build Next.js application
32-
RUN pnpm build
40+
# Copy the rest of the source
41+
COPY . .
42+
43+
# Build with caches
44+
RUN --mount=type=cache,target=/app/.next/cache \
45+
--mount=type=cache,target=/root/.cache \
46+
pnpm build
3347

48+
# ---------- runner ----------
3449
FROM node:20-alpine AS runner
3550
WORKDIR /app
51+
ENV NODE_ENV=production \
52+
NEXT_TELEMETRY_DISABLED=1 \
53+
PORT=3000 \
54+
HOSTNAME=0.0.0.0
3655

37-
ENV NODE_ENV=production
38-
ENV NEXT_TELEMETRY_DISABLED=1
39-
40-
RUN addgroup --system --gid 1001 nodejs
41-
RUN adduser --system --uid 1001 nextjs
56+
RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
4257

43-
# Install pnpm
44-
RUN npm install -g pnpm
45-
46-
# Copy necessary files from builder
47-
COPY --from=builder /app/package.json ./package.json
48-
COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next
49-
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
50-
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
58+
# Copy standalone output from Next
59+
COPY --from=builder /app/.next/standalone ./
60+
COPY --from=builder /app/.next/static ./.next/static
61+
COPY --from=builder /app/public ./public
5162

5263
USER nextjs
53-
5464
EXPOSE 3000
55-
56-
ENV PORT=3000
57-
ENV HOSTNAME="0.0.0.0"
58-
59-
CMD ["pnpm", "start"]
60-
65+
# In standalone output, the entrypoint is `server.js` at repo root
66+
CMD ["node", "server.js"]

frontend/next.config.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,43 @@
11
import type { NextConfig } from "next";
2+
import path from "node:path";
23

34
const nextConfig: NextConfig = {
4-
/* config options here */
5+
output: "standalone",
6+
7+
eslint: {
8+
ignoreDuringBuilds: true,
9+
},
10+
typescript: {
11+
ignoreBuildErrors: false,
12+
},
13+
14+
productionBrowserSourceMaps: false,
15+
swcMinify: true,
16+
17+
optimizeFonts: false,
18+
19+
experimental: {
20+
optimizePackageImports: ["lodash", "date-fns", "rxjs"],
21+
},
22+
23+
webpack: (config, { isServer }) => {
24+
config.cache = {
25+
type: "filesystem",
26+
cacheDirectory: path.join(process.cwd(), ".next", "cache", isServer ? "server" : "client"),
27+
buildDependencies: {
28+
config: [__filename],
29+
},
30+
};
31+
32+
config.resolve.fallback = {
33+
...config.resolve.fallback,
34+
fs: false,
35+
path: false,
36+
os: false,
37+
};
38+
39+
return config;
40+
},
541
};
642

743
export default nextConfig;

0 commit comments

Comments
 (0)