Skip to content

Fix inconsistent processed events with many dynamic contracts - #399

Merged
DZakh merged 5 commits into
mainfrom
dz/flat-registers
Dec 25, 2024
Merged

DZakh merged 5 commits into
mainfrom
dz/flat-registers

Conversation

@DZakh

@DZakh DZakh commented Dec 24, 2024

Copy link
Copy Markdown
Member

Terminology:

  • FetchState/Partition - This is a chain fetching state split by the max partition contracts limit
  • Register - A subset of a FetchState. If there's a dynamic contract, we create a register for it with a specific startBlock.

We have two fetching modes:

  1. When there are registered dynamic contracts, we fetch their contracts up till the latest fetch block
  2. When all registers catch up to the latest one, they are merged, and we start fetching all the addresses together without a hard endBlock

There were two main issues:

  • We incorrectly created a new FetchState when the previous one exceeded the limit. The newly created register used to start fetching from the chain startBlock instead of from the dynamic contract registering event block. It caused inconsistently getting events before a dynamic contract registration block.
  • When a new FetchState was created, there was another issue, that it could easily go into an incorrect state, when the next dynamic contract would create a register with a latestFetchBlock higher than the Root register. And since we used to store registers in the list-like structure ordered by latestFetchBlock, it was very challenging to operate with.

To fix the second issue, I've change the ordered list-like structure for registers to an independent array, simplifying operations with them, and allowing to addition new dynamic registrations after the latestFetchBlock. Also, this will simplify the further improvement to fetch the dynamic contract registers in parallel. Currently, the fetch requests to catch up to the latest register are done sequentially.

@DZakh
DZakh requested review from JasoonS and JonoPrest December 24, 2024 13:22
let newPartition = FetchState.make(
~partitionId=partitions->Array.length,
~startBlock,
~startBlock=dynamicContractRegistration.registeringEventBlockNumber,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is a fix for the first issue. I had to fix the second one with a huge refactoring to be able to reproduce and find the little first one 😁

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah man, I had a feeling there was something going wrong when creating new partitions. Good catch

Comment on lines -491 to -511
if register.latestFetchedBlock.blockNumber > nextRegister.latestFetchedBlock.blockNumber {
let logger = Logging.createChild(
~params={
"context": "Merging Dynamic Contract Registers",
"currentRegister": {
"id": register->getRegisterId,
"latestFetchedBlock": register.latestFetchedBlock.blockNumber,
"addresses": register.contractAddressMapping->ContractAddressingMap.getAllAddresses,
},
"nextRegister": {
"id": nextRegister->getRegisterId,
"latestFetchedBlock": nextRegister.latestFetchedBlock.blockNumber,
"addresses": nextRegister.registerType->isRootRegister
? "Root"->Utils.magic
: nextRegister.contractAddressMapping->ContractAddressingMap.getAllAddresses,
},
},
)
NextRegisterIsLessThanCurrent->ErrorHandling.mkLogAndRaise(
~msg="Unexpected: Dynamic contract register latest fetched block is greater than next register when it should be equal",
~logger,

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is not possible anymore

Comment on lines +753 to +776
let rollbackRegister = (
register: register,
~lastScannedBlock,
~firstChangeEvent: blockNumberAndLogIndex,
~parent: option<Parent.t>=?,
) => {
let handleParent = updated =>
switch parent {
| Some(parent) => parent->Parent.joinChild(updated)
| None => updated
}

switch self.registerType {
//Case 1 Root register that has only fetched up to a confirmed valid block number
//Should just return itself unchanged
| RootRegister if self.latestFetchedBlock.blockNumber < firstChangeEvent.blockNumber =>
self->handleParent
//Case 2 Dynamic register that has only fetched up to a confirmed valid block number
//Should just return itself, with the next register rolled back recursively
| DynamicContractRegister({id, nextRegister})
if self.latestFetchedBlock.blockNumber < firstChangeEvent.blockNumber =>
nextRegister->rollbackRegister(
~lastScannedBlock,
~firstChangeEvent,
~parent=self->Parent.make(~dynamicContractId=id, ~parent),
)

//Case 3 Root register that has fetched further than the confirmed valid block number
//Should prune its queue and set its latest fetched block data to the latest known confirmed block
| RootRegister =>
{
...self,
fetchedEventQueue: self.fetchedEventQueue->pruneQueueFromFirstChangeEvent(~firstChangeEvent),
latestFetchedBlock: lastScannedBlock,
}
->pruneDynamicContractAddressesFromFirstChangeEvent(~firstChangeEvent)
->handleParent
//Case 4 DynamicContract register that has fetched further than the confirmed valid block number
//Should prune its queue, set its latest fetched blockdata + pruned queue
//And recursivle prune the nextRegister
| DynamicContractRegister({id, nextRegister}) =>
if register.latestFetchedBlock.blockNumber < firstChangeEvent.blockNumber {
Some(register)
} else {
let updatedWithRemovedDynamicContracts =
self->pruneDynamicContractAddressesFromFirstChangeEvent(~firstChangeEvent)

register->pruneDynamicContractAddressesFromFirstChangeEvent(~firstChangeEvent)
if updatedWithRemovedDynamicContracts.contractAddressMapping->ContractAddressingMap.isEmpty {
//If the contractAddressMapping is empty after pruning dynamic contracts, then this
//is a dead register. Simly return its next register rolled back
nextRegister->rollbackRegister(~lastScannedBlock, ~firstChangeEvent, ~parent?)
//If the contractAddressMapping is empty after pruning dynamic contracts,
// then this is a dead register.
None
} else {
//If there are still values in the contractAddressMapping, we should keep the register but
//prune queues and next register
let updated = {
//If there are still values in the contractAddressMapping,
//we should keep the register but prune queues
Some({
...updatedWithRemovedDynamicContracts,
fetchedEventQueue: self.fetchedEventQueue->pruneQueueFromFirstChangeEvent(
fetchedEventQueue: register.fetchedEventQueue->pruneQueueFromFirstChangeEvent(
~firstChangeEvent,
),
latestFetchedBlock: lastScannedBlock,
}
nextRegister->rollbackRegister(
~lastScannedBlock,
~firstChangeEvent,
~parent=updated->Parent.make(~dynamicContractId=id, ~parent),
)
})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This is absolutely the same, but it is so much simpler without a recursion.

partitionId: 1,
baseRegister: {
registerType: RootRegister,
latestFetchedBlock: {blockNumber: 0, blockTimestamp: 0},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Here's the wrong test for the first issue. After the fix it became:

latestFetchedBlock: {blockNumber: 9, blockTimestamp: 0},

@DZakh
DZakh merged commit 93883ce into main Dec 25, 2024
@DZakh
DZakh deleted the dz/flat-registers branch December 25, 2024 09:42
Comment on lines +152 to +160
// Must use the reference to copied value, so we use find
mostBehindRegister: registers
->Js.Array2.find(r => r.id == self.mostBehindRegister.id)
->Option.getExn,
nextMostBehindRegister: switch self.nextMostBehindRegister {
| Some(nextMostBehindRegister) =>
registers->Js.Array2.find(r => r.id == nextMostBehindRegister.id)->Option.getExn->Some
| None => None
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just a thought, maybe worth using an array index here rather than an obj reference? Not sure of the performance hit though.

Looks something like an arena/index tree data structure.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think a reference should be faster, since we don't need to get it every time we need it. Regarding memory, it should be the same. In any way the difference is neglectable.

Also, find looks dirty here, but I've decided to keep it as it is, since I was thinking to get rid of mostBehindRegister and nextMostBehindRegister when I turn registers into partitions.

Comment on lines +277 to +279
~registers=fetchState.registers,
~pendingDynamicContracts=fetchState.pendingDynamicContracts,
~isFetchingAtHead=fetchState.isFetchingAtHead,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Woah, I didn't know you could default to another param in rescript 😅

Comment on lines +514 to +523
let item = ref(registers->Js.Array2.unsafe_get(0)->getEarliestEventInRegister)
for idx in 1 to registers->Js.Array2.length - 1 {
let register = registers->Js.Array2.unsafe_get(idx)
let registerItem = register->getEarliestEventInRegister
if registerItem->qItemLt(item.contents) {
item := registerItem
}
}
item.contents
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this much faster than reduce?

If so looks like we should just make this into our own reduce util?

@DZakh DZakh Dec 27, 2024

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I suppose the speed difference with our number of registers is neglectable, but I think regarding the indexer code we should strive for the better, especially when it doesn't cost anything.

I'm not really into the reduce util. There's only one pro in using reduce - readability, but this is a very arguable and I find the go-like style code equally readable, especially when it's scoped inside of a small function covered with tests.

@JonoPrest JonoPrest Dec 27, 2024

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Well there's a number of places this pattern is used. If it's faster we should make a util for it try convert all other reduce use cases to this IMO.

It's not really readability since as you say procedural code often reads easier, rather composability/reusability and safety from the off by ones/bad index accessing etc. Those bugs can creep in from time to time.

| RootRegister => accum
| DynamicContractRegister({nextRegister}) => nextRegister->getRegisterAddressMaps(~accum)
}
// FIXME: Should include pending contracts?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Yes it definitely should!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I didn't fix it since I planned to change the way we split partitions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants