From f962baf1a1be6663200c6b1f2c2ca44acbd7678a Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Fri, 24 Oct 2025 12:57:16 +0200 Subject: [PATCH] add process handler for starting and managing GradidoNode as subprocess --- dlt-connector/src/bootstrap/shutdown.ts | 2 + .../client/GradidoNode/GradidoNodeProcess.ts | 117 ++++++++++++++++++ dlt-connector/src/config/const.ts | 7 ++ dlt-connector/src/config/schema.ts | 1 + 4 files changed, 127 insertions(+) create mode 100644 dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts diff --git a/dlt-connector/src/bootstrap/shutdown.ts b/dlt-connector/src/bootstrap/shutdown.ts index 781430008..415742d38 100644 --- a/dlt-connector/src/bootstrap/shutdown.ts +++ b/dlt-connector/src/bootstrap/shutdown.ts @@ -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() } diff --git a/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts new file mode 100644 index 000000000..ad25c2461 --- /dev/null +++ b/dlt-connector/src/client/GradidoNode/GradidoNodeProcess.ts @@ -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 { + 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() + } + } +} diff --git a/dlt-connector/src/config/const.ts b/dlt-connector/src/config/const.ts index 6b2a3b1a2..0547f0c8a 100644 --- a/dlt-connector/src/config/const.ts +++ b/dlt-connector/src/config/const.ts @@ -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 diff --git a/dlt-connector/src/config/schema.ts b/dlt-connector/src/config/schema.ts index 2ce01dcee..027dfc364 100644 --- a/dlt-connector/src/config/schema.ts +++ b/dlt-connector/src/config/schema.ts @@ -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'),