Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(simulator): miscellaneous cleanup fixes and improvements #3897

Merged
merged 1 commit into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion simulator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"lint": "tsc -b && eslint --ext .ts,.tsx,.js,.jsx src/",
"lint:fix": "tsc -b && eslint --ext .ts,.tsx,.js,.jsx src/ --fix",
"start:js": "cross-env OCLIF_TS_NODE=0 IRONFISH_DEBUG=1 node --expose-gc --inspect=:0 --inspect-publish-uid=http --enable-source-maps bin/run",
"start": "yarn build && yarn start:js start",
"start": "cd .. && yarn build && cd simulator && yarn start:js start",
"stop": "yarn build && yarn start:js stop",
"test": "yarn clean && tsc -b && tsc -b tsconfig.test.json && jest --passWithNoTests",
"test:coverage:html": "tsc -b tsconfig.test.json && jest --passWithNoTests --coverage --coverage-reporters html --testPathIgnorePatterns",
Expand Down
1 change: 0 additions & 1 deletion simulator/src/commands/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
export * from './start'
export * from './stop'
6 changes: 5 additions & 1 deletion simulator/src/commands/start.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,14 @@ export abstract class Start extends Command {
verboseLogging: flags.verbose,
})

logger.log(`created simulation ${simulator.simulationID}`)

try {
await simulation.run(simulator, logger)
} catch (e) {
logger.error(`simulation encountered ${String(e)}, shutting down...`)
logger.error(
`simulation ${simulator.simulationID} encountered ${String(e)}, shutting down...`,
)
simulator.exit(1)
}

Expand Down
50 changes: 0 additions & 50 deletions simulator/src/commands/stop.ts

This file was deleted.

58 changes: 10 additions & 48 deletions simulator/src/simulations/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
LogEvent,
SECOND,
sendTransaction,
SimulationNodeConfig,
Simulator,
} from '../simulator'

Expand All @@ -23,14 +22,11 @@ import {
* Description: <what the simulation is doing>
*/

export async function run(logger: Logger): Promise<void> {
// Create a new simulation handler.
// The simulator handles managing nodes and data dirs.
const simulator = new Simulator(logger)

export async function run(simulator: Simulator, logger: Logger): Promise<void> {
// Register event handlers.
// These hooks will be called when a node logs, closes, exits, or errors.
// This is useful when you want to validate certain behaviour, such as a node successfully exiting.
// These event handlers are optional, you can also add the default handler by passing the `-v` flag to the simulator.

// These sample handlers just log the event to the console.
const onLog = (event: LogEvent): void => {
Expand All @@ -45,19 +41,20 @@ export async function run(logger: Logger): Promise<void> {
logger.log(`[${event.node}:error] ${JSON.stringify(event)}`)
}

const nodes = []

// Create the nodes in the simulation.
// This will start the nodes and wait for them to initialize.
// The handlers must be passed into the addNode function to ensure that no events are missed.
const nodes = await Promise.all(
nodeConfig.map(async (cfg) => {
return simulator.startNode({
cfg,
for (let i = 0; i < 3; i++) {
nodes.push(
await simulator.startNode({
onLog: [onLog],
onExit: [onExit],
onError: [onError],
})
}),
)
}),
)
}

// This starts the miner on the first node.
// The miner can also be stopped via `node[0].stopMiner()`
Expand Down Expand Up @@ -104,38 +101,3 @@ export async function run(logger: Logger): Promise<void> {
// Call this to keep the simulation running. This currently will wait for all the nodes to exit.
await simulator.waitForShutdown()
}

// Configuration for the nodes in the simulation
const nodeConfig: SimulationNodeConfig[] = [
{
nodeName: 'node1',
blockGraffiti: '1',
peerPort: 7001,
dataDir: '~/.ironfish-atn/node1',
networkId: 2,
bootstrapNodes: ["''"], // This is a hack to register the bootstrap node
rpcTcpHost: 'localhost',
rpcTcpPort: 9001,
verbose: true,
},
{
nodeName: 'node2',
blockGraffiti: '2',
peerPort: 7002,
dataDir: '~/.ironfish-atn/node2',
networkId: 2,
bootstrapNodes: ['localhost:7001'],
rpcTcpHost: 'localhost',
rpcTcpPort: 9002,
},
{
nodeName: 'node3',
blockGraffiti: '3',
peerPort: 7003,
dataDir: '~/.ironfish-atn/node3',
networkId: 2,
bootstrapNodes: ['localhost:7001'],
rpcTcpHost: 'localhost',
rpcTcpPort: 9003,
},
]
19 changes: 19 additions & 0 deletions simulator/src/simulator/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ export type LogEvent = {
timestamp: string
}

/**
* Formats a LogEvent into a pretty string.
*/
export function logEventToString(l: LogEvent): string {
const msg = {
node: l.node,
proc: l.proc,
type: l.type,
message: l.jsonMessage,
timestamp: l.timestamp,
}

if (msg.message === undefined) {
return ''
}

return JSON.stringify(msg, undefined, 2)
}

/**
* NodeLogEvent is the JSON object that is logged by the Ironfish node.
* This is wrapped in a LogEvent when it is emitted to any listeners.
Expand Down
52 changes: 38 additions & 14 deletions simulator/src/simulator/simulation-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
supportedNodeChildProcesses,
} from './events'
import { sleep } from './misc'
import { getLatestBlockHash, importAccount } from './utils'
import { importAccount } from './utils'

export const rootCmd = 'ironfish'

Expand All @@ -52,6 +52,8 @@ export type RequiredSimulationNodeConfig = Required<
| 'networkId'
| 'rpcTcpHost'
| 'rpcTcpPort'
| 'rpcHttpHost'
| 'rpcHttpPort'
| 'bootstrapNodes'
>
>
Expand All @@ -64,7 +66,15 @@ export type OptionalSimulationNodeConfig = {
* The data directory for the node. If not provided, a temporary directory will be created.
*/
dataDir: string
/**
* Display verbose logging from the node.
*/
verbose?: boolean

/**
* Tags to include in the node logs. If omitted, all tags will be included.
*/
logTags?: string[]
/**
* Whether the genesis account should be added to this node.
* An explicit rescan will follow the import so the balance is immediately available.
Expand Down Expand Up @@ -231,14 +241,14 @@ export class SimulationNode {
const nodeConfig = new Config(fileSystem, config.dataDir)
await nodeConfig.load()

// TODO: support all the log levels
// TODO: support all the log levels, not just verbose
if (config.verbose) {
nodeConfig.set('logLevel', '*:debug')
}

for (const [key, value] of Object.entries(config)) {
// TODO(holahula): this is a hack to get around the fact that the config
// has `dataDir` / `verbose` properties that are not valid config option
// This is a hack to get around the fact that the simulation node config
// has `dataDir` / `verbose` properties that are not valid ironfish config options
if (key === 'dataDir' || key === 'verbose') {
continue
}
Expand All @@ -257,7 +267,7 @@ export class SimulationNode {

const node = new SimulationNode(config, client, logger, options)

// Continue to attempt to connect the client to the node until successful
// Attempt to connect the client to the node until successful
let connected = false
let tries = 0
while (!connected && tries < 12) {
Expand All @@ -270,7 +280,9 @@ export class SimulationNode {
throw new Error(`failed to connect to node ${config.nodeName}`)
}

node.initializeBlockStream(await getLatestBlockHash(node))
const { content: chainInfo } = await node.client.chain.getChainInfo()

node.initializeBlockStream(chainInfo.currentBlockIdentifier.hash)

if (config.importGenesisAccount) {
await importAccount(node, `'${JSON.stringify(DEV_GENESIS_ACCOUNT)}'`, true)
Expand Down Expand Up @@ -535,6 +547,8 @@ export class SimulationNode {
/**
* Executes a short-lived cli command via `child_process.spawn()`.
*
* If the user does not provide callbacks for errors or logs from the command, they will be printed to the console.
*
* This allows for the logs to be streamed to the console and the command to be executed in a separate process.
* If the command fails, the promise will reject.
*
Expand All @@ -547,31 +561,38 @@ export class SimulationNode {
*/
async executeCliCommand(
command: string,
args: string[],
commandArgs?: string[],
options?: {
onError: (err: Error) => void
onLog: (stdout: string) => void
onError?: (err: Error) => void
onLog?: (stdout: string) => void
},
): Promise<void> {
const args = commandArgs || []

args.push('--datadir', this.config.dataDir)

const cmdString = rootCmd + ' ' + command + ' ' + args.join(' ')

this.logger.log(`executing cli command (spawn): ${cmdString}`)

const onLog = options?.onLog || ((stdout) => this.logger.log(stdout))
const onError = options?.onError || ((err) => this.logger.error(JSON.stringify(err)))

return new Promise((resolve, reject) => {
const process = spawn(rootCmd, [command, ...args])

process.stdout.on('data', (data) => {
const message = (data as Buffer).toString()
options?.onLog(`${command}:stdout: ${message}`)
onLog(`${command}:stdout: ${message}`)
})

process.stderr.on('data', (data) => {
const message = (data as Buffer).toString()
options?.onLog(`${command}:stderr: ${message}`)
onLog(`${command}:stderr: ${message}`)
})

process.on('error', (err) => {
options?.onError(err)
onError(err)
reject(err)
})

Expand Down Expand Up @@ -616,14 +637,17 @@ export class SimulationNode {
* @param args The arguments for the command
* @throws an `ExecException` if the command fails
* @returns a promise containing the stdout and stderr output of the command
* // TODO: make args optional
*/
async executeCliCommandWithExec(
command: string,
args: string[],
args?: string[],
): Promise<{ stdout: string; stderr: string }> {
const execWithPromise = promisify(exec)

if (!args) {
args = []
}

args.push('--datadir', this.config.dataDir)

const cmdString = rootCmd + ' ' + command + ' ' + args.join(' ')
Expand Down
Loading