skip to Main Content

Motivation

I want to ensure there is no brute (sub-)process termination and that any open requests are fulfilled when the Express server is closing.

import express, { Express, Request, Response } from 'express';

// Initialize the Express server application
var app = express();
var server = this.app.listen(process.env.PORT || 5000, () => { ... } );

// POST endpoint to process the shopping cart
app.post('/purchase', async (req, res) => {
  // Get the items from the request body

  try {
      const { buyer, items } = req.body;
      
      // Process the items (e.g., calculate total price, initiate payment, etc.)
      const totalPrice = calculateTotalPrice(items);
      // Charge the buyer for the purchase
      const payment = chargeBuyerForPurchase(totalPrice);
      
      // 🥷 I don't want the server to close before it executes this line for every request

      // Create shipping for the purchased items
      const shipping = carrierAPI.createShipping(items, buyer.address);

      res.status(200).json([totalPrice, payment, shipping]);

      // 💁‍♂️ This is now safe to close the server
    } catch (error) {
      next(error);
    }

});

Proposed Solution

See documentation at this link

// ./src/app.ts
import process from 'node:process';

// Begin reading from stdin so the process does not exit.
process.stdin.resume();

process.on('SIGINT', () => {
  console.log('Received SIGINT. Press Control-D to exit.');
});

// FYI docker "stop" <container>, a process manager, and most hosts will send SIGTERM signal when it is shutting down.
// server.close stops the server from accepting new connections and closes all connections connected to this server that are not sending a request or waiting for a response

function gracefulClose(signal) {
  console.log(`Received ${signal}`);
  server.close( () => { log('HTTP(S) server closed') } );

}

process.on('SIGINT', gracefulClose);
process.on('SIGTERM', gracefulClose);

Is it a feature to implement? Is this feature / code redundant?

2

Answers


  1. Chosen as BEST ANSWER

    THESE ARE MY FINDINGS:

    1. When the SIGTERM signal is emitted, the Node.JS server will be immediately closed. The Node.Js application / process will still be alive. Moreover, a response will not be sent to any client even if its connection was open before the SIGTERM signal was received.

    2. When the SIGINT signal is emitted, the Node.JS (Express) server and application or process will be immediately closed.

    3. When a custom signal is emitted, SIGUSR1 or SIGUSR2, the Node.JS server will immediately close any open connections. The Node.Js application / process will still be alive. The Node.Js (Express) server will still be alive too and fulfill only new requests and responses.

    PS: Signals are software interrupts sent to a program to indicate that an important event has occurred. Another common method for delivering signals is to use the kill command, the syntax of which is as follows −

    $ kill -signal <pid>
    

    MY SOLUTION:

    I have created an npm package to super-gracefully-shutdown your Node.Js Express server. It ensures a response is sent to every client for any connections that were open before you send a super-graceful `shutdown` message.


    Usage / Implementation

    import express from 'express';
    import SGS from 'super-graceful-shutdown';
    
    const app = express();
    const router = express.Router();
    
    const express = require('express');
    const SGS = require('super-graceful-shutdown');
    
    const app = express();
    const router = express.Router();
    const port = 80; // Set your desired port number
    
    // ...
    
    router.get('/api', async function(req, res, next){
      try {
          // Fulfill this request in 3 seconds
          setTimeout(() => { res.status(200).send(`<html> <body> <h1>${date()}</h1> </body> </html>`)  }, 3000);
        } catch (error) {
          next(error);
        }
    });
    
    const server = app.listen(port, () => console.log(`Example Express app listening on port ${port}!`) );
    
    // ℹ️ Before you initialize other routes, you need to initialize super-graceful-shutdown
    new SGS(app, server);
    
    // 👨‍💻 Then, you can initialize other routes
    app.use('/', router);
    

    Request From Host To Shutdown Node.Js Express App

    To super-gracefully-shutdown your Node.Js Express application, send the TCP shutdown message on port 3000:

    echo "shutdown" | nc localhost 3000
    

  2. Handle SIGTERM to permit Docker to cause a graceful exit

    With respect to Docker, yes you have to handle the SIGTERM event and gracefully exit. If you do not, then after having signalled your container to exit, Docker will wait for some period of time (10 sec, I believe) for your container to exit, and if it does not do so in time, Docker will forcibly kill your container.

    In general, this would be sufficient:

    process.on('SIGTERM', process.exit);
    process.on('SIGINT', process.exit);
    

    You can’t handle SIGTERM events during synchronous code anyway

    In your example code, your post handler is synchronous. JavaScript is single threaded and cannot run your gracefulClose handler (in response to the SIGTERM event) until your synchronous function has completed. So your code is unnecessary in this case. If the post handler function were asynchronous, then you’d have to a different situation.

    Deferring a graceful exit during an asynchronous operation

    If you were to modify your post handler to be asynchronous, such that you could have a reason not to exit and possibly have the SIGTERM handler run before your reason not to exit is resolved, then yes, you’d have to implement something special to handle that.

    Such an implementation might go like this:

    Any time you create a situation that constitutes a reason to prevent forcibly closing, push an object representing that reason into an array. When the situation no longer exists, remove the reason from the array.

    When you get a SIGTERM signal, set a flag saying that you want to exit.

    Then write a function that checks to see if it’s ok to exit. The condition will be that you have set the exit flag, and the reasons-not-to-exit array is empty. If it’s ok to exit, then you may call process.exit.

    Call this function after setting the exit flag, and also call it after removing a reason from the array.

    const reasonsNotToExit = [];
    let isExitRequested = false;
    
    function requestGracefulExit() {
      isExitRequested = true;
      exitIfRequired();
    }
    
    function exitIfRequired() {
      if (isExitRequested && resonsNotToExit.length == 0) {
        process.exit();
      }
    }
    
    process.on('SIGINT', requestGracefulExit);
    process.on('SIGTERM', requestGracefulExit);
    

    Example deferral: (for example, in the body of your post handler.)

       // Add a reason not to exit
       let reason = {};
       reasonsNotToExit.push(reason);
    
       // Imagine some async thing that should cause graceful exits to be deferred:
       // note use of `await`
       const shipping = await carrierAPI.createShipping(items, buyer.address);
       // ... and now imagine that the reason not to exit has been resolved.
    
       // Remove the reason from the array.
       reasonsNotToExit.splice(reasonsNotToExit.indexOf(reason), 1);
       // We are obligated to call this when we remove a reason:
       exitIfRequired();
    

    Note that this example has explicitly awaited some hypothetical async operation using await, which is missing from your original example, in which this code was not necessary.

    Note that even though you’ve coded up something to handle a graceful exit, after politely asking your container to exit, Docker essentially starts a doomsday timer, and it may forcibly terminate your program after 10 seconds anyway, whether you’ve finished cleaning up or not. Also docker kill will ungracefully terminate your program, even if this code is in place.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search