restHandler.ts 11.1 KB
Newer Older
liuzhe-lz's avatar
liuzhe-lz committed
1
2
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
Deshui Yu's avatar
Deshui Yu committed
3
4
5
6
7
8
9
10
11

'use strict';

import { Request, Response, Router } from 'express';
import * as path from 'path';

import * as component from '../common/component';
import { DataStore, MetricDataRecord, TrialJobInfo } from '../common/datastore';
import { NNIError, NNIErrorNames } from '../common/errors';
SparkSnail's avatar
SparkSnail committed
12
import { isNewExperiment, isReadonly } from '../common/experimentStartupInfo';
Deshui Yu's avatar
Deshui Yu committed
13
import { getLogger, Logger } from '../common/log';
SparkSnail's avatar
SparkSnail committed
14
import { ExperimentProfile, Manager, TrialJobStatistics, ExperimentStartUpMode } from '../common/manager';
15
16
import { ValidationSchemas } from './restValidationSchemas';
import { NNIRestServer } from './nniRestServer';
17
import { getVersion } from '../common/utils';
Deshui Yu's avatar
Deshui Yu committed
18

19
20
const expressJoi = require('express-joi-validator');

Deshui Yu's avatar
Deshui Yu committed
21
class NNIRestHandler {
22
    private restServer: NNIRestServer;
Deshui Yu's avatar
Deshui Yu committed
23
24
25
    private nniManager: Manager;
    private log: Logger;

26
    constructor(rs: NNIRestServer) {
Deshui Yu's avatar
Deshui Yu committed
27
28
29
30
31
32
33
34
35
36
        this.nniManager = component.get(Manager);
        this.restServer = rs;
        this.log = getLogger();
    }

    public createRestHandler(): Router {
        const router: Router = Router();

        // tslint:disable-next-line:typedef
        router.use((req: Request, res: Response, next) => {
chicm-ms's avatar
chicm-ms committed
37
            this.log.debug(`${req.method}: ${req.url}: body:\n${JSON.stringify(req.body, undefined, 4)}`);
Deshui Yu's avatar
Deshui Yu committed
38
39
40
41
42
43
44
45
            res.header('Access-Control-Allow-Origin', '*');
            res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
            res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS');

            res.setHeader('Content-Type', 'application/json');
            next();
        });

Gems Guo's avatar
Gems Guo committed
46
        this.version(router);
Deshui Yu's avatar
Deshui Yu committed
47
48
49
        this.checkStatus(router);
        this.getExperimentProfile(router);
        this.updateExperimentProfile(router);
50
        this.importData(router);
Deshui Yu's avatar
Deshui Yu committed
51
52
53
54
55
56
57
58
        this.startExperiment(router);
        this.getTrialJobStatistics(router);
        this.setClusterMetaData(router);
        this.listTrialJobs(router);
        this.getTrialJob(router);
        this.addTrialJob(router);
        this.cancelTrialJob(router);
        this.getMetricData(router);
59
60
        this.getMetricDataByRange(router);
        this.getLatestMetricData(router);
61
        this.exportData(router);
Deshui Yu's avatar
Deshui Yu committed
62

63
64
65
66
67
68
69
70
71
        // Express-joi-validator configuration
        router.use((err: any, req: Request, res: Response, next: any) => {
            if (err.isBoom) {
                this.log.error(err.output.payload);

                return res.status(err.output.statusCode).json(err.output.payload);
            }
        });

Deshui Yu's avatar
Deshui Yu committed
72
73
74
        return router;
    }

SparkSnail's avatar
SparkSnail committed
75
    private handle_error(err: Error, res: Response, isFatal: boolean = false, errorCode: number = 500): void {
Deshui Yu's avatar
Deshui Yu committed
76
77
78
        if (err instanceof NNIError && err.name === NNIErrorNames.NOT_FOUND) {
            res.status(404);
        } else {
SparkSnail's avatar
SparkSnail committed
79
            res.status(errorCode);
Deshui Yu's avatar
Deshui Yu committed
80
81
82
83
        }
        res.send({
            error: err.message
        });
84
85

        // If it's a fatal error, exit process
chicm-ms's avatar
chicm-ms committed
86
        if (isFatal) {
87
            this.log.fatal(err);
88
            process.exit(1);
chicm-ms's avatar
chicm-ms committed
89
90
        } else {
            this.log.error(err);
91
        }
Deshui Yu's avatar
Deshui Yu committed
92
93
    }

Gems Guo's avatar
Gems Guo committed
94
95
    private version(router: Router): void {
        router.get('/version', async (req: Request, res: Response) => {
96
97
            const version = await getVersion();
            res.send(version);
Gems Guo's avatar
Gems Guo committed
98
99
100
        });
    }

Deshui Yu's avatar
Deshui Yu committed
101
102
103
104
105
    // TODO add validators for request params, query, body
    private checkStatus(router: Router): void {
        router.get('/check-status', (req: Request, res: Response) => {
            const ds: DataStore = component.get<DataStore>(DataStore);
            ds.init().then(() => {
106
                res.send(this.nniManager.getStatus());
Deshui Yu's avatar
Deshui Yu committed
107
108
109
            }).catch(async (err: Error) => {
                this.handle_error(err, res);
                this.log.error(err.message);
chicm-ms's avatar
chicm-ms committed
110
                this.log.error(`Datastore initialize failed, stopping rest server...`);
Deshui Yu's avatar
Deshui Yu committed
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
                await this.restServer.stop();
            });
        });
    }

    private getExperimentProfile(router: Router): void {
        router.get('/experiment', (req: Request, res: Response) => {
            this.nniManager.getExperimentProfile().then((profile: ExperimentProfile) => {
                res.send(profile);
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

    private updateExperimentProfile(router: Router): void {
127
        router.put('/experiment', expressJoi(ValidationSchemas.UPDATEEXPERIMENT), (req: Request, res: Response) => {
Deshui Yu's avatar
Deshui Yu committed
128
129
130
131
132
133
134
            this.nniManager.updateExperimentProfile(req.body, req.query.update_type).then(() => {
                res.send();
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }
135

136
137
138
139
140
141
142
143
144
    private importData(router: Router): void {
        router.post('/experiment/import-data', (req: Request, res: Response) => {
            this.nniManager.importData(JSON.stringify(req.body)).then(() => {
                res.send();
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }
Deshui Yu's avatar
Deshui Yu committed
145
146

    private startExperiment(router: Router): void {
147
        router.post('/experiment', expressJoi(ValidationSchemas.STARTEXPERIMENT), (req: Request, res: Response) => {
Deshui Yu's avatar
Deshui Yu committed
148
149
150
151
152
153
            if (isNewExperiment()) {
                this.nniManager.startExperiment(req.body).then((eid: string) => {
                    res.send({
                        experiment_id: eid
                    });
                }).catch((err: Error) => {
154
                    // Start experiment is a step of initialization, so any exception thrown is a fatal
Deshui Yu's avatar
Deshui Yu committed
155
156
157
                    this.handle_error(err, res);
                });
            } else {
SparkSnail's avatar
SparkSnail committed
158
                this.nniManager.resumeExperiment(isReadonly()).then(() => {
Deshui Yu's avatar
Deshui Yu committed
159
160
                    res.send();
                }).catch((err: Error) => {
161
                    // Resume experiment is a step of initialization, so any exception thrown is a fatal
Deshui Yu's avatar
Deshui Yu committed
162
163
                    this.handle_error(err, res);
                });
SparkSnail's avatar
SparkSnail committed
164
            } 
Deshui Yu's avatar
Deshui Yu committed
165
166
167
168
169
170
171
172
173
174
175
176
177
178
        });
    }

    private getTrialJobStatistics(router: Router): void {
        router.get('/job-statistics', (req: Request, res: Response) => {
            this.nniManager.getTrialJobStatistics().then((statistics: TrialJobStatistics[]) => {
                res.send(statistics);
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

    private setClusterMetaData(router: Router): void {
179
180
181
        router.put(
            '/experiment/cluster-metadata', expressJoi(ValidationSchemas.SETCLUSTERMETADATA),
            async (req: Request, res: Response) => {
SparkSnail's avatar
SparkSnail committed
182
183
184
185
186
187
188
189
190
191
192
                // tslint:disable-next-line:no-any
                const metadata: any = req.body;
                const keys: string[] = Object.keys(metadata);
                try {
                    for (const key of keys) {
                        await this.nniManager.setClusterMetadata(key, JSON.stringify(metadata[key]));
                    }
                    res.send();
                } catch (err) {
                    // setClusterMetata is a step of initialization, so any exception thrown is a fatal
                    this.handle_error(NNIError.FromError(err), res, true);
Deshui Yu's avatar
Deshui Yu committed
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
                }
        });
    }

    private listTrialJobs(router: Router): void {
        router.get('/trial-jobs', (req: Request, res: Response) => {
            this.nniManager.listTrialJobs(req.query.status).then((jobInfos: TrialJobInfo[]) => {
                jobInfos.forEach((trialJob: TrialJobInfo) => {
                    this.setErrorPathForFailedJob(trialJob);
                });
                res.send(jobInfos);
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

    private getTrialJob(router: Router): void {
        router.get('/trial-jobs/:id', (req: Request, res: Response) => {
            this.nniManager.getTrialJob(req.params.id).then((jobDetail: TrialJobInfo) => {
                const jobInfo: TrialJobInfo = this.setErrorPathForFailedJob(jobDetail);
                res.send(jobInfo);
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

    private addTrialJob(router: Router): void {
        router.post('/trial-jobs', async (req: Request, res: Response) => {
223
224
            this.nniManager.addCustomizedTrialJob(JSON.stringify(req.body)).then((sequenceId: number) => {
                res.send({sequenceId});
Deshui Yu's avatar
Deshui Yu committed
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

    private cancelTrialJob(router: Router): void {
        router.delete('/trial-jobs/:id', async (req: Request, res: Response) => {
            this.nniManager.cancelTrialJobByUser(req.params.id).then(() => {
                res.send();
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

    private getMetricData(router: Router): void {
242
        router.get('/metric-data/:job_id*?', async (req: Request, res: Response) => {
Deshui Yu's avatar
Deshui Yu committed
243
            this.nniManager.getMetricData(req.params.job_id, req.query.type).then((metricsData: MetricDataRecord[]) => {
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
                res.send(metricsData);
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

    private getMetricDataByRange(router: Router): void {
        router.get('/metric-data-range/:min_seq_id/:max_seq_id', async (req: Request, res: Response) => {
            const minSeqId = Number(req.params.min_seq_id);
            const maxSeqId = Number(req.params.max_seq_id);
            this.nniManager.getMetricDataByRange(minSeqId, maxSeqId).then((metricsData: MetricDataRecord[]) => {
                res.send(metricsData);
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

    private getLatestMetricData(router: Router): void {
        router.get('/metric-data-latest/', async (req: Request, res: Response) => {
            this.nniManager.getLatestMetricData().then((metricsData: MetricDataRecord[]) => {
Deshui Yu's avatar
Deshui Yu committed
266
267
268
269
270
271
272
                res.send(metricsData);
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

273
274
275
276
277
278
279
280
281
282
    private exportData(router: Router): void {
        router.get('/export-data', (req: Request, res: Response) => {
            this.nniManager.exportData().then((exportedData: string) => {
                res.send(exportedData);
            }).catch((err: Error) => {
                this.handle_error(err, res);
            });
        });
    }

Deshui Yu's avatar
Deshui Yu committed
283
284
285
286
    private setErrorPathForFailedJob(jobInfo: TrialJobInfo): TrialJobInfo {
        if (jobInfo === undefined || jobInfo.status !== 'FAILED' || jobInfo.logPath === undefined) {
            return jobInfo;
        }
chicm-ms's avatar
chicm-ms committed
287
        jobInfo.stderrPath = path.join(jobInfo.logPath, 'stderr');
Deshui Yu's avatar
Deshui Yu committed
288
289
290
291
292

        return jobInfo;
    }
}

293
export function createRestHandler(rs: NNIRestServer): Router {
Deshui Yu's avatar
Deshui Yu committed
294
295
296
297
    const handler: NNIRestHandler = new NNIRestHandler(rs);

    return handler.createRestHandler();
}