lifecycle.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. 'use strict';
  2. const is = require('is-type-of');
  3. const assert = require('assert');
  4. const getReady = require('get-ready');
  5. const { Ready } = require('ready-callback');
  6. const { EventEmitter } = require('events');
  7. const debug = require('debug')('egg-core:lifecycle');
  8. const INIT = Symbol('Lifycycle#init');
  9. const INIT_READY = Symbol('Lifecycle#initReady');
  10. const DELEGATE_READY_EVENT = Symbol('Lifecycle#delegateReadyEvent');
  11. const REGISTER_READY_CALLBACK = Symbol('Lifecycle#registerReadyCallback');
  12. const CLOSE_SET = Symbol('Lifecycle#closeSet');
  13. const IS_CLOSED = Symbol('Lifecycle#isClosed');
  14. const BOOT_HOOKS = Symbol('Lifecycle#bootHooks');
  15. const BOOTS = Symbol('Lifecycle#boots');
  16. const utils = require('./utils');
  17. class Lifecycle extends EventEmitter {
  18. /**
  19. * @param {object} options - options
  20. * @param {String} options.baseDir - the directory of application
  21. * @param {EggCore} options.app - Application instance
  22. * @param {Logger} options.logger - logger
  23. */
  24. constructor(options) {
  25. super();
  26. this.options = options;
  27. this[BOOT_HOOKS] = [];
  28. this[BOOTS] = [];
  29. this[CLOSE_SET] = new Set();
  30. this[IS_CLOSED] = false;
  31. this[INIT] = false;
  32. getReady.mixin(this);
  33. this.timing.start('Application Start');
  34. // get app timeout from env or use default timeout 10 second
  35. const eggReadyTimeoutEnv = Number.parseInt(process.env.EGG_READY_TIMEOUT_ENV || 10000);
  36. assert(
  37. Number.isInteger(eggReadyTimeoutEnv),
  38. `process.env.EGG_READY_TIMEOUT_ENV ${process.env.EGG_READY_TIMEOUT_ENV} should be able to parseInt.`);
  39. this.readyTimeout = eggReadyTimeoutEnv;
  40. this[INIT_READY]();
  41. this
  42. .on('ready_stat', data => {
  43. this.logger.info('[egg:core:ready_stat] end ready task %s, remain %j', data.id, data.remain);
  44. })
  45. .on('ready_timeout', id => {
  46. this.logger.warn('[egg:core:ready_timeout] %s seconds later %s was still unable to finish.', this.readyTimeout / 1000, id);
  47. });
  48. this.ready(err => {
  49. this.triggerDidReady(err);
  50. this.timing.end('Application Start');
  51. });
  52. }
  53. get app() {
  54. return this.options.app;
  55. }
  56. get logger() {
  57. return this.options.logger;
  58. }
  59. get timing() {
  60. return this.app.timing;
  61. }
  62. legacyReadyCallback(name, opt) {
  63. return this.loadReady.readyCallback(name, opt);
  64. }
  65. addBootHook(hook) {
  66. assert(this[INIT] === false, 'do not add hook when lifecycle has been initialized');
  67. this[BOOT_HOOKS].push(hook);
  68. }
  69. addFunctionAsBootHook(hook) {
  70. assert(this[INIT] === false, 'do not add hook when lifecycle has been initialized');
  71. // app.js is exported as a function
  72. // call this function in configDidLoad
  73. this[BOOT_HOOKS].push(class Hook {
  74. constructor(app) {
  75. this.app = app;
  76. }
  77. configDidLoad() {
  78. hook(this.app);
  79. }
  80. });
  81. }
  82. /**
  83. * init boots and trigger config did config
  84. */
  85. init() {
  86. assert(this[INIT] === false, 'lifecycle have been init');
  87. this[INIT] = true;
  88. this[BOOTS] = this[BOOT_HOOKS].map(t => new t(this.app));
  89. }
  90. registerBeforeStart(scope) {
  91. this[REGISTER_READY_CALLBACK]({
  92. scope,
  93. ready: this.loadReady,
  94. timingKeyPrefix: 'Before Start',
  95. });
  96. }
  97. registerBeforeClose(fn) {
  98. assert(is.function(fn), 'argument should be function');
  99. assert(this[IS_CLOSED] === false, 'app has been closed');
  100. this[CLOSE_SET].add(fn);
  101. }
  102. async close() {
  103. // close in reverse order: first created, last closed
  104. const closeFns = Array.from(this[CLOSE_SET]);
  105. for (const fn of closeFns.reverse()) {
  106. await utils.callFn(fn);
  107. this[CLOSE_SET].delete(fn);
  108. }
  109. // Be called after other close callbacks
  110. this.app.emit('close');
  111. this.removeAllListeners();
  112. this.app.removeAllListeners();
  113. this[IS_CLOSED] = true;
  114. }
  115. triggerConfigWillLoad() {
  116. for (const boot of this[BOOTS]) {
  117. if (boot.configWillLoad) {
  118. boot.configWillLoad();
  119. }
  120. }
  121. this.triggerConfigDidLoad();
  122. }
  123. triggerConfigDidLoad() {
  124. for (const boot of this[BOOTS]) {
  125. if (boot.configDidLoad) {
  126. boot.configDidLoad();
  127. }
  128. // function boot hook register after configDidLoad trigger
  129. const beforeClose = boot.beforeClose && boot.beforeClose.bind(boot);
  130. if (beforeClose) {
  131. this.registerBeforeClose(beforeClose);
  132. }
  133. }
  134. this.triggerDidLoad();
  135. }
  136. triggerDidLoad() {
  137. debug('register didLoad');
  138. for (const boot of this[BOOTS]) {
  139. const didLoad = boot.didLoad && boot.didLoad.bind(boot);
  140. if (didLoad) {
  141. this[REGISTER_READY_CALLBACK]({
  142. scope: didLoad,
  143. ready: this.loadReady,
  144. timingKeyPrefix: 'Did Load',
  145. scopeFullName: boot.fullPath + ':didLoad',
  146. });
  147. }
  148. }
  149. }
  150. triggerWillReady() {
  151. debug('register willReady');
  152. this.bootReady.start();
  153. for (const boot of this[BOOTS]) {
  154. const willReady = boot.willReady && boot.willReady.bind(boot);
  155. if (willReady) {
  156. this[REGISTER_READY_CALLBACK]({
  157. scope: willReady,
  158. ready: this.bootReady,
  159. timingKeyPrefix: 'Will Ready',
  160. scopeFullName: boot.fullPath + ':willReady',
  161. });
  162. }
  163. }
  164. }
  165. triggerDidReady(err) {
  166. debug('trigger didReady');
  167. (async () => {
  168. for (const boot of this[BOOTS]) {
  169. if (boot.didReady) {
  170. try {
  171. await boot.didReady(err);
  172. } catch (e) {
  173. this.emit('error', e);
  174. }
  175. }
  176. }
  177. debug('trigger didReady done');
  178. })();
  179. }
  180. triggerServerDidReady() {
  181. (async () => {
  182. for (const boot of this[BOOTS]) {
  183. try {
  184. await utils.callFn(boot.serverDidReady, null, boot);
  185. } catch (e) {
  186. this.emit('error', e);
  187. }
  188. }
  189. })();
  190. }
  191. [INIT_READY]() {
  192. this.loadReady = new Ready({ timeout: this.readyTimeout });
  193. this[DELEGATE_READY_EVENT](this.loadReady);
  194. this.loadReady.ready(err => {
  195. debug('didLoad done');
  196. if (err) {
  197. this.ready(err);
  198. } else {
  199. this.triggerWillReady();
  200. }
  201. });
  202. this.bootReady = new Ready({ timeout: this.readyTimeout, lazyStart: true });
  203. this[DELEGATE_READY_EVENT](this.bootReady);
  204. this.bootReady.ready(err => {
  205. this.ready(err || true);
  206. });
  207. }
  208. [DELEGATE_READY_EVENT](ready) {
  209. ready.once('error', err => ready.ready(err));
  210. ready.on('ready_timeout', id => this.emit('ready_timeout', id));
  211. ready.on('ready_stat', data => this.emit('ready_stat', data));
  212. ready.on('error', err => this.emit('error', err));
  213. }
  214. [REGISTER_READY_CALLBACK]({ scope, ready, timingKeyPrefix, scopeFullName }) {
  215. if (!is.function(scope)) {
  216. throw new Error('boot only support function');
  217. }
  218. // get filename from stack if scopeFullName is undefined
  219. const name = scopeFullName || utils.getCalleeFromStack(true, 4);
  220. const timingkey = `${timingKeyPrefix} in ` + utils.getResolvedFilename(name, this.app.baseDir);
  221. this.timing.start(timingkey);
  222. const done = ready.readyCallback(name);
  223. // ensure scope executes after load completed
  224. process.nextTick(() => {
  225. utils.callFn(scope).then(() => {
  226. done();
  227. this.timing.end(timingkey);
  228. }, err => {
  229. done(err);
  230. this.timing.end(timingkey);
  231. });
  232. });
  233. }
  234. }
  235. module.exports = Lifecycle;