Skip to content

Introduce mutable fetch state - #385

Merged
DZakh merged 13 commits into
mainfrom
dz/mutable-fetch-state
Dec 13, 2024
Merged

Introduce mutable fetch state#385
DZakh merged 13 commits into
mainfrom
dz/mutable-fetch-state

Conversation

@DZakh

@DZakh DZakh commented Dec 12, 2024

Copy link
Copy Markdown
Member
  • Fix a stale fetching partition on reorg
  • Improve test coverage of the core fetch execution logic from ~5% up to ~90%
  • Potentially fixes an issue with double fetching of the same partition, which could lead to a corrupted data (not proved)
  • Cleans up and removes unused code

@DZakh
DZakh requested a review from JonoPrest December 12, 2024 16:41
Comment on lines -73 to -99
let applyConditionalFunction = (value: 'a, condition: bool, callback: 'a => 'b) => {
condition ? callback(value) : value
}

let queryEventsWithCombinedFilter = async (
~contractInterfaceManager,
~fromBlock,
~toBlock,
~minFromBlockLogIndex=0,
~provider,
~logger: Pino.t,
): array<Ethers.log> => {
let combinedFilterRes = await makeCombinedEventFilterQuery(
~provider,
~contractInterfaceManager,
~fromBlock,
~toBlock,
~logger,
)

combinedFilterRes->applyConditionalFunction(minFromBlockLogIndex > 0, arrLogs => {
arrLogs->Belt.Array.keep(log => {
log.blockNumber > fromBlock ||
(log.blockNumber == fromBlock && log.logIndex >= minFromBlockLogIndex)
})
})
}

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've noticed that we never set minFromBlockLogIndex to something besides 0, so the filter was never used.

Comment on lines -31 to -34
let eventIndexSchema = S.object(s => {
blockNumber: s.field("blockNumber", S.int),
logIndex: s.field("logIndex", S.int),
})

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.

Remove unused

Comment on lines -532 to -572
let (displayAddr, restCount) = addressesAll->Array.reduce(([], 0), (
(currentDisplay, restCount),
addr,
) => {
if currentDisplay->Array.length < 3 {
(Array.concat(currentDisplay, [addr->Address.toString]), restCount)
} else {
(currentDisplay, restCount + 1)
}
})

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.

The Array.reduce could actually affect peformance for indexers with many addresses. Optimised it a little bit.

Comment on lines -799 to -837
Logging.childTrace(
logger,
{
"msg": "adding contract address",
"contractName": contractName,
"address": address,
},
)

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.

Should I keep the log? I don't see a big use in it

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.

No I don't think so.

Comment on lines -849 to -883
/**
Instantiates a fetch state with root register
*/
let makeRoot = (~endBlock) => makeInternal(~registerType=RootRegister({endBlock: endBlock}), ...)

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.

Combined makeInternal and makeRoot into a single make.

Comment on lines -182 to -192

/**
Retrieves an array of partitions that are most behind with a max number based on
the max number of queries with the context of the partitions currently fetching.

The array could be shorter than the max number of queries if the partitions are
at the max queue size.
*/
let getMostBehindPartitions = (
{partitions}: t,
~maxNumQueries,

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.

Moved this to SourceMap and covered with tests

Comment on lines +3 to +16
let toQueryId = (query: FetchState.nextQuery) => {
query.fetchStateRegisterId->FetchState.registerIdToString ++ "-" ++ query.fromBlock->Int.toString
}

// Can't simply store fetching partitions, since fetchBatch
// can be called with old chainFetchers after isFetching was set to false,
// but state isn't still updated with fetched data.
// This is temporary until all fetching logic moved to mutable state,
// but for now prevent fetching the same partition twice with the same query,
// using lastFetchedQueryId (aka idempotency key)
type partitionFetchingState = {
isFetching: bool,
lastFetchedQueryId?: string,
}

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.

We'll be able to get rid of these, after the code start managing fetching itself instead of being called by global state module.

Comment on lines +50 to +60
let fetchBatch = async (
sourceManger: t,
~allPartitions: PartitionedFetchState.allPartitions,
~currentBlockHeight,
~executePartitionQuery,
~waitForNewBlock,
~onNewBlock,
~maxPerChainQueueSize,
~setMergedPartitions,
~stateId,
) => {

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.

Even though it looks complicated, but now it has almost 100% test coverage, so I find it a drastic improvement.

Comment on lines +290 to 301
// DynamicContract
// fromBlock: 0
// toBlock: 0
await dispatchAllTasks()
// DynamicContract
// fromBlock: 0
// toBlock: 3
await dispatchAllTasks()
// DynamicContract
// fromBlock: 2
// toBlock: 3
await dispatchAllTasks()

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.

Previously all 3 queries of the same fetchState somehow managed to run during a single await dispatchAllTasks() call. Now it the run one by one, thanks to the mutable fetching partitions set.

Comment on lines -55 to -66
it(
"With multiple partitions always returns the most behind partitions up to the max concurrency level",
() => {
let maxNumQueries = 3
let partitionsCurrentlyFetching = Set.Int.empty

let mostBehindPartitions =
partitionedFetchState->PartitionedFetchState.getMostBehindPartitions(
~maxNumQueries,
~maxPerChainQueueSize=10,
~partitionsCurrentlyFetching,
)

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.

Redone the tests in SourceManager_test.res

@DZakh DZakh changed the title Dz/mutable fetch state Introduce mutable fetch state Dec 13, 2024
Comment thread codegenerator/cli/templates/static/codegen/src/eventFetching/ChainFetcher.res Outdated
~stateId,
) => {
if stateId < sourceManager.currentStateId {
()

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.

Maybe it's worth logging a trace or a warning for this case?

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 don't think that it'll be useful. The fetchBatch function currently might be called however times you want, and it won't be an issue. Also, I hope to get rid of the check at some point, when we move all the fetching state to the module.

switch allPartitionsFetchingState->Belt.Array.get(partitionId) {
// Deduplicate queries when fetchBatch is called after
// isFetching was set to false, but state isn't updated with fetched data
| Some({lastFetchedQueryId}) if lastFetchedQueryId === toQueryId(nextQuery) => 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.

Out of interest does === vs == have any effect on strings?

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.

No, it's the same generated code, I just don't like using == both in Js and ReScript 😅

Comment on lines +125 to +126
| // For the case with currentBlockHeight=0 we should
// force getting the known chain block, even if there are no ready queries

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.

Does this handle a chain with endblock?

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.

Yes. It filters out all completely fetched partitions

}
true
}
let {chainConfig: {chainWorker}, logger, currentBlockHeight, fetchState} = chainFetcher

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.

should probably rename fetchState to partitionedFetchState on the chain fetcher

@JonoPrest JonoPrest left a comment

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.

Hey @DZakh, from a high level it looks good. I don't have the time right now to pick apart everything and I haven't had a chance to review each of your tests.

I would suggest being cautious about releasing without running it on a known indexer but also looks like you've tested the parts thoroughly

@DZakh
DZakh merged commit 5605da1 into main Dec 13, 2024
@DZakh
DZakh deleted the dz/mutable-fetch-state branch December 13, 2024 16:15
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.

2 participants