Egg: 通过app.beforeClose方法加入的钩子函数在关闭应用时不会执行

Created on 31 Jul 2017  ·  19Comments  ·  Source: eggjs/egg


如题,打日志发现函数确实注册成功了,但是SIGTERM和SIGINT信号关闭应用时都不执行。目前没有发现能够正常执行钩子的关闭方法。

egg-cluster

Most helpful comment

-9是异常退出场景,正常的退出行为都是SIGINT或者SIGTERM退出,node语言层面也提供了支持异步的进程退出钩子,个人认为框架层应该提供优雅关闭的方法。

All 19 comments

关闭的钩子用 app.beforeClose 不要手动监听信号量,在 egg-cluster 里面已经拦截了。
之前看错题目。

并没有手动监听信号量,就是用app.beforeClose注册的钩子

是不会执行,在单元测试用的,一般进程退出不需要做什么。

我们在做eureka的集成,目前这个组件需要在程序退出的时候挂钩子做优雅关闭的,不排除有其他类型插件同样需要优雅关闭。

最好不要这样做,如果 kill -9 的话是不会执行的

-9是异常退出场景,正常的退出行为都是SIGINT或者SIGTERM退出,node语言层面也提供了支持异步的进程退出钩子,个人认为框架层应该提供优雅关闭的方法。

我试过kill是可以正确触发退出钩子
平时开发在Terminal里面只有Ctrl + \ (SIGQUIT) 能正常退出
Ctrl + c都无法正常退出

我加一个

beforeClose 不会执行的话, https://github.com/eggjs/egg/blob/master/lib/egg.js#L103 这里是不是会有问题?

cc @popomore @gxcsoccer

因为我之前也受这个问题困扰,所以当时看了一下源码,有一些分析整理了一下

触发事件

https://github.com/eggjs/egg-cluster/blob/6cdd2c1280516a1156f397a5050c889f4410bdc9/lib/master.js#L104

  constructor(options) {
    // ...
    // https://nodejs.org/api/process.html#process_signal_events
    // https://en.wikipedia.org/wiki/Unix_signal
    // kill(2) Ctrl-C
    process.once('SIGINT', this.onSignal.bind(this, 'SIGINT'));
    // kill(3) Ctrl-\
    process.once('SIGQUIT', this.onSignal.bind(this, 'SIGQUIT'));
    // kill(15) default
    process.once('SIGTERM', this.onSignal.bind(this, 'SIGTERM'));
    // ...
  }

  // ...
  /**
   * close agent worker, App Worker will closed by cluster
   *
   * https://www.exratione.com/2013/05/die-child-process-die/
   * make sure Agent Worker exit before master exit
   */
  killAgentWorker() {
    if (this.agentWorker) {
      this.log('[master] kill agent worker with signal SIGTERM');
      this.agentWorker.removeAllListeners();
      this.agentWorker.kill('SIGTERM');
    }
  }

  killAppWorkers() {
    for (const id in cluster.workers) {
      const worker = cluster.workers[id];
      worker.disableRefork = true;
      worker.process.kill('SIGTERM');
    }
  }
  // ...
  onSignal(signal) {
    if (this.closed) return;

    this.logger.info('[master] receive signal %s, closing', signal);
    this.close();
  }
  // ...

  close() {
    this.closed = true;
    // kill app workers
    // kill agent worker
    // exit itself
    this.killAppWorkers();
    this.killAgentWorker();
    // sleep 100ms to make sure SIGTERM send to the child processes
    this.log('[master] send kill SIGTERM to app workers and agent worker, will exit with code:0 after 100ms');
    setTimeout(() => {
      this.log('[master] close done, exiting with code:0');
      process.exit(0);
    }, 100);
  }

从egg-cluster中能我看到 master 监听了SIGINT SIGQUIT SIGTERM 三个事件
触发事件后,master 会向 agent 和 app 发 SIGTERM

beforeClose

beforeClose注册在 https://github.com/eggjs/egg-core/blob/b36c6854fdbd8d1cebe4c0a1da5f02d78476a0a4/lib/egg.js#L67

226~263行

  /**
   * Close all, it will close
   * - callbacks registered by beforeClose
   * - emit `close` event
   * - remove add listeners
   *
   * If error is thrown when it's closing, the promise will reject.
   * It will also reject after following call.
   * @return {Promise} promise
   * @since 1.0.0
   */
  close() {
    if (this[CLOSE_PROMISE]) return this[CLOSE_PROMISE];

    this[CLOSE_PROMISE] = co(function* closeFunction() {
      // close in reverse order: first created, last closed
      const closeFns = Array.from(this[CLOSESET]);
      for (const fn of closeFns.reverse()) {
        yield utils.callFn(fn);
        this[CLOSESET].delete(fn);
      }

      // Be called after other close callbacks
      this.emit('close');
      this.removeAllListeners();
      this[ISCLOSE] = true;
    }.bind(this));
    return this[CLOSE_PROMISE];
  }

  /**
   * Register a function that will be called when app close
   * @param {Function} fn - the function that can be generator function or async function
   */
  beforeClose(fn) {
    assert(is.function(fn), 'argument should be function');
    this[CLOSESET].add(fn);
  }

可以看到close方法返回的就是触发这些callback的promise
我没有找到任何触发这个close方法的地方

根据上面的代码,app 和 agent 应该要监听 SIGTERM,并且把close的promise执行完,但我没有在任何地方看到监听的代码,那么所有的包括默认注册的beforeClose都不会执行
我现在的解决方案是在app和agnet中直接监听SIGTERM

值得注意的是,用kill发信号的时候会正常工作
而在控制台中用Ctrl+cCtrl+\ 这种快捷键的时候,master 和 worker 都能收到对应的信号,这样就会触发两次事件

补个示例,可以向 master 进程发送信号,和快捷键发送信号
看close.log

看了下不太好实现,先搁置。

我觉得这个问题导致注册beforeClose没有意义了,希望尽快修复一下。
每个worker执行完beforeClose后发信号给master,master确保每个worker都已经退出再关闭

这个 api 的目的是给单元测试用的,不是没有意义。

原来是不支持阿, 折腾了半天准备提 issue 了才搜索到这个。
https://eggjs.org/zh-cn/basics/app-start.html 文档应该注明下,或者干脆删除掉关于 beforeClose 的。

备注下使用 docker 时无法触发的情景

情况: 使用 docker 时,entrypoint: 是 npm start 或 npm run dev
docker stop 时 app.js 中 beforeClose 无法被正确触发,因为收到 SIGTERM 信号的进程不是 egg-scripts;如果直接 kill egg-scripts,能正常触发 beforeClose。

解决方案:
不要把 npm start 作为 entrypoint,eggjs 改成
entrypoint: node node_modules/.bin/egg-scripts start --title=title

就能在 docker stop 时正常触发 beforeClose

是不是你 npm start 里面有 --daemon

没有 daemon,egg 应用在线上跑了很多年了,加 --daemon 都无法正常启动 。
搜到类似这种文章明白了原理:https://juejin.im/post/5d9c5d3451882541391a2b7f

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lvgg3271 picture lvgg3271  ·  3Comments

Quekie picture Quekie  ·  3Comments

dizhifeng picture dizhifeng  ·  3Comments

skyyangpeng picture skyyangpeng  ·  3Comments

Leungkingman picture Leungkingman  ·  3Comments