Skip to content

Commit edbc594

Browse files
Merge pull request #19 from abhishek97/runModes
add support for mode parameter for POST /runs
2 parents 4c98006 + 2c2e0b3 commit edbc594

File tree

9 files changed

+350
-28
lines changed

9 files changed

+350
-28
lines changed

config.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,14 @@ exports = module.exports = {
2323
PASS: process.env.AMQP_PASS || 'codingblocks',
2424
HOST: process.env.AMQP_HOST || 'localhost',
2525
PORT: process.env.AMQP_PORT || 5672
26+
},
27+
28+
S3: {
29+
endpoint: process.env.S3_ENDPOINT || 'localhost',
30+
port: process.env.S3_PORT || 9000,
31+
ssl: process.env.S3_SSL || false,
32+
accessKey: process.env.S3_ACCESS_KEY || '',
33+
secretKey: process.env.S3_SECRET_KEY || '',
34+
bucket: process.env.S3_BUCKET || 'judge-submissions'
2635
}
2736
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
alter table submissions
2+
add column outputs varchar[];

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "judge-api",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Judge API",
55
"main": "dist/server.js",
66
"repository": "https://github.com/coding-blocks/judge-api",
@@ -11,17 +11,21 @@
1111
"@types/amqplib": "^0.5.4",
1212
"amqplib": "^0.5.2",
1313
"apidoc": "^0.17.6",
14+
"axios": "^0.18.0",
1415
"base-64": "^0.1.0",
1516
"debug": "^4.0.0",
1617
"express": "^4.16.2",
18+
"minio": "^7.0.3",
1719
"pg": "^7.4.3",
1820
"pg-hstore": "^2.3.2",
19-
"sequelize": "^4.22.6"
21+
"sequelize": "^4.22.6",
22+
"uuid": "^3.3.2"
2023
},
2124
"devDependencies": {
2225
"@types/chai": "^4.0.4",
2326
"@types/debug": "^0.0.30",
2427
"@types/express": "^4.0.39",
28+
"@types/minio": "^7.0.1",
2529
"@types/mocha": "^5.0.0",
2630
"@types/request": "^2.0.8",
2731
"@types/sequelize": "^4.0.79",

src/db/models.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const Langs = db.define('langs', {
2727
})
2828
export type LangsAttributes = { lang_slug: string, lang_name:string, lang_version: string }
2929

30-
const Submissions = db.define('submissions', {
30+
const Submissions = db.define<SubmissionInstance ,SubmissionAttributes>('submissions', {
3131
id: {
3232
type: Sequelize.INTEGER,
3333
autoIncrement: true,
@@ -42,7 +42,8 @@ const Submissions = db.define('submissions', {
4242
},
4343
start_time: Sequelize.DATE,
4444
end_time: Sequelize.DATE,
45-
results: Sequelize.ARRAY(Sequelize.INTEGER)
45+
results: Sequelize.ARRAY(Sequelize.INTEGER),
46+
outputs: Sequelize.ARRAY(Sequelize.STRING),
4647
}, {
4748
paranoid: true, // We do not want to lose any submission data
4849
timestamps: false // Start and end times are already logged
@@ -53,8 +54,11 @@ export type SubmissionAttributes = {
5354
start_time: Date
5455
end_time?: Date
5556
results?: Array<number>
57+
outputs?: Array<string>
5658
}
5759

60+
export type SubmissionInstance = Sequelize.Instance<SubmissionAttributes> & SubmissionAttributes
61+
5862
const ApiKeys = db.define('apikeys', {
5963
id: {
6064
type: Sequelize.INTEGER,

src/routes/api/run.ts

Lines changed: 106 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
import {Response, Router, Request} from 'express'
2-
import {SubmissionAttributes, Submissions} from '../../db/models'
2+
import axios from 'axios'
3+
4+
import {SubmissionAttributes, Submissions, db} from '../../db/models'
35
import {RunJob, queueJob, successListener} from '../../rabbitmq/jobqueue'
46
import {isInvalidRunRequest} from '../../validators/SubmissionValidators'
7+
import {upload} from '../../utils/s3'
58
import config = require('../../../config')
69

710
const route: Router = Router()
811

912
export type RunRequestBody = {
1013
source: string, //Base64 encoded
1114
lang: string,
12-
stdin: string
15+
stdin: string,
16+
mode: string,
17+
callback?: string
1318
}
1419
export interface RunRequest extends Request {
1520
body: RunRequestBody
@@ -21,7 +26,77 @@ export interface RunResponse {
2126
stderr: string
2227
}
2328

24-
const runPool: {[x: number]: Response} = {}
29+
export type RunPoolElement = {
30+
mode: string,
31+
res: Response,
32+
callback?: string
33+
}
34+
35+
const runPool: {[x: number]: RunPoolElement} = {}
36+
37+
const handleTimeoutForSubmission = function (submissionId: number) {
38+
const job = runPool[submissionId]
39+
const errorResponse = {
40+
id: submissionId,
41+
code: 408,
42+
message: "Compile/Run timed out",
43+
}
44+
45+
switch (job.mode) {
46+
case 'sync':
47+
job.res.status(408).json(errorResponse)
48+
break;
49+
case 'callback':
50+
axios.post(job.callback, errorResponse)
51+
}
52+
}
53+
54+
const handleSuccessForSubmission = function (result: RunResponse) {
55+
const job = runPool[result.id]
56+
switch (job.mode) {
57+
case 'sync':
58+
job.res.status(200).json(result)
59+
break;
60+
case 'callback':
61+
// send a post request to callback
62+
(async () => {
63+
// 1. upload the result to s3 and get the url
64+
const {url} = await upload(result)
65+
66+
// 2. save the url in db
67+
await Submissions.update(<any>{
68+
outputs: [url]
69+
}, {
70+
where: {
71+
id: result.id
72+
}
73+
})
74+
75+
// make the callback request
76+
await axios.post(job.callback, {id: result.id, outputs: [url]})
77+
})()
78+
break;
79+
}
80+
}
81+
82+
/**
83+
* Returns a runPoolElement for request
84+
*/
85+
const getRunPoolElement = function (body: RunRequestBody, res: Response): RunPoolElement {
86+
switch (body.mode) {
87+
case 'sync':
88+
return ({
89+
mode: 'sync',
90+
res
91+
})
92+
case 'callback':
93+
return ({
94+
mode: 'callback',
95+
res,
96+
callback: body.callback
97+
})
98+
}
99+
}
25100

26101
/**
27102
* @api {post} /runs POST /runs
@@ -33,6 +108,8 @@ const runPool: {[x: number]: Response} = {}
33108
* @apiParam {String(Base64)} source source code to run (encoded in base64)
34109
* @apiParam {Enum} lang Language of code to execute
35110
* @apiParam {String(Base64)} input [Optional] stdin input for the program (encoded in base64)
111+
* @apiParam {Enum} mode [Optional] mode for request. Default = `sync`, see: https://github.com/coding-blocks/judge-api/issues/16
112+
* @apiParam {String)} callback [Optional] callback url for request. Required for `mode = callback`
36113
*
37114
* @apiUse AvailableLangs
38115
*
@@ -41,14 +118,26 @@ const runPool: {[x: number]: Response} = {}
41118
* @apiSuccess {String(Base64)} stderr Output of stderr of execution (encoded in base64)
42119
* @apiSuccess {Number} statuscode Result of operation
43120
*
44-
* @apiSuccessExample {JSON} Success-Response:
121+
* @apiSuccessExample {JSON} Success-Response(mode=sync):
45122
* HTTP/1.1 200 OK
46123
* {
47124
* "id": 10,
48125
* "statuscode": 0,
49126
* "stdout": "NA0KMg0KMw=="
50127
* "stderr": "VHlwZUVycm9y"
51128
* }
129+
* @apiSuccessExample {JSON} Success-Response(mode=callback):
130+
* HTTP/1.1 200 OK
131+
* {
132+
* "id": 10
133+
* }
134+
*
135+
* @apiSuccessExample {JSON} Body for Callback(mode=callback):
136+
* HTTP/1.1 200 OK
137+
* {
138+
* "id": 10,
139+
* "outputs": ["http://localhost/judge-submissions/file.json"]
140+
* }
52141
*/
53142
route.post('/', (req, res, next) => {
54143
const invalidRequest = isInvalidRunRequest(req)
@@ -70,19 +159,24 @@ route.post('/', (req, res, next) => {
70159
lang: req.body.lang,
71160
stdin: req.body.stdin
72161
})
162+
73163
// Put into pool and wait for judge-worker to respond
74-
runPool[submission.id] = res
164+
runPool[submission.id] = getRunPoolElement(req.body, res)
165+
75166
setTimeout(() => {
76167
if (runPool[submission.id]) {
77-
runPool[submission.id].status(408).json({
78-
id: submission.id,
79-
code: 408,
80-
message: "Compile/Run timed out",
81-
})
168+
handleTimeoutForSubmission(submission.id)
82169
delete runPool[submission.id]
83170
}
84171
}, config.RUN.TIMEOUT)
85172

173+
switch (req.body.mode) {
174+
case 'callback':
175+
res.json({
176+
id: submission.id
177+
})
178+
}
179+
86180
}).catch(err => {
87181
res.status(501).json({
88182
code: 501,
@@ -97,10 +191,10 @@ route.post('/', (req, res, next) => {
97191
*/
98192
successListener.on('success', (result: RunResponse) => {
99193
if (runPool[result.id]) {
100-
runPool[result.id].status(200).json(result)
194+
handleSuccessForSubmission(result)
101195
delete runPool[result.id]
102196
}
103-
Submissions.update({
197+
Submissions.update(<any>{
104198
end_time: new Date()
105199
}, {
106200
where: {

src/utils/s3.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Minio = require('minio')
2+
import v4 = require('uuid/v4')
3+
import config = require('../../config')
4+
5+
const client = new Minio.Client({
6+
endPoint: config.S3.endpoint,
7+
port: config.S3.port,
8+
useSSL: config.S3.ssl,
9+
accessKey: config.S3.accessKey,
10+
secretKey: config.S3.secretKey,
11+
})
12+
13+
export type savedFile = {
14+
etag: string,
15+
url: string
16+
}
17+
18+
export const urlForFilename = (bucket: string, filename: string) : string => `http${config.S3.ssl ? 's': ''}://${config.S3.endpoint}/${bucket}/${filename}`
19+
20+
export const upload = function (object:object, filename:string = v4() + '.json' ,bucket:string = config.S3.bucket) : Promise<savedFile> {
21+
return new Promise((resolve, reject) => {
22+
client.putObject(bucket, filename, JSON.stringify(object), function(err, etag) {
23+
if (err) return reject(err)
24+
resolve({etag, url: urlForFilename(bucket, filename) })
25+
})
26+
})
27+
}

src/validators/SubmissionValidators.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ export function isInvalidRunRequest(req: Request): Error | boolean {
1212
if (!req.body.stdin) {
1313
req.body.stdin = ''
1414
}
15+
if (!req.body.mode) {
16+
req.body.mode = 'sync'
17+
}
18+
if (!['sync', 'callback'].includes(req.body.mode)) {
19+
return new Error('Mode must be one of sync, callback')
20+
}
21+
if (req.body.mode === 'callback' && !req.body.callback) {
22+
return new Error('Must specify a callback for callback mode')
23+
}
1524

1625
return false
1726
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"compilerOptions": {
33
"module": "commonjs",
4-
"target": "es2015",
4+
"target": "es2016",
55
"moduleResolution": "node",
66
"allowSyntheticDefaultImports": true,
77
"sourceMap": true,

0 commit comments

Comments
 (0)