Skip to content

fix: app stopped before ready is restarted - #6170

Open
aletorrado wants to merge 1 commit into
Unitech:developmentfrom
aletorrado:fix/exit-before-ready-status
Open

aletorrado wants to merge 1 commit into
Unitech:developmentfrom
aletorrado:fix/exit-before-ready-status

Conversation

@aletorrado

Copy link
Copy Markdown

Hi! I work in production with workers in cluster_mode + wait_ready + kill_timeout + listen_timeout. I've been noticing that sometimes, after just after a cold start (while booting a server), some workers randomly were marked as online but with memory = 0.

I've debugged this issue with the help of Claude, and came to the conclusion that there was a bug in PM2 which got fixed by commit c87dd75. Nevertheless, Claude found another bug introduced in that same commit in which callbacks are clashing. I'm leaving the full description co-authored with Claude, including reproduction code below for your reference.

Problem

Since c87dd75 ("process flow refactor"), when a process exits before it is ready, both exit handlers in God.executeApp (cluster and fork) call readyCb() to answer the caller. readyCb() also sets status = online (and logs/notifies online) for a process that has just exited.

When the exit was caused by pm2 stop, this overwrites the stopping status. handleExit() then no longer sees a stop, treats the exit as a crash, and restarts the app.

Reproduction (7.0.4 and development):

// app.js: never sends 'ready'
setInterval(() => {}, 1000);
// ecosystem.config.js
module.exports = { apps: [{ name: 'repro', script: 'app.js', exec_mode: 'cluster',
  wait_ready: true, listen_timeout: 5000 }] };
pm2 start ecosystem.config.js & sleep 2; pm2 stop repro; sleep 5; pm2 ls

Expected: stopped. Actual: the app was restarted (↺ 1) and is online. The log shows Stopping app, exited with code [0], online, starting in -cluster mode-.

Fix

Add exitedCb(), which concludes the caller (keeping the no-hang guarantee from c87dd75) without marking the process online. Both exit handlers use it, so handleExit() sees the real status.

The cb_called guard still makes the pending ready timer / process:msg listener a no-op once the process has exited.

Context

On 6.x the same path showed up differently: the ready timer was not cleared on exit, so listen_timeout later marked an exited process online with pid 0 / memory 0 (e.g. after pm2 stop during boot, or during a restart_delay wait). c87dd75 fixed that part; this PR fixes the restart regression it introduced.

Tests

  • New test/programmatic/stop_before_ready.mocha.js (cluster and fork mode), registered in test/unit.sh. It fails without the fix (expected 'online' to equal 'stopped') and passes with it.
  • Still passing: cluster_exit_before_online, issue_6142_stale_exit_event, issue_6129_reload_collision, graceful, cluster, god, auto_restart, exp_backoff_restart_delay.

Since the exit-before-online change, an app that exits before being ready
concludes executeApp through readyCb(), which sets its status to online.
When the exit comes from pm2 stop, this overwrites the 'stopping' status,
so handleExit() takes it as a crash and restarts the app.

Conclude the caller without marking the app online, so handleExit() sees
the real status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CLAassistant

CLAassistant commented Oct 9, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants