Skip to content

Commit

Permalink
feat: Handle termination signals
Browse files Browse the repository at this point in the history
This patch adds handling of SIGINT and SIGTERM so that the server
is closed gracefully. It also moves the code that starts the server into
a function (in `server.ts`), separate from the command-line entrypoint
(in newly-added `main.ts`). This way, each code part's responsibilities
are clearer than if the process exit logic was conflated with running
Fastify.
  • Loading branch information
meyfa committed Jul 19, 2024
1 parent 08d91bd commit ed46262
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 45 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ EXPOSE 8080

# use tini as init process since Node.js isn't designed to be run as PID 1
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "--disable-proto=delete", "dist/server.js"]
CMD ["node", "--disable-proto=delete", "dist/main.js"]
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"build": "npm run build --workspaces && node -e \"fs.rmSync('./dist',{force:true,recursive:true})\" && tsc",
"lint": "npm run lint --workspaces && tsc --noEmit -p tsconfig.lint.json && eslint --ignore-path .gitignore src",
"test": "npm run test --workspaces --if-present && mocha --require tsx --recursive \"test/**/*.ts\"",
"start": "node --disable-proto=delete dist/server.js"
"start": "node --disable-proto=delete dist/main.js"
},
"repository": {
"type": "git",
Expand Down
89 changes: 89 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import path from 'node:path'
import { Config, readConfigDirectory } from './config.js'
import { StructError } from 'superstruct'
import pino from 'pino'
import { startServer } from './server.js'
import { getPort } from './environment.js'
import { KubeConfig } from '@kubernetes/client-node'
import { loadKubeConfig } from './kube-config.js'

const log = pino({
level: 'info',
// do not log pid and hostname
base: undefined,
// use ISO strings for timestamps instead of milliseconds
timestamp: pino.stdTimeFunctions.isoTime,
// use string levels (e.g., "info") instead of level numbers (e.g., 30)
formatters: {
level: (label) => ({ level: label }),
log: (object) => {
if (object instanceof Error) {
return pino.stdSerializers.errWithCause(object)
}
return object
}
}
})

log.info('process_start')

let config: Config
try {
config = await readConfigDirectory(path.join(process.cwd(), 'config'))
} catch (error) {
if (error instanceof StructError) {
// the value may contain secrets, so don't log it
error.value = undefined
error.branch = []
}
log.fatal(error, 'config_error')
process.exit(1)
}

let port: number
try {
port = getPort()
} catch (error) {
log.fatal(error, 'config_error')
process.exit(1)
}

let kubeConfig: KubeConfig
try {
kubeConfig = loadKubeConfig(log, config)
} catch (error) {
log.fatal(error, 'config_error')
process.exit(1)
}

const abortController = new AbortController()
for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.once(signal, () => {
log.info({ signal }, 'process_signal')
abortController.abort()
})
}

try {
const close = await startServer({
log,
config,
port,
kubeConfig
})

if (abortController.signal.aborted) {
await close()
} else {
abortController.signal.addEventListener('abort', () => {
close().then(() => {
log.info('server_closed')
}).catch((error) => {
log.error(error, 'server_close_error')
})
})
}
} catch (error) {
log.fatal(error, 'uncaught_error')
process.exit(1)
}
58 changes: 15 additions & 43 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,49 +4,20 @@ import { backend, notFound } from 'backend'
import { fastifyStatic } from '@fastify/static'
import path from 'node:path'
import { fastify } from 'fastify'
import { Config, readConfigDirectory } from './config.js'
import { getPort } from './environment.js'
import { loadKubeConfig } from './kube-config.js'
import { StructError } from 'superstruct'
import { Config } from './config.js'
import { KubeConfig } from '@kubernetes/client-node'
import { handleError } from './handle-error.js'
import pino from 'pino'
import { BaseLogger } from 'pino'

const log = pino({
level: 'info',
// do not log pid and hostname
base: undefined,
// use ISO strings for timestamps instead of milliseconds
timestamp: pino.stdTimeFunctions.isoTime,
// use string levels (e.g., "info") instead of level numbers (e.g., 30)
formatters: {
level: (label) => ({ level: label }),
log: (object) => {
if (object instanceof Error) {
return pino.stdSerializers.errWithCause(object)
}
return object
}
}
})

log.info('process_start')

let config: Config
try {
config = await readConfigDirectory(path.join(process.cwd(), 'config'))
} catch (error) {
if (error instanceof StructError) {
// the value may contain secrets, so don't log it
error.value = undefined
error.branch = []
}
log.fatal(error, 'config_error')
process.exit(1)
}
type CloseFunction = () => Promise<void>

try {
const port = getPort()
const kubeConfig = loadKubeConfig(log, config)
export async function startServer (options: {
log: BaseLogger
config: Config
port: number
kubeConfig: KubeConfig
}): Promise<CloseFunction> {
const { log, config, port, kubeConfig } = options

const app = fastify({
logger: log
Expand Down Expand Up @@ -85,7 +56,8 @@ try {
})

await app.listen({ port, host: '::' })
} catch (error) {
log.fatal(error, 'uncaught_error')
process.exit(1)

return async () => {
await app.close()
}
}

0 comments on commit ed46262

Please sign in to comment.