make restart and exit GradidoNode Process more robust, fix error in ensureCommunitiesAvailable

This commit is contained in:
einhornimmond 2025-11-13 09:52:01 +01:00
parent f66f33307d
commit 94ce58a68f
6 changed files with 34 additions and 20 deletions

View File

@ -3,11 +3,13 @@ import { getLogger, Logger } from 'log4js'
import { CONFIG } from '../../config' import { CONFIG } from '../../config'
import { import {
GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS, GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS,
GRADIDO_NODE_MIN_RUNTIME_BEFORE_EXIT_MILLISECONDS,
GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS, GRADIDO_NODE_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS,
GRADIDO_NODE_RUNTIME_PATH, GRADIDO_NODE_RUNTIME_PATH,
LOG4JS_BASE_CATEGORY, LOG4JS_BASE_CATEGORY,
} from '../../config/const' } from '../../config/const'
import { Mutex } from 'async-mutex'
import { delay } from '../../utils/time'
/** /**
* A Singleton class defines the `getInstance` method that lets clients access * A Singleton class defines the `getInstance` method that lets clients access
* the unique singleton instance. * the unique singleton instance.
@ -22,6 +24,7 @@ export class GradidoNodeProcess {
private logger: Logger private logger: Logger
private lastStarted: Date | null = null private lastStarted: Date | null = null
private exitCalled: boolean = false private exitCalled: boolean = false
private restartMutex: Mutex = new Mutex()
private constructor() { private constructor() {
// constructor is private to prevent instantiation from outside // constructor is private to prevent instantiation from outside
@ -55,7 +58,7 @@ export class GradidoNodeProcess {
USERPROFILE: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER, USERPROFILE: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER,
HOME: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER, HOME: CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER,
}, },
onExit(proc, exitCode, signalCode, error) { onExit(_proc, exitCode, signalCode, error) {
logger.warn(`GradidoNodeProcess exited with code ${exitCode} and signalCode ${signalCode}`) logger.warn(`GradidoNodeProcess exited with code ${exitCode} and signalCode ${signalCode}`)
if (error) { if (error) {
logger.error(`GradidoNodeProcess exit error: ${error}`) logger.error(`GradidoNodeProcess exit error: ${error}`)
@ -69,7 +72,6 @@ export class GradidoNodeProcess {
}) })
}*/ }*/
} }
logger.debug(`ressource usage: ${JSON.stringify(proc?.resourceUsage(), null, 2)}`)
const gradidoNodeProcess = GradidoNodeProcess.getInstance() const gradidoNodeProcess = GradidoNodeProcess.getInstance()
gradidoNodeProcess.proc = null gradidoNodeProcess.proc = null
if ( if (
@ -90,11 +92,16 @@ export class GradidoNodeProcess {
} }
public async restart() { public async restart() {
const release = await this.restartMutex.acquire()
try {
if (this.proc) { if (this.proc) {
await this.exit() await this.exit()
this.exitCalled = false this.exitCalled = false
this.start() this.start()
} }
} finally {
release()
}
} }
public getLastStarted(): Date | null { public getLastStarted(): Date | null {
@ -104,6 +111,9 @@ export class GradidoNodeProcess {
public async exit(): Promise<void> { public async exit(): Promise<void> {
this.exitCalled = true this.exitCalled = true
if (this.proc) { if (this.proc) {
if (this.lastStarted && Date.now() - this.lastStarted.getTime() < GRADIDO_NODE_MIN_RUNTIME_BEFORE_EXIT_MILLISECONDS) {
await delay(GRADIDO_NODE_MIN_RUNTIME_BEFORE_EXIT_MILLISECONDS - Date.now() - this.lastStarted.getTime())
}
this.proc.kill('SIGTERM') this.proc.kill('SIGTERM')
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
this.logger.warn( this.logger.warn(

View File

@ -27,7 +27,8 @@ export async function ensureCommunitiesAvailable(communityTopicIds: HieroId[]):
CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER, CONFIG.DLT_GRADIDO_NODE_SERVER_HOME_FOLDER,
GRADIDO_NODE_HOME_FOLDER_NAME, GRADIDO_NODE_HOME_FOLDER_NAME,
) )
if (!checkCommunityAvailable(communityTopicIds, homeFolder)) { const communityTopicIdsSet = new Set(communityTopicIds)
if (!checkCommunityAvailable(communityTopicIdsSet, homeFolder)) {
await exportCommunities(homeFolder, BackendClient.getInstance()) await exportCommunities(homeFolder, BackendClient.getInstance())
return GradidoNodeProcess.getInstance().restart() return GradidoNodeProcess.getInstance().restart()
} }
@ -65,7 +66,7 @@ export async function exportCommunities(homeFolder: string, client: BackendClien
logger.info(`exported ${communitiesForDltNodeServer.length} communities to ${communitiesPath}`) logger.info(`exported ${communitiesForDltNodeServer.length} communities to ${communitiesPath}`)
} }
export function checkCommunityAvailable(communityTopicIds: HieroId[], homeFolder: string): boolean { export function checkCommunityAvailable(communityTopicIds: Set<HieroId>, homeFolder: string): boolean {
const communitiesPath = path.join(homeFolder, 'communities.json') const communitiesPath = path.join(homeFolder, 'communities.json')
if (!checkFileExist(communitiesPath)) { if (!checkFileExist(communitiesPath)) {
return false return false
@ -73,12 +74,13 @@ export function checkCommunityAvailable(communityTopicIds: HieroId[], homeFolder
const communities = JSON.parse(fs.readFileSync(communitiesPath, 'utf-8')) const communities = JSON.parse(fs.readFileSync(communitiesPath, 'utf-8'))
let foundCount = 0 let foundCount = 0
for (const community of communities) { for (const community of communities) {
if (communityTopicIds.includes(community.hieroTopicId)) { if (communityTopicIds.has(community.hieroTopicId)) {
foundCount++ foundCount++
if (foundCount >= communityTopicIds.length) { if (foundCount >= communityTopicIds.size) {
return true return true
} }
} }
} }
logger.debug(`community not found for topic ids: ${communityTopicIds}, communities: ${JSON.stringify(communities, null, 2)}`)
return false return false
} }

View File

@ -15,7 +15,7 @@ import {
TransactionId, TransactionId,
Wallet, Wallet,
} from '@hashgraph/sdk' } from '@hashgraph/sdk'
import { GradidoTransaction } from 'gradido-blockchain-js' import { GradidoTransaction, Profiler } from 'gradido-blockchain-js'
import { getLogger, Logger } from 'log4js' import { getLogger, Logger } from 'log4js'
import * as v from 'valibot' import * as v from 'valibot'
import { CONFIG } from '../../config' import { CONFIG } from '../../config'
@ -24,8 +24,7 @@ import { HieroId, hieroIdSchema } from '../../schemas/typeGuard.schema'
import { type TopicInfoOutput, topicInfoSchema } from './output.schema' import { type TopicInfoOutput, topicInfoSchema } from './output.schema'
import { GradidoNodeClient } from '../GradidoNode/GradidoNodeClient' import { GradidoNodeClient } from '../GradidoNode/GradidoNodeClient'
import { GradidoNodeProcess } from '../GradidoNode/GradidoNodeProcess' import { GradidoNodeProcess } from '../GradidoNode/GradidoNodeProcess'
import { printTimeDuration } from '../../utils/time' import { durationInMinutesFromDates, printTimeDuration } from '../../utils/time'
// https://docs.hedera.com/hedera/sdks-and-apis/hedera-api/consensus/consensusupdatetopic // https://docs.hedera.com/hedera/sdks-and-apis/hedera-api/consensus/consensusupdatetopic
export const MIN_AUTORENEW_PERIOD = 6999999 //seconds export const MIN_AUTORENEW_PERIOD = 6999999 //seconds
export const MAX_AUTORENEW_PERIOD = 8000001 // seconds export const MAX_AUTORENEW_PERIOD = 8000001 // seconds
@ -75,7 +74,7 @@ export class HieroClient {
topicId: HieroId, topicId: HieroId,
transaction: GradidoTransaction, transaction: GradidoTransaction,
): Promise<TransactionId | null> { ): Promise<TransactionId | null> {
const startTime = new Date() const timeUsed = new Profiler()
this.transactionInternNr++ this.transactionInternNr++
const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.HieroClient`) const logger = getLogger(`${LOG4JS_BASE_CATEGORY}.client.HieroClient`)
logger.addContext('trNr', this.transactionInternNr) logger.addContext('trNr', this.transactionInternNr)
@ -96,11 +95,11 @@ export class HieroClient {
.then(async (signedHieroTransaction) => { .then(async (signedHieroTransaction) => {
const sendResponse = await signedHieroTransaction.executeWithSigner(this.wallet) const sendResponse = await signedHieroTransaction.executeWithSigner(this.wallet)
logger.info( logger.info(
`message sent to topic ${topicId}, transaction id: ${sendResponse.transactionId.toString()}`, `message sent to topic ${topicId}, transaction id: ${sendResponse.transactionId.toString()}, timeUsed: ${timeUsed.string()}`,
) )
// TODO: fix issue in GradidoNode // TODO: fix issue in GradidoNode
// hot fix, when gradido node is running some time, the hiero listener stop working, so we check if our new transaction is received // hot fix, when gradido node is running some time, the hiero listener stop working, so we check if our new transaction is received
// after 1 second, else restart GradidoNode // after 10 seconds, else restart GradidoNode
setTimeout(async () => { setTimeout(async () => {
const transaction = await GradidoNodeClient.getInstance().getTransaction({ const transaction = await GradidoNodeClient.getInstance().getTransaction({
topic: topicId, topic: topicId,
@ -110,7 +109,7 @@ export class HieroClient {
const process = GradidoNodeProcess.getInstance() const process = GradidoNodeProcess.getInstance()
const lastStarted = process.getLastStarted() const lastStarted = process.getLastStarted()
if (lastStarted) { if (lastStarted) {
const serverRunTime = printTimeDuration(new Date().getTime() - lastStarted.getTime()) const serverRunTime = printTimeDuration(durationInMinutesFromDates(lastStarted, new Date()))
this.logger.error(`transaction not found, restart GradidoNode after ${serverRunTime}`) this.logger.error(`transaction not found, restart GradidoNode after ${serverRunTime}`)
await GradidoNodeProcess.getInstance().restart() await GradidoNodeProcess.getInstance().restart()
} else { } else {
@ -118,7 +117,7 @@ export class HieroClient {
GradidoNodeProcess.getInstance().start() GradidoNodeProcess.getInstance().start()
} }
} }
}, 1000) }, 10000)
if (logger.isInfoEnabled()) { if (logger.isInfoEnabled()) {
// only for logging // only for logging
sendResponse.getReceiptWithSigner(this.wallet).then((receipt) => { sendResponse.getReceiptWithSigner(this.wallet).then((receipt) => {
@ -127,9 +126,8 @@ export class HieroClient {
// only for logging // only for logging
sendResponse.getRecordWithSigner(this.wallet).then((record) => { sendResponse.getRecordWithSigner(this.wallet).then((record) => {
logger.info(`message sent, cost: ${record.transactionFee.toString()}`) logger.info(`message sent, cost: ${record.transactionFee.toString()}`)
const localEndTime = new Date()
logger.info( logger.info(
`HieroClient.sendMessage used time (full process): ${localEndTime.getTime() - startTime.getTime()}ms`, `HieroClient.sendMessage used time (full process): ${timeUsed.string()}`,
) )
}) })
} }
@ -141,8 +139,7 @@ export class HieroClient {
this.pendingPromises.splice(pendingPromiseIndex, 1) this.pendingPromises.splice(pendingPromiseIndex, 1)
}), }),
) )
const endTime = new Date() logger.debug(`create transactionId: ${hieroTransaction.transactionId?.toString()}, used time: ${timeUsed.string()}`)
logger.info(`HieroClient.sendMessage used time: ${endTime.getTime() - startTime.getTime()}ms`)
return hieroTransaction.transactionId return hieroTransaction.transactionId
} }

View File

@ -16,6 +16,7 @@ export const GRADIDO_NODE_RUNTIME_PATH = path.join(
) )
// if last start was less than this time, do not restart // 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_MIN_RUNTIME_BEFORE_RESTART_MILLISECONDS = 1000 * 30
export const GRADIDO_NODE_MIN_RUNTIME_BEFORE_EXIT_MILLISECONDS = 1000 * 2
export const GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS = 10000 export const GRADIDO_NODE_KILL_TIMEOUT_MILLISECONDS = 10000
// currently hard coded in gradido node, update in future // currently hard coded in gradido node, update in future
export const GRADIDO_NODE_HOME_FOLDER_NAME = '.gradido' export const GRADIDO_NODE_HOME_FOLDER_NAME = '.gradido'

View File

@ -104,7 +104,7 @@ async function sendViaHiero(
if (!transactionId) { if (!transactionId) {
throw new Error('missing transaction id from hiero') throw new Error('missing transaction id from hiero')
} }
logger.info('transmitted Gradido Transaction to Hiero', { logger.debug('give Gradido Transaction to Hiero Client', {
transactionId: transactionId.toString(), transactionId: transactionId.toString(),
}) })
return v.parse(hieroTransactionIdStringSchema, transactionId.toString()) return v.parse(hieroTransactionIdStringSchema, transactionId.toString())

View File

@ -1,3 +1,5 @@
import { promisify } from 'node:util'
/** /**
* @param {number} time - in minutes * @param {number} time - in minutes
*/ */
@ -40,3 +42,5 @@ export const printTimeDuration = (duration: number): string => {
} }
return result return result
} }
export const delay = promisify(setTimeout)