Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cron #254

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
149 changes: 149 additions & 0 deletions app/controllers/cronjobs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
CronJobs Controller
*/

// native modules

// 3rd party modules
const Promise = require('bluebird');
const _ = require('lodash');
const mongoose = require('mongoose');

// own modules
const {Collection, Model, setHandler} = require('../models/cronjob');
const DefaultController = require('./');
const logger = require('../tools/logger');


class CronJobsController extends DefaultController {
constructor() {
super(Collection);
this.logger = logger;
setHandler(this._handler.bind(this));
Model.on('mongoose-cron:error', CronJobsController._onError);
this._cron = Model.createCron().start();
logger.info('Cron started');
}
_handler(doc) {
const prefix = `Cronjob '${doc.name}':`;
const cronLogger = {
debug: msg => logger.debug(`${prefix} ${msg}`),
error: msg => logger.error(`${prefix} ${msg}`),
warn: msg => logger.warn(`${prefix} ${msg}`),
info: msg => logger.info(`${prefix} ${msg}`)
};
cronLogger.info('started');
const {type} = doc.type;
const defaultHandler = this._handleViewAggregate;
const handlers = {
view: this._handleViewAggregate,
email: this._handleEmail
// @todo support for more different types..
};
const handler = _.get(handlers, type, defaultHandler);
const startTime = new Date();
return handler.bind(this)(doc, cronLogger)
.then(() => {
const duration = new Date() - startTime;
cronLogger.info(` took ${duration / 1000} seconds`);
});
}
showView(req, res) {
const view = _.get(req.cronjobs.toObject(), 'view.view');
if (!view) {
return res.status(404).json({message: `View ${view} not found`});
}
if (req.cronjobs.view.processing) {
this.logger.silly(`Requesting processing view: ${req.cronjobs.name}`);
return res.status(503).json({message: `View ${view} is under processing`});
}
const collectionName = CronJobsController._getViewCollection(view);
return CronJobsController._validCollection(collectionName)
.then(() => CronJobsController._getQuery(req))
.then(query => mongoose.connection.db.collection(collectionName).find(query))
.then(docs => docs.toArray())
.then(docs => res.json(docs))
.catch((error) => {
logger.warn(`showView rejected with: ${error}`);
const statusCode = error.statusCode || 500;
res.status(statusCode).json({error: `${error}`, stack: error.stack});
});
}
static _getQuery(req) {
if (req.query.q) {
return CronJobsController.parseJson(req.query.q);
}
return {};
}
static parseJson(str) {
return Promise.try(() => JSON.parse(str))
.catch((error) => {
_.set(error, 'statusCode', 400); // bad request
throw error;
});
}
static _getCollectionNames() {
const pending = mongoose.connection.db.listCollections().toArray();
return pending.then(colls => _.map(colls, col => col.name));
}
static _hasCollection(col) {
const pending = CronJobsController._getCollectionNames();
return pending.then(colls => (colls.indexOf(col) >= 0));
}
static _validCollection(col) {
return CronJobsController._hasCollection(col)
.then((yes) => {
if (!yes) {
const error = new Error(`Collection ${col} not found`);
error.statusCode = 404;
throw error;
}
return col;
});
}
static _getViewCollection(view) {
return `cronjobs.${view}`;
}
_handleViewAggregate(doc, cronLogger) {
return Promise.try(() => {
const docJson = doc.toJSON();
this.logger.debug(`Handle view: ${doc.name}`);
const {col, aggregate, view} = docJson.view;

// validate
if (_.find(mongoose.modelNames(), view) >= 0) {
const msg = 'Cannot overwrite default collections!';
cronLogger.warn(msg);
throw new Error(msg);
}
if (!col) {
throw new Error('view.coll is missing');
}
if (!view) {
throw new Error('view.view is missing');
}
if (!aggregate) {
throw new Error('view.aggregate is missing');
}
// all seems to be okay.. -> let processing
return CronJobsController._hasCollection(col)
.then(yes => (yes ? Model.db.dropCollection(view) : true))
.then(() => CronJobsController.parseJson(aggregate))
.then(json => Model.db.createCollection(
CronJobsController._getViewCollection(view),
{viewOn: col, aggregate: json}
));
});
}
_handleEmail(doc) {
const msg = `Cronjob ${doc.name} uses email type but it is not supported yet`;
this.logger.warn(msg);
throw new Error(msg);
}
static _onError(error, doc) {
logger.error(`Cronjob '${_.get(doc, 'name')}' error: ${error}`);
logger.error(error.stack);
}
}

module.exports = CronJobsController;
53 changes: 53 additions & 0 deletions app/models/cronjob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const mongoose = require('mongoose');
const QueryPlugin = require('mongoose-query');
const {cronPlugin} = require('mongoose-cron');


const {Schema} = mongoose;
const {Types} = Schema;
const {ObjectId} = Types;

const requireForView = function () { return this.type === 'view'; };


const CronJobSchema = new Schema({
cre: {
time: {type: Date, default: Date.now},
user: {type: ObjectId, ref: 'User'}
},
mod: {
time: {type: Date, default: Date.now},
user: {type: ObjectId, ref: 'User'}
},
name: {type: String},
type: {type: String, enum: ['view'], default: 'view'},
view: {
view: {type: String, required: requireForView},
col: {type: String, required: requireForView},
aggregate: {type: String, required: requireForView}
}
});

/**
* Register plugins
*/
CronJobSchema.plugin(QueryPlugin); // install QueryPlugin

let interHandler = () => {};
const setHandler = (func) => {
interHandler = func;
};
CronJobSchema.plugin(cronPlugin, {
handler: doc => interHandler(doc), // triggered on job processing
// When there are no jobs to process, wait 30s before
// checking for processable jobs again (default: 0).
idleDelay: 1000,
// Wait 60s before processing the same job again in case
// the job is a recurring job (default: 0).
nextDelay: 1000
});

const Collection = 'cronjobs';
const Model = mongoose.model(Collection, CronJobSchema);

module.exports = {Model, Collection, Schema: CronJobSchema, setHandler};
34 changes: 34 additions & 0 deletions app/routes/cronjobs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// 3rd party modules
const express = require('express');

// application modules
const {requireAuth} = require('./middlewares/authorization');
const CronJobsController = require('./../controllers/cronjobs');

function Route(app) {
const router = express.Router();
const controller = new CronJobsController();

router.param('cronjobs', controller.modelParam.bind(controller));

router.route('/')
.all(requireAuth)
.all(controller.all.bind(controller))
.get(controller.find.bind(controller))
.post(controller.create.bind(controller));

router.route('/:cronjobs')
.all(requireAuth)
.all(controller.all.bind(controller))
.get(controller.get.bind(controller))
.put(controller.update.bind(controller))
.delete(controller.remove.bind(controller));

router.route('/:cronjobs/view')
.all(requireAuth)
.get(controller.showView.bind(controller));

app.use('/api/v0/cron', router);
}

module.exports = Route;
81 changes: 81 additions & 0 deletions doc/APIs/cronjobs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Cron API

Cron API allows to automate background tasks, e.g.
to generate read-only views using mongodb aggregates.


**NOTE** Following API's require authentication token to work.

Data schema available [here](../../app/models/cronjob.js)

Cronjobs support clustering mode as well as multiple instances.
Job locking is done using mongodb.

## Find Cron jobs
Find Cron jobs.

* **URL**

/api/v0/cron

* **Method**

`GET`

## New Cron jobs

* **URL**

/api/v0/cron

* **Method**

`POST`

## View Cron jobs

* **URL**

/api/v0/cron/:Job

* **Method**

`GET`

## Example

```
POST /api/v0/cron
payload:
{
name: "test",
type: "view,
view: {
col: "results",
view: "myview",
aggregate: '[{"$projet": "name"}, {"$limit": 10}]'
},
cron: {
enabled: true,
cron: "* * * * * *"
}
}
```

This would generate collection `cronjobs.myview` once per
second using aggregate from `results` -collection:
```
[{"$projet": "name", "_id": 0}, {"$limit": 3}]'
```

To get this view use:
```
GET /api/v0/cron/:id/view

result:
[
{name": xxx},
{name": xxx},
{name": xxx},
]
```
5 changes: 5 additions & 0 deletions doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* Builds
* Test Results
* Events
* CronJobs
* Reports
* Report template's
* Addons
Expand Down Expand Up @@ -66,6 +67,10 @@ Test Result contains all information related Test Execution phase, like final ve
### Events
[This API](APIs/events.md) contains system related events.

### CronJobs
[This API](APIs/cronjobs.md) contains cron jobs.


### Reports (not yet implemented)
Report is like snapshot of database state in certain time period. Reports cannot be change afterward. Reports can be use any (some exceptions) other API's to fetch actual data to report. For example we could create Test Report from Test Results, or Test Case Report from Test Cases.

Expand Down
Loading