Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /