-
-
Notifications
You must be signed in to change notification settings - Fork 474
refactor: separate writeBlockInputToDb into parallel block and column writes #8974
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
Changes from all commits
27aedf3
abb9a4a
b1722b2
9cde8a5
8cef8fd
8e6a5c8
5dc06f8
81ba7dd
40b89d1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,15 @@ | ||
| import {fulu} from "@lodestar/types"; | ||
| import {prettyPrintIndices, toRootHex} from "@lodestar/utils"; | ||
| import {ForkPostDeneb, isForkPostDeneb} from "@lodestar/params"; | ||
| import {SignedBeaconBlock} from "@lodestar/types"; | ||
| import {fromHex, toRootHex} from "@lodestar/utils"; | ||
| import {getBlobKzgCommitments} from "../../util/dataColumns.js"; | ||
| import {BeaconChain} from "../chain.js"; | ||
| import {IBlockInput, isBlockInputBlobs, isBlockInputColumns} from "./blockInput/index.js"; | ||
| import { | ||
| IBlockInput, | ||
| IDataColumnsInput, | ||
| isBlockInputBlobs, | ||
| isBlockInputColumns, | ||
| isBlockInputNoData, | ||
| } from "./blockInput/index.js"; | ||
| import {BLOB_AVAILABILITY_TIMEOUT} from "./verifyBlocksDataAvailability.js"; | ||
|
|
||
| /** | ||
|
|
@@ -10,129 +18,139 @@ import {BLOB_AVAILABILITY_TIMEOUT} from "./verifyBlocksDataAvailability.js"; | |
| * | ||
| * This operation may be performed before, during or after importing to the fork-choice. As long as errors | ||
| * are handled properly for eventual consistency. | ||
| * | ||
| * Block+blobs (pre-fulu) and data columns (fulu+) are written in parallel. | ||
| */ | ||
| export async function writeBlockInputToDb(this: BeaconChain, blocksInputs: IBlockInput[]): Promise<void> { | ||
| const fnPromises: Promise<void>[] = []; | ||
| // track slots for logging | ||
| const slots: number[] = []; | ||
|
|
||
| for (const blockInput of blocksInputs) { | ||
| const block = blockInput.getBlock(); | ||
| const slot = block.message.slot; | ||
| slots.push(slot); | ||
| const blockRoot = this.config.getForkTypes(block.message.slot).BeaconBlock.hashTreeRoot(block.message); | ||
| const blockRootHex = toRootHex(blockRoot); | ||
| const blockBytes = this.serializedCache.get(block); | ||
| if (blockBytes) { | ||
| // skip serializing data if we already have it | ||
| this.metrics?.importBlock.persistBlockWithSerializedDataCount.inc(); | ||
| fnPromises.push(this.db.block.putBinary(this.db.block.getId(block), blockBytes)); | ||
| } else { | ||
| this.metrics?.importBlock.persistBlockNoSerializedDataCount.inc(); | ||
| fnPromises.push(this.db.block.add(block)); | ||
| } | ||
| export async function writeBlockInputToDb(this: BeaconChain, blockInput: IBlockInput): Promise<void> { | ||
| const promises: Promise<void>[] = [writeBlockAndBlobsToDb.call(this, blockInput)]; | ||
|
|
||
| this.logger.debug("Persist block to hot DB", { | ||
| slot: block.message.slot, | ||
| root: blockRootHex, | ||
| inputType: blockInput.type, | ||
| }); | ||
| if (isBlockInputColumns(blockInput)) { | ||
| promises.push(writeDataColumnsToDb.call(this, blockInput)); | ||
| } | ||
|
|
||
| if (!blockInput.hasAllData()) { | ||
| await blockInput.waitForAllData(BLOB_AVAILABILITY_TIMEOUT); | ||
| } | ||
| await Promise.all(promises); | ||
| this.logger.debug("Persisted blockInput to db", {slot: blockInput.slot, root: blockInput.blockRootHex}); | ||
| } | ||
|
|
||
| // NOTE: Old data is pruned on archive | ||
| if (isBlockInputColumns(blockInput)) { | ||
| if (!blockInput.hasComputedAllData()) { | ||
| // Supernodes may only have a subset of the data columns by the time the block begins to be imported | ||
| // because full data availability can be assumed after NUMBER_OF_COLUMNS / 2 columns are available. | ||
| // Here, however, all data columns must be fully available/reconstructed before persisting to the DB. | ||
| await blockInput.waitForComputedAllData(BLOB_AVAILABILITY_TIMEOUT).catch(() => { | ||
| this.logger.debug("Failed to wait for computed all data", {slot, blockRoot: blockRootHex}); | ||
| }); | ||
| } | ||
| async function writeBlockAndBlobsToDb(this: BeaconChain, blockInput: IBlockInput): Promise<void> { | ||
| const block = blockInput.getBlock(); | ||
| const slot = block.message.slot; | ||
| const blockRoot = this.config.getForkTypes(slot).BeaconBlock.hashTreeRoot(block.message); | ||
| const blockRootHex = toRootHex(blockRoot); | ||
| const numBlobs = isForkPostDeneb(blockInput.forkName) | ||
| ? getBlobKzgCommitments(blockInput.forkName, block as SignedBeaconBlock<ForkPostDeneb>).length | ||
| : undefined; | ||
| const fnPromises: Promise<void>[] = []; | ||
|
|
||
| const {custodyColumns} = this.custodyConfig; | ||
| const blobsLen = (block.message as fulu.BeaconBlock).body.blobKzgCommitments.length; | ||
| let dataColumnsLen: number; | ||
| if (blobsLen === 0) { | ||
| dataColumnsLen = 0; | ||
| } else { | ||
| dataColumnsLen = custodyColumns.length; | ||
| } | ||
| const blockBytes = this.serializedCache.get(block); | ||
| if (blockBytes) { | ||
| // skip serializing data if we already have it | ||
| this.metrics?.importBlock.persistBlockWithSerializedDataCount.inc(); | ||
| fnPromises.push(this.db.block.putBinary(this.db.block.getId(block), blockBytes)); | ||
| } else { | ||
| this.metrics?.importBlock.persistBlockNoSerializedDataCount.inc(); | ||
| fnPromises.push(this.db.block.add(block)); | ||
| } | ||
|
|
||
| const dataColumnSidecars = blockInput.getCustodyColumns(); | ||
| if (dataColumnSidecars.length !== dataColumnsLen) { | ||
| this.logger.debug( | ||
| `Invalid dataColumnSidecars=${dataColumnSidecars.length} for custody expected custodyColumnsLen=${dataColumnsLen}` | ||
| ); | ||
| } | ||
| this.logger.debug("Persist block to hot DB", {slot, root: blockRootHex, inputType: blockInput.type, numBlobs}); | ||
|
|
||
| const binaryPuts = []; | ||
| const nonbinaryPuts = []; | ||
| for (const dataColumnSidecar of dataColumnSidecars) { | ||
| // skip reserializing column if we already have it | ||
| const serialized = this.serializedCache.get(dataColumnSidecar); | ||
| if (serialized) { | ||
| binaryPuts.push({key: dataColumnSidecar.index, value: serialized}); | ||
| } else { | ||
| nonbinaryPuts.push(dataColumnSidecar); | ||
| if (isBlockInputBlobs(blockInput)) { | ||
| fnPromises.push( | ||
| (async () => { | ||
| if (!blockInput.hasAllData()) { | ||
| await blockInput.waitForAllData(BLOB_AVAILABILITY_TIMEOUT); | ||
| } | ||
| } | ||
| fnPromises.push(this.db.dataColumnSidecar.putManyBinary(blockRoot, binaryPuts)); | ||
| fnPromises.push(this.db.dataColumnSidecar.putMany(blockRoot, nonbinaryPuts)); | ||
| this.logger.debug("Persisted dataColumnSidecars to hot DB", { | ||
| slot: block.message.slot, | ||
| root: blockRootHex, | ||
| dataColumnSidecars: dataColumnSidecars.length, | ||
| numBlobs: blobsLen, | ||
| custodyColumns: custodyColumns.length, | ||
| }); | ||
| } else if (isBlockInputBlobs(blockInput)) { | ||
| const blobSidecars = blockInput.getBlobs(); | ||
| fnPromises.push(this.db.blobSidecars.add({blockRoot, slot: block.message.slot, blobSidecars})); | ||
| this.logger.debug("Persisted blobSidecars to hot DB", { | ||
| blobsLen: blobSidecars.length, | ||
| slot: block.message.slot, | ||
| root: blockRootHex, | ||
| }); | ||
| } | ||
| const blobSidecars = blockInput.getBlobs(); | ||
| await this.db.blobSidecars.add({blockRoot, slot, blobSidecars}); | ||
| this.logger.debug("Persisted blobSidecars to hot DB", { | ||
| slot, | ||
| root: blockRootHex, | ||
| numBlobs: blobSidecars.length, | ||
| }); | ||
| })() | ||
| ); | ||
| } | ||
|
|
||
| await Promise.all(fnPromises); | ||
| this.logger.debug("Persisted blocksInput to db", { | ||
| blocksInput: blocksInputs.length, | ||
| slots: prettyPrintIndices(slots), | ||
| await Promise.all(fnPromises); | ||
| } | ||
|
|
||
| /** | ||
|
twoeths marked this conversation as resolved.
|
||
| * Persists data columns to DB for a given block. Accepts a narrow sub-interface of IBlockInput | ||
| * so it can be reused across forks (e.g. Fulu, Gloas). | ||
| * | ||
| * NOTE: Old data is pruned on archive. | ||
| */ | ||
| export async function writeDataColumnsToDb(this: BeaconChain, blockInput: IDataColumnsInput): Promise<void> { | ||
| const {slot, blockRootHex} = blockInput; | ||
| const blockRoot = fromHex(blockRootHex); | ||
|
|
||
| if (!blockInput.hasComputedAllData()) { | ||
| // Supernodes may only have a subset of the data columns by the time the block begins to be imported | ||
| // because full data availability can be assumed after NUMBER_OF_COLUMNS / 2 columns are available. | ||
| // Here, however, all data columns must be fully available/reconstructed before persisting to the DB. | ||
| await blockInput.waitForComputedAllData(BLOB_AVAILABILITY_TIMEOUT).catch(() => { | ||
| this.logger.debug("Failed to wait for computed all data", {slot, blockRoot: blockRootHex}); | ||
| }); | ||
| } | ||
|
|
||
| const {custodyColumns} = this.custodyConfig; | ||
| const dataColumnSidecars = blockInput.getCustodyColumns(); | ||
|
|
||
| const binaryPuts: {key: number; value: Uint8Array}[] = []; | ||
| const nonbinaryPuts = []; | ||
| for (const dataColumnSidecar of dataColumnSidecars) { | ||
| // skip reserializing column if we already have it | ||
| const serialized = this.serializedCache.get(dataColumnSidecar); | ||
| if (serialized) { | ||
| binaryPuts.push({key: dataColumnSidecar.index, value: serialized}); | ||
| } else { | ||
| nonbinaryPuts.push(dataColumnSidecar); | ||
| } | ||
| } | ||
|
|
||
| await Promise.all([ | ||
| this.db.dataColumnSidecar.putManyBinary(blockRoot, binaryPuts), | ||
| this.db.dataColumnSidecar.putMany(blockRoot, nonbinaryPuts), | ||
| ]); | ||
|
|
||
| this.logger.debug("Persisted dataColumnSidecars to hot DB", { | ||
| slot, | ||
| root: blockRootHex, | ||
| dataColumnSidecars: dataColumnSidecars.length, | ||
| custodyColumns: custodyColumns.length, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes it's helpful for debug, will get it from the 1st DataColumnSidecar
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I also added |
||
| numBlobs: dataColumnSidecars[0]?.column.length, | ||
| }); | ||
| } | ||
|
|
||
| export async function persistBlockInputs(this: BeaconChain, blockInputs: IBlockInput[]): Promise<void> { | ||
| export async function persistBlockInput(this: BeaconChain, blockInput: IBlockInput): Promise<void> { | ||
| await writeBlockInputToDb | ||
| .call(this, blockInputs) | ||
| .call(this, blockInput) | ||
| .catch((e) => { | ||
| this.logger.debug( | ||
| "Error persisting block input in hot db", | ||
| { | ||
| count: blockInputs.length, | ||
| slot: blockInputs[0].slot, | ||
| root: blockInputs[0].blockRootHex, | ||
| slot: blockInput.slot, | ||
| root: blockInput.blockRootHex, | ||
|
ensi321 marked this conversation as resolved.
|
||
| }, | ||
| e | ||
| ); | ||
| }) | ||
| .finally(() => { | ||
| for (const blockInput of blockInputs) { | ||
| this.seenBlockInputCache.prune(blockInput.blockRootHex); | ||
| } | ||
| this.seenBlockInputCache.prune(blockInput.blockRootHex); | ||
| // Without forcefully clearing this cache, we would rely on WeakMap to evict memory which is not reliable. | ||
| // Clear here (after the DB write) so that writeBlockInputToDb can still use the cached serialized bytes. | ||
| this.serializedCache.clear(); | ||
| if (blockInputs.length === 1) { | ||
| this.logger.debug("Pruned block input", { | ||
| slot: blockInputs[0].slot, | ||
| root: blockInputs[0].blockRootHex, | ||
| }); | ||
| // | ||
| // For Gloas (BlockInputNoData), the execution payload and columns arrive separately after the beacon block. | ||
| // Do NOT clear the cache here — it must remain available for writeDataColumnsToDb when the payload arrives. | ||
| // The cache is cleared in the Gloas payload persistence path instead. | ||
| if (!isBlockInputNoData(blockInput)) { | ||
| // TODO: enhance this SerializedCache for Gloas because payload may not come | ||
| // see https://github.com/ChainSafe/lodestar/pull/8974#discussion_r2885598229 | ||
| this.serializedCache.clear(); | ||
|
Comment on lines
+146
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On post-gloas blocks ( Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for gloas that should be called in #8962 |
||
| } | ||
| this.logger.debug("Pruned block input", { | ||
| slot: blockInput.slot, | ||
| root: blockInput.blockRootHex, | ||
| }); | ||
| }); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.