4.0.9 • Published 10 days ago

@gquittet/graceful-server v4.0.9

Weekly downloads
42,758
License
MIT
Repository
github
Last release
10 days ago

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: https://github.com/fastify/fastify/issues/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.

NameTypeDefaultDescription
closePromises(() => Promise)[][]The functions to run when the API is stopping
timeoutnumber1000The time in milliseconds to wait before shutting down the server
healthCheckbooleantrueEnable/Disable the default endpoints (liveness and readiness)
kubernetesbooleanfalseEnable/Disable the kubernetes mode
livenessEndpointstring/liveThe liveness endpoint
readinessEndpointstring/readyThe 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! ❤️

4.0.9

10 days ago

4.0.8

8 months ago

4.0.7

10 months ago

4.0.6

10 months ago

4.0.5

1 year ago

4.0.4

1 year ago

4.0.3

1 year ago

4.0.2

1 year ago

4.0.1

2 years ago

3.0.2

2 years ago

3.0.0

2 years ago

2.5.2

2 years ago

2.5.0

3 years ago

2.4.1

3 years ago

2.5.1

3 years ago

2.4.0

3 years ago

2.3.3

3 years ago

2.3.1

3 years ago

2.2.2

3 years ago

2.2.0

3 years ago

2.1.1

3 years ago

2.0.2

3 years ago

2.1.0

3 years ago

2.0.1

3 years ago

1.3.0

3 years ago

2.0.0

3 years ago

1.2.8

4 years ago

1.2.7

4 years ago

1.2.6

4 years ago

1.2.5

4 years ago

1.2.4

4 years ago

1.2.3

4 years ago

1.2.2

4 years ago

1.2.1

4 years ago

1.2.0

4 years ago

1.1.0

4 years ago

1.0.0

4 years ago