Skip to content

Commit

Permalink
feat: implement checker
Browse files Browse the repository at this point in the history
implement checker loader, load `app/checker`.
Checkers will be called after all readyCallbacks called,
then emit `app.ready(true)`.

Refs: eggjs/egg#2520
  • Loading branch information
零弌 committed May 16, 2018
1 parent a43dcb9 commit 4de9ecb
Show file tree
Hide file tree
Showing 19 changed files with 276 additions and 8 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ Directory structure
| ├── router.js
│ ├── controller
│ │ └── home.js
│ ├── checker (optional)
│ │ └── health_checker.js (optional)
| ├── extend (optional)
│ | ├── helper.js (optional)
│ | ├── filter.js (optional)
Expand Down Expand Up @@ -131,6 +133,10 @@ Load agent.js

Load app/service

#### loadChecker

Load app/checker

### Low Level APIs

#### getServerEnv()
Expand Down
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
const EggCore = require('./lib/egg');
const EggLoader = require('./lib/loader/egg_loader');
const BaseContextClass = require('./lib/utils/base_context_class');
const BaseCheckerClass = require('./lib/utils/base_checker_class');
const utils = require('./lib/utils');

module.exports = {
EggCore,
EggLoader,
BaseContextClass,
BaseCheckerClass,
utils,
};
66 changes: 65 additions & 1 deletion lib/egg.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const debug = require('debug')('egg-core');
const is = require('is-type-of');
const co = require('co');
const BaseContextClass = require('./utils/base_context_class');
const BaseCheckerClass = require('./utils/base_checker_class');
const utils = require('./utils');
const Router = require('./utils/router');

Expand All @@ -20,6 +21,8 @@ const ROUTER = Symbol('EggCore#router');
const EGG_LOADER = Symbol.for('egg#loader');
const INIT_READY = Symbol('EggCore#initReady');
const EGG_READY_TIMEOUT_ENV = Symbol('EggCore#eggReadyTimeoutEnv');
const INJECT_READY = Symbol('EggCore#injectReady');
const CALL_CHECKERS = Symbol('EggCore#callCheckers');

class EggCore extends KoaApplication {

Expand Down Expand Up @@ -76,6 +79,23 @@ class EggCore extends KoaApplication {
*/
this.BaseContextClass = BaseContextClass;

/**
* @member {BaseCheckerClass} EggCore#BaseCheckerClass
* @since 4.8.0
*/
this.BaseCheckerClass = BaseCheckerClass;

const Checker = this.BaseCheckerClass;

/**
* Base checker to be extended by checker in `app.checker`
* @class Checker
* @extends BaseCheckerClass
* @example
* class HealthChecker extends app.Checker {}
*/
this.Checker = Checker;

/**
* Base controller to be extended by controller in `app.controller`
* @class Controller
Expand Down Expand Up @@ -294,7 +314,7 @@ class EggCore extends KoaApplication {
* const done = app.readyCallback('mysql');
* mysql.ready(done);
*/
require('ready-callback')({ timeout: this[EGG_READY_TIMEOUT_ENV] }).mixin(this);
this[INJECT_READY]();

this.on('ready_stat', data => {
this.console.info('[egg:core:ready_stat] end ready task %s, remain %j', data.id, data.remain);
Expand Down Expand Up @@ -364,6 +384,50 @@ class EggCore extends KoaApplication {
return yield obj;
});
}

/**
* inject READY_HOOK_CALLBACK to ready.cache
* if cache have only READY_HOOK_CALLBACK,
* call checkers, and emit `ready('true')`
* after checkers all done.
* @private
*/
[INJECT_READY]() {
const readyDelegate = require('ready-callback')({ timeout: this[EGG_READY_TIMEOUT_ENV] }).mixin(this);
// inject READY_HOOK_CALLBACK to callback cache
const readyHookCb = this.readyCallback('READY_HOOK_CALLBACK');

const readyStatListener = () => {
const cache = readyDelegate.cache;
// if ready callback cache have more than one callbacks, should wait
if (cache.size > 1) return;
// remove readyStatListener to prevent checker called again
this.removeListener('ready_stat', readyStatListener);
// call checkers, then emit ready('true')
this[CALL_CHECKERS]().then(() => readyHookCb());
};
this.on('ready_stat', readyStatListener);

// fire callback directly when no registered ready callback
setImmediate(readyStatListener);
}


/**
* call all checkers
* @private
* @return {Promise} promise
*/
[CALL_CHECKERS]() {
return Promise.all(Object.keys(this.checker || {})
.map(name => {
const checker = this.checker[name];
if (is.function(checker)) {
return utils.callFn(checker, []);
}
return utils.callFn(checker.check, [], checker);
}));
}
}

// delegate all router method to application
Expand Down
1 change: 1 addition & 0 deletions lib/loader/egg_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ const loaders = [
require('./mixin/middleware'),
require('./mixin/controller'),
require('./mixin/router'),
require('./mixin/checker'),
];

for (const loader of loaders) {
Expand Down
35 changes: 35 additions & 0 deletions lib/loader/mixin/checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
'use strict';

const path = require('path');
const is = require('is-type-of');
const assert = require('assert');

module.exports = {
loadChecker(opt) {
opt = Object.assign({
caseStyle: 'lower',
directory: path.join(this.options.baseDir, 'app/checker'),
initializer: (obj, opt) => {
const { pathName, path } = opt;
assert(is.function(obj) || is.class(obj), `${path} checker should be function or class`);
if (is.function(obj) && !is.generatorFunction(obj) && !is.class(obj) && !is.asyncFunction(obj)) {
obj = obj(this.app);
}
if (is.class(obj)) {
obj.prototype.pathName = pathName;
obj.prototype.fullPath = path;
assert(is.function(obj.prototype.check), `${path} checker should have check method`);
return new obj(this.app);
}
if (is.function(obj)) {
return () => obj;
}
assert.fail(`${path} checker is invalidate`);
},
}, opt);
const checkerBase = opt.directory;

this.loadToApp(checkerBase, 'checker', opt);
this.options.logger.info('[egg:loader] Checker loaded: %s', checkerBase);
},
};
28 changes: 28 additions & 0 deletions lib/utils/base_checker_class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

const assert = require('assert');

class BaseCheckerClass {
/**
* @constructor
* @param {Application} app - egg application
* @since 4.8.0
*/
constructor(app) {
/**
* @member {Application} BaseContextClass#app
* @since 4.8.0
*/
this.app = app;
}

/**
* check app status before app ready callbacks called
* @since 4.8.0
*/
check() {
assert.fail('should implement in subclass');
}
}

module.exports = BaseCheckerClass;
18 changes: 11 additions & 7 deletions test/egg.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const request = require('supertest');
const coffee = require('coffee');
const utils = require('./utils');
const EggCore = require('..').EggCore;
const pedding = require('pedding');

describe('test/egg.test.js', () => {
afterEach(mm.restore);
Expand Down Expand Up @@ -143,12 +144,13 @@ describe('test/egg.test.js', () => {
afterEach(() => app.close());

it('should log info when plugin is not ready', done => {
done = pedding(2, done);
app = utils.createApp('notready');
app.loader.loadAll();
mm(app.console, 'warn', (message, b, a) => {
assert(message === '[egg:core:ready_timeout] %s seconds later %s was still unable to finish.');
assert(b === 10);
assert(a === 'a');
assert(a === 'a' || a === 'READY_HOOK_CALLBACK');
done();
});
app.ready(() => {
Expand All @@ -164,8 +166,8 @@ describe('test/egg.test.js', () => {
message += util.format.apply(null, [ a, b, c ]);
});
app.ready(() => {
assert(/\[egg:core:ready_stat] end ready task a, remain \["b"]/.test(message));
assert(/\[egg:core:ready_stat] end ready task b, remain \[]/.test(message));
assert(/\[egg:core:ready_stat] end ready task a, remain \["READY_HOOK_CALLBACK","b"]/.test(message));
assert(/\[egg:core:ready_stat] end ready task b, remain \["READY_HOOK_CALLBACK"]/.test(message));
done();
});
});
Expand Down Expand Up @@ -209,12 +211,13 @@ describe('test/egg.test.js', () => {
});

it('should beforeStart excute timeout without EGG_READY_TIMEOUT_ENV too short', function(done) {
done = pedding(2, done);
mm(process.env, 'EGG_READY_TIMEOUT_ENV', '1000');
app = utils.createApp('beforestart-with-timeout-env');
app.loader.loadAll();
app.once('ready_timeout', id => {
app.on('ready_timeout', id => {
const file = path.normalize('test/fixtures/beforestart-with-timeout-env/app.js');
assert(id.includes(file));
assert(id === 'READY_HOOK_CALLBACK' || id.includes(file));
done();
});
});
Expand All @@ -240,11 +243,12 @@ describe('test/egg.test.js', () => {
});

it('should beforeStart excute timeout', done => {
done = pedding(2, done);
app = utils.createApp('beforestart-timeout');
app.loader.loadAll();
app.once('ready_timeout', id => {
app.on('ready_timeout', id => {
const file = path.normalize('test/fixtures/beforestart-timeout/app.js');
assert(id.includes(file));
assert(id === 'READY_HOOK_CALLBACK' || id.includes(file));
done();
});
});
Expand Down
14 changes: 14 additions & 0 deletions test/fixtures/checker-app/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

const sleep = require('mz-modules/sleep');

module.exports = function(app) {
app.readyQueue = [];
app.beforeStart(async () => {
await sleep(20);
app.readyQueue.push('beforeStart');
});
app.ready(() => {
app.readyQueue.push('ready');
});
};
10 changes: 10 additions & 0 deletions test/fixtures/checker-app/app/checker/async_function_checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const assert = require('assert');

module.exports = function(app) {
return async function() {
assert.strictEqual(app.name, 'checker-app');
app.readyQueue.push('async_function_checker');
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

const assert = require('assert');

module.exports = app => {
return class AsyncChecker extends app.Checker {
async check() {
assert.strictEqual(this.app.name, 'checker-app');
this.app.readyQueue.push('class_async_function_checker');
}
}
};
12 changes: 12 additions & 0 deletions test/fixtures/checker-app/app/checker/class_function_checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

const assert = require('assert');

module.exports = app => {
return class FuncChecker extends app.Checker {
check() {
assert.strictEqual(this.app.name, 'checker-app');
this.app.readyQueue.push('class_function_checker');
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';

const assert = require('assert');

module.exports = app => {
return class GeneratorChecker extends app.Checker {
* check() {
assert.strictEqual(this.app.name, 'checker-app');
this.app.readyQueue.push('class_generator_function_checker');
}
}
};
10 changes: 10 additions & 0 deletions test/fixtures/checker-app/app/checker/function_checker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const assert = require('assert');

module.exports = function(app) {
return function() {
assert.strictEqual(app.name, 'checker-app');
app.readyQueue.push('function_checker');
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

const assert = require('assert');

module.exports = function(app) {
return function* () {
assert.strictEqual(app.name, 'checker-app');
app.readyQueue.push('generator_function_checker');
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

const assert = require('assert');

module.exports = class AsyncChecker {
constructor(app) {
this.app = app;
}

async check() {
assert.strictEqual(this.app.name, 'checker-app');
this.app.readyQueue.push('module_class_function_checker');
}
};
4 changes: 4 additions & 0 deletions test/fixtures/checker-app/app/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';

module.exports = app => {
};
3 changes: 3 additions & 0 deletions test/fixtures/checker-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"name": "checker-app"
}
1 change: 1 addition & 0 deletions test/fixtures/egg/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class AppLoader extends EggLoader {
this.loadService();
this.loadController();
this.loadRouter();
this.loadChecker();
}
}

Expand Down
Loading

0 comments on commit 4de9ecb

Please sign in to comment.