Giter VIP home page Giter VIP logo

graceful-server's Introduction

๐Ÿš€ Graceful Server ๐Ÿข

GitHub package.json version JavaScript Style Guide npm type definitions License npmjs download Donate with PayPal

Tiny (~5k), KISS, dependency-free Node.JS library to make your Rest API graceful.


Features

โœ” It's listening system events to gracefully close your API on interruption.

โœ” It facilitates the disconnect of data sources on shutdown.

โœ” It facilitates the use of liveness and readiness.

โœ” It manages the connections of your API.

โœ” It avoid boilerplate codes.

โœ” Kubernetes compliant.

โœ” Dependency-free.

โœ” KISS code base.

Requirements

โœ” NodeJS >= 14.0

Installation

NPM

npm install --save @gquittet/graceful-server

Yarn

yarn add @gquittet/graceful-server

PNPM

pnpm add @gquittet/graceful-server

Endpoint

Below you can find the default endpoint but you can setup or disable them. To do that, check out the Options part.

/live

The endpoint responds:

  • 200 status code with the uptime of the server in second.
{ "uptime": 42 }

Used to configure liveness probe.

/ready

The endpoint responds:

  • 200 status code if the server is ready.
{ "status": "ready" }
  • 503 status code with an empty response if the server is not ready (started, shutting down, etc).

Example

ExpressJS

The library works with the default HTTP NodeJS object. So, when you're using Express you can't pass directly the app object from Express. But, you can easily generate an HTTP NodeJS object from the app object.

Just follow the bottom example:

const express = require('express')
const helmet = require('helmet')
const http = require('http')
const GracefulServer = require('@gquittet/graceful-server')
const { connectToDb, closeDbConnection } = require('./db')

const app = express()
const server = http.createServer(app)
const gracefulServer = GracefulServer(server, { closePromises: [closeDbConnection] })

app.use(helmet())

app.get('/test', (_, res) => {
  return res.send({ uptime: process.uptime() | 0 })
})

gracefulServer.on(GracefulServer.READY, () => {
  console.log('Server is ready')
})

gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => {
  console.log('Server is shutting down')
})

gracefulServer.on(GracefulServer.SHUTDOWN, error => {
  console.log('Server is down because of', error.message)
})

server.listen(8080, async () => {
  await connectToDb()
  gracefulServer.setReady()
})

As you can see, we're using the app object from Express to set up the endpoints and middleware. But it can't listen (you can do it but app hasn't any liveness or readiness). The listening of HTTP calls need to be done by the default NodeJS HTTP object (aka server).

Fastify

const fastify = require('fastify')({ logger: true })
const GracefulServer = require('@gquittet/graceful-server')

const gracefulServer = GracefulServer(fastify.server)

gracefulServer.on(GracefulServer.READY, () => {
  console.log('Server is ready')
})

gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => {
  console.log('Server is shutting down')
})

gracefulServer.on(GracefulServer.SHUTDOWN, error => {
  console.log('Server is down because of', error.message)
})

// Declare a route
fastify.get('/', async (request, reply) => {
  return { hello: 'world' }
})

// Run the server!
const start = async () => {
  try {
    await fastify.listen({ port: 3000 })
    fastify.log.info(`server listening on ${fastify.server.address().port}`)
    gracefulServer.setReady()
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}
start()

Be careful, if you are using Fastify v4.x.x with Node 16 and below, you have to use

await fastify.listen({ port: 3000, host: '0.0.0.0' })

because Node 16 and below does not support multiple addresses binding.

See: fastify/fastify#3536

Koa

const GracefulServer = require('@gquittet/graceful-server')
const Koa = require('koa')
const http = require('http')
const Router = require('koa-router')

const app = new Koa()
const router = new Router()

const server = http.createServer(app.callback())
gracefulServer = GracefulServer(server)

router.get('/test')
app.use(router.routes())

// response
app.use(ctx => {
  ctx.body = 'Hello Koa'
})

gracefulServer.on(GracefulServer.READY, () => {
  console.log('Server is ready')
})

gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => {
  console.log('Server is shutting down')
})

gracefulServer.on(GracefulServer.SHUTDOWN, error => {
  console.log('Server is down because of', error.message)
})

server.listen(8080, async () => {
  gracefulServer.setReady()
})

As you can see, we're using the app object from Express to set up the endpoints and middleware. But it can't listen (you can do it but app hasn't any liveness or readiness). The listening of HTTP calls need to be done by the default NodeJS HTTP object (aka server).

HTTP Server

import http from 'http'
import url from 'url'
import GracefulServer from '@gquittet/graceful-server'
import { connectToDb, closeDbConnection } from './db'

const server = http.createServer((req, res) => {
  if (req.url === '/test' && req.method === 'GET') {
    res.statusCode = 200
    res.setHeader('Content-Type', 'application/json')
    return res.end(JSON.stringify({ uptime: process.uptime() | 0 }))
  }
  res.statusCode = 404
  return res.end()
})

const gracefulServer = GracefulServer(server, { closePromises: [closeDbConnection] })

gracefulServer.on(GracefulServer.READY, () => {
  console.log('Server is ready')
})

gracefulServer.on(GracefulServer.SHUTTING_DOWN, () => {
  console.log('Server is shutting down')
})

gracefulServer.on(GracefulServer.SHUTDOWN, error => {
  console.log('Server is down because of', error.message)
})

server.listen(8080, async () => {
  await connectToDb()
  gracefulServer.setReady()
})

API

GracefulServer

;((server: http.Server, options?: IGracefulServerOptions | undefined) => IGracefulServer) & typeof State

where State is an enum that contains, STARTING, READY, SHUTTING_DOWN and SHUTDOWN.

IGracefulServerOptions

All of the below options are optional.

Name Type Default Description
closePromises (() => Promise)[] [] The functions to run when the API is stopping
timeout number 1000 The time in milliseconds to wait before shutting down the server
healthCheck boolean true Enable/Disable the default endpoints (liveness and readiness)
kubernetes boolean false Enable/Disable the kubernetes mode
livenessEndpoint string /live The liveness endpoint
readinessEndpoint string /ready The readiness endpoint

If you use Kubernetes, enable the kubernetes mode to let it handles the incoming traffic of your application.

The Kubernetes mode will only work if you haven't disabled the health checks.

GracefulServer Instance

export default interface IGracefulServer {
  isReady: () => boolean
  setReady: () => void
  on: (name: string, callback: (...args: any[]) => void) => EventEmitter
}

Integration with Docker

HEALTH CHECK in Dockerfile

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"]

Content of healthcheck.js

const http = require('http')

const options = {
  timeout: 2000,
  host: 'localhost',
  port: 8080,
  path: '/live'
}

const request = http.request(options, res => {
  console.info('STATUS:', res.statusCode)
  process.exitCode = res.statusCode === 200 ? 0 : 1
  process.exit()
})

request.on('error', err => {
  console.error('ERROR', err)
  process.exit(1)
})

request.end()

Example of Dockerfile

POC level

FROM node:12-slim

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"]
CMD [ "node", "server.js" ]

Company level

FROM node:12-slim as base
ENV NODE_ENV=production
ENV TINI_VERSION=v0.18.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini && \
  mkdir -p /node_app/app && \
  chown -R node:node /node_app
WORKDIR /node_app
USER node
COPY --chown=node:node package.json package-lock*.json ./
RUN npm ci && \
  npm cache clean --force
WORKDIR /node_app/app

FROM base as source
COPY --chown=node:node . .

FROM source as dev
ENV NODE_ENV=development
ENV PATH=/node_app/node_modules/.bin:$PATH
RUN npm install --only=development --prefix /node_app
CMD ["nodemon", "--inspect=0.0.0.0:9229"]

FROM source as test
ENV NODE_ENV=development
ENV PATH=/node_app/node_modules/.bin:$PATH
COPY --from=dev /node_app/node_modules /node_app/node_modules
RUN npm run lint
ENV NODE_ENV=test
RUN npm test
CMD ["npm", "test"]

FROM test as audit
RUN npm audit --audit-level critical
USER root
ADD https://get.aquasec.com/microscanner /
RUN chmod +x /microscanner && \
    /microscanner your_token --continue-on-failure

FROM source as buildProd
ENV PATH=/node_app/node_modules/.bin:$PATH
COPY --from=dev /node_app/node_modules /node_app/node_modules
RUN npm run build

FROM source as prod
COPY --from=buildProd --chown=node:node /node_app/app/build ./build
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s CMD ["node healthcheck.js"]
ENTRYPOINT ["/tini", "--"]
CMD ["node", "./build/src/main.js"]

Integration with Kubernetes

Don't forget to enable the kubernetes mode. Check here (related to this issue)

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  failureThreshold: 1
  initialDelaySeconds: 5
  periodSeconds: 5
  successThreshold: 1
  timeoutSeconds: 5
livenessProbe:
  httpGet:
    path: /live
    port: 8080
  failureThreshold: 3
  initialDelaySeconds: 10
  # Allow sufficient amount of time (90 seconds = periodSeconds * failureThreshold)
  # for the registered shutdown handlers to run to completion.
  periodSeconds: 30
  successThreshold: 1
  # Setting a very low timeout value (e.g. 1 second) can cause false-positive
  # checks and service interruption.
  timeoutSeconds: 5

# As per Kubernetes documentation (https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#when-should-you-use-a-startup-probe),
# startup probe should point to the same endpoint as the liveness probe.
#
# Startup probe is only needed when container is taking longer to start than
# `initialDelaySeconds + failureThreshold ร— periodSeconds` of the liveness probe.
startupProbe:
  httpGet:
    path: /live
    port: 8080
  failureThreshold: 3
  initialDelaySeconds: 10
  periodSeconds: 30
  successThreshold: 1
  timeoutSeconds: 5

Thanks

โ˜… Terminus

โ˜… Lightship

โ˜… Stoppable

โ˜… Bret Fisher for his great articles and videos

โ˜… IBM documentation

โ˜… Node HTTP documentation

โ˜… Cloud Health

โ˜… Cloud Health Connect

Sponsors

JetBrains Logo

Donate

Donate

If you like my job, don't hesitate to contribute to this project! โค๏ธ

graceful-server's People

Contributors

anri-asaturov avatar cibergarri avatar dependabot[bot] avatar gquittet avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

graceful-server's Issues

Make a kubernetes mode

At this time, @gquittet/graceful-server manages the incoming traffic.

As requested here: #5, we need to let Kubernetes manages the traffic of the application.

The simple solution is to add a Kubernetes mode, that is disabled by default.

Users can enable it in the settings of graceful-server.

Missing kubernetes in options interface.

I forgot to add the kubernetes boolean to the IGracefulServerOptions interface.

Just add it like this:

Before

export default interface IGracefulServerOptions {
  closePromises?: (() => Promise<unknown>)[]
  timeout?: number
  healthCheck?: boolean
  livenessEndpoint?: string
  readinessEndpoint?: string
}

After

export default interface IGracefulServerOptions {
  closePromises?: (() => Promise<unknown>)[]
  timeout?: number
  healthCheck?: boolean
  kubernetes?: boolean
  livenessEndpoint?: string
  readinessEndpoint?: string
}

ESM module import requires using .default property

When you have "type": "module", specified in your package.json, then using any types from GracefulServer must be postfixed with default, like so GracefulServer.default.

Import:
import GracefulServer from "@gquittet/graceful-server";

Not a blocker, but getting a nicer experience with ESM would be sweet๐Ÿ‘

Reference of a similar issue in unrelated repository: ajv-validator/ajv#1381

No /live or /ready registered

I am using this module and it does not appear to work.

I do not appear to get any registrations for live or ready -- i registered an onRoute before it and nothing happened either.

Also would be nice if we could register closePromises after the fact.. so if future dependencies could call fastify.onShuttingDown(async () => {}) to provide their own shutdown requirements within the plugin itself (that is what i implemented myself via the closePromises in example below...)

/**
   * @see https://github.com/gquittet/graceful-server
   * @see https://github.com/gquittet/graceful-server#integration-with-kubernetes
   * @see https://github.com/gquittet/graceful-server#integration-with-docker (for Docker HealthCheck if needed)
   */
  const graceful = GracefulServer(server.server, {
    kubernetes: true,
    timeout: 5000,
    healthCheck: true,
    livenessEndpoint: '/live',
    readinessEndpoint: '/ready',
    closePromises: [
      async function handleShuttingdown() {
        logger.server.info('Starting Graceful Shutdown');
        await Promise.all(
          Array.from(server.handleOnShuttingDown.entries()).map(
            ([name, fn]) => {
              logger.server.info(`Shutting Down "${name}"`);
              return fn(server);
            },
          ),
        );
        await onShuttingDown?.(server);
      },
    ],
  });

Improve documentation

For now, the documentation is too long to read.

  • Split technical documentation into another file
  • Write a CONTRIBUTE.md to facility the contributions

To analyze: use wiki or .md files

Empty replies during grace/timeout period

Hi

I'm trying to create a simple implementation example using Fastify and I noticed that once a SIGTERM or SIGINT signal is sent to the server that only the liveness and healthcheck endpoints continue to respond to requests.

All other routes send an empty reply.

I'm wondering is this is intended behaviour?

I'm currently using GCLB (Google Cloud Load Balancer) with Kubernetes ingress controllers and NEGs.
As far as I'm aware, the GCLB will continue to serve client traffic to all pods until the health check fails.
However, the server will respond with an empty reply which might get propagated back to the client before the health checks fail causing a couple of seconds downtime.

Some more information regarding this:
https://cloud.google.com/kubernetes-engine/docs/how-to/container-native-load-balancing#traffic_does_not_reach_endpoints

Full code for my experiment below. In this case /foo will result in an empty reply when stopping the server.

Note that I've set a timeout of 30s. which is the time it should keep responding with a 503 to allow for health checks to fail.

const Fastify = require('fastify')
const GracefulServer = require('@gquittet/graceful-server')

const fastify = Fastify({ logger: true })
const gracefulServer = GracefulServer(fastify.server, {
  timeout: 30000,
  closePromises: [fastify.close]
})

// Health check
fastify.get('/foo', (req, res) => res.send('bar'))

const start = async () => {
  try {
    await fastify.listen(8080)
    gracefulServer.setReady()
  } catch (err) {
    fastify.log.error(err)
    process.exit(1)
  }
}

start()

Any thoughts on this?

Remove gracePeriod

With the new Kubernetes mode, grace time will become useless.

  • Remove it from the code base.
  • Remove it from the documentation.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.