Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

fix: handle initialization errors on server.listen, handle fallback rejections #1227

Merged
merged 4 commits into from
Sep 22, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
270 changes: 139 additions & 131 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,140 +256,149 @@ export default class Blockchain extends Emittery.Typed<
const options = this.#options;
const instamine = this.#instamine;

let common: Common;
if (this.fallback) {
await Promise.all([database.initialize(), this.fallback.initialize()]);
common = this.common = this.fallback.common;
options.fork.blockNumber = this.fallback.blockNumber.toNumber();
options.chain.networkId = common.networkId();
options.chain.chainId = common.chainId();
} else {
await database.initialize();
common = this.common = createCommon(
options.chain.chainId,
options.chain.networkId,
options.chain.hardfork
);
}

const blocks = (this.blocks = await BlockManager.initialize(
this,
common,
database.blockIndexes,
database.blocks
));

this.blockLogs = new BlockLogManager(database.blockLogs, this);
this.transactions = new TransactionManager(
options.miner,
common,
this,
database.transactions
);
this.transactionReceipts = new TransactionReceiptManager(
database.transactionReceipts,
this
);
this.accounts = new AccountManager(this);
this.storageKeys = database.storageKeys;

// if we have a latest block, use it to set up the trie.
const { latest } = blocks;
{
let stateRoot: Data | null;
if (latest) {
this.#blockBeingSavedPromise = Promise.resolve({
block: latest,
blockLogs: null
});
({ stateRoot } = latest.header);
try {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is just wrapping things in a try/catch

let common: Common;
if (this.fallback) {
await this.fallback.initialize();
await database.initialize();
Comment on lines +262 to +263
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was changed because previously, if the fallback throws while database is still starting it becomes very tricky to ensure we can wait for the database to finish before trying to shut the blockchain class down. We can do it, and I did, but it was a lot of code involving handling the weird return values of Promise.allSettled combined with throwing an AggregateError in some cases and not in others.


common = this.common = this.fallback.common;
options.fork.blockNumber = this.fallback.blockNumber.toNumber();
options.chain.networkId = common.networkId();
options.chain.chainId = common.chainId();
} else {
stateRoot = null;
await database.initialize();
common = this.common = createCommon(
options.chain.chainId,
options.chain.networkId,
options.chain.hardfork
);
}
this.trie = makeTrie(this, database.trie, stateRoot);
}

// create VM and listen to step events
this.vm = await this.createVmFromStateTrie(
this.trie,
options.chain.allowUnlimitedContractSize,
true
);
const blocks = (this.blocks = await BlockManager.initialize(
this,
common,
database.blockIndexes,
database.blocks
));

{
// create first block
let firstBlockTime: number;
if (options.chain.time != null) {
// If we were given a timestamp, use it instead of the `_currentTime`
const t = options.chain.time.getTime();
firstBlockTime = Math.floor(t / 1000);
this.setTime(t);
} else {
firstBlockTime = this.#currentTime();
this.blockLogs = new BlockLogManager(database.blockLogs, this);
this.transactions = new TransactionManager(
options.miner,
common,
this,
database.transactions
);
this.transactionReceipts = new TransactionReceiptManager(
database.transactionReceipts,
this
);
this.accounts = new AccountManager(this);
this.storageKeys = database.storageKeys;

// if we have a latest block, use it to set up the trie.
const { latest } = blocks;
{
let stateRoot: Data | null;
if (latest) {
this.#blockBeingSavedPromise = Promise.resolve({
block: latest,
blockLogs: null
});
({ stateRoot } = latest.header);
} else {
stateRoot = null;
}
this.trie = makeTrie(this, database.trie, stateRoot);
}

// if we don't already have a latest block, create a genesis block!
if (!latest) {
if (initialAccounts.length > 0) {
await this.#commitAccounts(initialAccounts);
}
// create VM and listen to step events
this.vm = await this.createVmFromStateTrie(
this.trie,
options.chain.allowUnlimitedContractSize,
true
);

this.#blockBeingSavedPromise = this.#initializeGenesisBlock(
firstBlockTime,
options.miner.blockGasLimit,
initialAccounts
);
blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then(
({ block }) => block
);
}
}
{
// create first block
let firstBlockTime: number;
if (options.chain.time != null) {
// If we were given a timestamp, use it instead of the `_currentTime`
const t = options.chain.time.getTime();
firstBlockTime = Math.floor(t / 1000);
this.setTime(t);
} else {
firstBlockTime = this.#currentTime();
}

{
// configure and start miner
const txPool = this.transactions.transactionPool;
const minerOpts = options.miner;
const miner = (this.#miner = new Miner(
minerOpts,
txPool.executables,
this.vm,
this.#readyNextBlock
));
// if we don't already have a latest block, create a genesis block!
if (!latest) {
if (initialAccounts.length > 0) {
await this.#commitAccounts(initialAccounts);
}

//#region re-emit miner events:
miner.on("ganache:vm:tx:before", event => {
this.emit("ganache:vm:tx:before", event);
});
miner.on("ganache:vm:tx:step", event => {
if (!this.#emitStepEvent) return;
this.emit("ganache:vm:tx:step", event);
});
miner.on("ganache:vm:tx:after", event => {
this.emit("ganache:vm:tx:after", event);
});
//#endregion

//#region automatic mining
const nullResolved = Promise.resolve(null);
const mineAll = (maxTransactions: Capacity) =>
this.#isPaused() ? nullResolved : this.mine(maxTransactions);
if (instamine) {
// insta mining
// whenever the transaction pool is drained mine the txs into blocks
txPool.on("drain", mineAll.bind(null, Capacity.Single));
} else {
// interval mining
const wait = () =>
// unref, so we don't hold the chain open if nothing can interact with it
unref((this.#timer = setTimeout(next, minerOpts.blockTime * 1e3)));
const next = () => mineAll(Capacity.FillBlock).then(wait);
wait();
this.#blockBeingSavedPromise = this.#initializeGenesisBlock(
firstBlockTime,
options.miner.blockGasLimit,
initialAccounts
);
blocks.earliest = blocks.latest = await this.#blockBeingSavedPromise.then(
({ block }) => block
);
}
}
//#endregion

miner.on("block", this.#handleNewBlockData);
{
// configure and start miner
const txPool = this.transactions.transactionPool;
const minerOpts = options.miner;
const miner = (this.#miner = new Miner(
minerOpts,
txPool.executables,
this.vm,
this.#readyNextBlock
));

//#region re-emit miner events:
miner.on("ganache:vm:tx:before", event => {
this.emit("ganache:vm:tx:before", event);
});
miner.on("ganache:vm:tx:step", event => {
if (!this.#emitStepEvent) return;
this.emit("ganache:vm:tx:step", event);
});
miner.on("ganache:vm:tx:after", event => {
this.emit("ganache:vm:tx:after", event);
});
//#endregion

//#region automatic mining
const nullResolved = Promise.resolve(null);
const mineAll = (maxTransactions: Capacity) =>
this.#isPaused() ? nullResolved : this.mine(maxTransactions);
if (instamine) {
// insta mining
// whenever the transaction pool is drained mine the txs into blocks
txPool.on("drain", mineAll.bind(null, Capacity.Single));
} else {
// interval mining
const wait = () =>
// unref, so we don't hold the chain open if nothing can interact with it
unref((this.#timer = setTimeout(next, minerOpts.blockTime * 1e3)));
const next = () => mineAll(Capacity.FillBlock).then(wait);
wait();
}
//#endregion

miner.on("block", this.#handleNewBlockData);

this.once("stop").then(() => miner.clearListeners());
this.once("stop").then(() => miner.clearListeners());
}
} catch (e) {
// we failed to start up. bail! :-(
this.#state = Status.stopping;
davidmurdoch marked this conversation as resolved.
Show resolved Hide resolved
this.stop();
throw e;
}

this.#state = Status.started;
Expand Down Expand Up @@ -1567,25 +1576,27 @@ export default class Blockchain extends Emittery.Typed<
* Gracefully shuts down the blockchain service and all of its dependencies.
*/
public async stop() {
// If the blockchain is still initalizing we don't want to shut down
// If the blockchain is still initializing we don't want to shut down
// yet because there may still be database calls in flight. Leveldb may
// cause a segfault due to a race condition between a db write and the close
// call.
if (this.#state === Status.starting) {
await this.once("ready");
}

this.#state = Status.stopping;

// stop the polling miner, if necessary
clearTimeout(this.#timer);

// clean up listeners
this.vm.removeAllListeners();
this.vm && this.vm.removeAllListeners();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we now call stop internally on initialization exceptions we might not have everything ready, so we check for things' existence before trying to clean them up.


// pause processing new transactions...
await this.transactions.pause();
this.transactions && (await this.transactions.pause());

// then pause the miner, too.
await this.#miner.pause();
this.#miner && (await this.#miner.pause());

// wait for anything in the process of being saved to finish up
await this.#blockBeingSavedPromise;
Expand All @@ -1594,10 +1605,7 @@ export default class Blockchain extends Emittery.Typed<

await this.emit("stop");

if (this.#state === Status.started) {
this.#state = Status.stopping;
await this.#database.close();
this.#state = Status.stopped;
}
this.#database && (await this.#database.close());
this.#state = Status.stopped;
}
}
2 changes: 1 addition & 1 deletion src/chains/ethereum/ethereum/src/forking/fork.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class Fork {
// TODO: remove support for legacy providers
// legacy `.send`
console.warn(
"WARNING: Ganache forking only supports EIP-1193-compliant providers. Legacy support for send is currently enabled, but will be removed in a future version _without_ a breaking change. To remove this warning, switch to an EIP-1193 provider. This error is probably caused by an old version of Web3's HttpProvider (or an ganache < v7)"
"WARNING: Ganache forking only supports EIP-1193-compliant providers. Legacy support for send is currently enabled, but will be removed in a future version _without_ a breaking change. To remove this warning, switch to an EIP-1193 provider. This error is probably caused by an old version of Web3's HttpProvider (or ganache < v7)"
);
return new Promise<T>((resolve, reject) => {
(forkingOptions.provider as any).send(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EthereumInternalOptions } from "@ganache/ethereum-options";
import { JsonRpcResponse, JsonRpcError } from "@ganache/utils";
import { AbortError } from "@ganache/ethereum-utils";
import { AbortError, CodedError } from "@ganache/ethereum-utils";
// TODO: support http2
import http, { RequestOptions, Agent as HttpAgent } from "http";
import https, { Agent as HttpsAgent } from "https";
Expand Down Expand Up @@ -172,7 +172,7 @@ export class HttpHandler extends BaseHandler implements Handler {
if ("result" in result) {
return result.result;
} else if ("error" in result) {
throw result.error;
throw new CodedError(result.error.message, result.error.code);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't throw plain objects, kids.

}
});
this.requestCache.set(data, promise);
Expand Down
4 changes: 2 additions & 2 deletions src/packages/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ const Ganache = {
provider: <T extends FlavorName = typeof DefaultFlavor>(
options?: ProviderOptions<T>
): ConnectorsByName[T]["provider"] => {
const connector = ConnectorLoader.initialize<T>(options);
return connector.provider;
const loader = ConnectorLoader.initialize<T>(options);
return loader.connector.provider;
}
};

Expand Down
8 changes: 5 additions & 3 deletions src/packages/core/src/connector-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@ const initialize = <T extends FlavorName = typeof DefaultFlavor>(
// connector.connect interface
const connectPromise = connector.connect
? connector.connect()
: (connector as any).initialize();
: ((connector as any).initialize() as Promise<void>);

// The request coordinator is initialized in a "paused" state; when the
// provider is ready we unpause.. This lets us accept queue requests before
// we've even fully initialized.
connectPromise.then(requestCoordinator.resume);

return connector;
return {
connector,
promise: connectPromise.then(requestCoordinator.resume)
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

give connector loader callers, like server.ts, the ability to handle start up promise rejections.

};

/**
Expand Down
Loading