add process handler for starting and managing GradidoNode as subprocess

This commit is contained in:
einhornimmond 2025-10-24 12:57:16 +02:00
parent b9d51269ca
commit f962baf1a1
4 changed files with 127 additions and 0 deletions

View File

@ -1,4 +1,5 @@
import { Logger } from 'log4js'
import { GradidoNodeProcess } from '../client/GradidoNode/GradidoNodeProcess'
import { type AppContextClients } from './appContext'
export function setupGracefulShutdown(logger: Logger, clients: AppContextClients) {
@ -25,4 +26,5 @@ export function setupGracefulShutdown(logger: Logger, clients: AppContextClients
async function gracefulShutdown(logger: Logger, clients: AppContextClients) {
logger.info('graceful shutdown')
await clients.hiero.waitForPendingPromises()
await GradidoNodeProcess.getInstance().exit()
}

View File

@ -0,0 +1,117 @@
import { Subprocess, spawn } from 'bun'
import { getLogger, Logger } from 'log4js'
import { CONFIG } from '../../config'
import {
GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS,
GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS,
GRADIDO_NODE_RUNTIME_PATH,
LOG4JS_BASE_CATEGORY,
} from '../../config/const'
/**
* A Singleton class defines the `getInstance` method that lets clients access
* the unique singleton instance.
*
* Singleton Managing GradidoNode if started as subprocess
* will restart GradidoNode if it exits more than `GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS` milliseconds after start
* if exit was called, it will first try to exit graceful with SIGTERM and then kill with SIGKILL after `GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS` milliseconds
*/
export class GradidoNodeProcess {
private static instance: GradidoNodeProcess | null = null
private proc: Subprocess | null = null
private logger: Logger
private lastStarted: Date | null = null
private exitCalled: boolean = false
private constructor() {
// constructor is private to prevent instantiation from outside
// of the class except from the static getInstance method.
this.logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.GradidoNodeProcess`)
}
/**
* Static method that returns the singleton instance of the class.
* @returns the singleton instance of the class.
*/
public static getInstance(): GradidoNodeProcess {
if (!GradidoNodeProcess.instance) {
GradidoNodeProcess.instance = new GradidoNodeProcess()
}
return GradidoNodeProcess.instance
}
public start() {
if (this.proc) {
this.logger.warn('GradidoNodeProcess already running.')
return
}
this.logger.info(`starting GradidoNodeProcess with path: ${GRADIDO_NODE_RUNTIME_PATH}`)
this.lastStarted = new Date()
const logger = this.logger
this.proc = spawn([GRADIDO_NODE_RUNTIME_PATH], {
env: {
CLIENTS_HIERO_NETWORKTYPE: CONFIG.HIERO_HEDERA_NETWORK,
SERVER_JSON_RPC_PORT: CONFIG.DLT_NODE_SERVER_PORT.toString(),
},
onExit(proc, exitCode, signalCode, error) {
logger.warn(`GradidoNodeProcess exited with code ${exitCode} and signalCode ${signalCode}`)
if (error) {
logger.error(`GradidoNodeProcess exit error: ${error}`)
if (logger.isDebugEnabled() && proc.stderr) {
// print error messages from GradidoNode in our own log if debug is enabled
proc.stderr
.getReader()
.read()
.then((chunk) => {
logger.debug(chunk.value?.toString())
})
}
}
logger.debug(`ressource usage: ${proc?.resourceUsage()}`)
const gradidoNodeProcess = GradidoNodeProcess.getInstance()
gradidoNodeProcess.proc = null
if (
!gradidoNodeProcess.exitCalled &&
gradidoNodeProcess.lastStarted &&
Date.now() - gradidoNodeProcess.lastStarted.getTime() >
GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS
) {
// restart only if enough time was passed since last start to prevent restart loop
gradidoNodeProcess.start()
}
},
stdout: 'ignore',
stderr: logger.isDebugEnabled() ? 'pipe' : 'ignore',
})
}
public async restart() {
if (this.proc) {
await this.exit()
this.exitCalled = false
this.start()
}
}
public async exit(): Promise<void> {
this.exitCalled = true
if (this.proc) {
this.proc.kill('SIGTERM')
const timeout = setTimeout(() => {
this.logger.warn(
`GradidoNode couldn't exit graceful after ${GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS} milliseconds with SIGTERM, killing with SIGKILL`,
)
this.proc?.kill('SIGKILL')
}, GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS)
try {
await this.proc.exited
} catch (error) {
this.logger.error(`GradidoNodeProcess exit error: ${error}`)
} finally {
clearTimeout(timeout)
}
} else {
return Promise.resolve()
}
}
}

View File

@ -1,5 +1,12 @@
import path from 'node:path'
export const LOG4JS_BASE_CATEGORY = 'dlt'
// 7 days
export const MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_UPDATE = 1000 * 60 * 60 * 24 * 7
// 10 minutes
export const MIN_TOPIC_EXPIRE_MILLISECONDS_FOR_SEND_MESSAGE = 1000 * 60 * 10
export const GRADIDO_NODE_RUNTIME_PATH = path.join(__dirname, 'gradido_node', 'bin', 'GradidoNode')
// if last start was less than this time, do not restart
export const GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS = 1000 * 30
export const GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS = 1000

View File

@ -78,6 +78,7 @@ export const configSchema = v.object({
),
'8340',
),
DLT_NODE_SERVER_VERSION: v.optional(v.string('The version of the DLT node server'), '0.9.0'),
PORT: v.optional(
v.pipe(
v.string('A valid port on which the backend server is running'),