Introduce mutable fetch state - #385
Conversation
| 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) | ||
| }) | ||
| }) | ||
| } |
There was a problem hiding this comment.
I've noticed that we never set minFromBlockLogIndex to something besides 0, so the filter was never used.
| let eventIndexSchema = S.object(s => { | ||
| blockNumber: s.field("blockNumber", S.int), | ||
| logIndex: s.field("logIndex", S.int), | ||
| }) |
| 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) | ||
| } | ||
| }) |
There was a problem hiding this comment.
The Array.reduce could actually affect peformance for indexers with many addresses. Optimised it a little bit.
| Logging.childTrace( | ||
| logger, | ||
| { | ||
| "msg": "adding contract address", | ||
| "contractName": contractName, | ||
| "address": address, | ||
| }, | ||
| ) |
There was a problem hiding this comment.
Should I keep the log? I don't see a big use in it
| /** | ||
| Instantiates a fetch state with root register | ||
| */ | ||
| let makeRoot = (~endBlock) => makeInternal(~registerType=RootRegister({endBlock: endBlock}), ...) |
There was a problem hiding this comment.
Combined makeInternal and makeRoot into a single make.
|
|
||
| /** | ||
| 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, |
There was a problem hiding this comment.
Moved this to SourceMap and covered with tests
| 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, | ||
| } |
There was a problem hiding this comment.
We'll be able to get rid of these, after the code start managing fetching itself instead of being called by global state module.
| let fetchBatch = async ( | ||
| sourceManger: t, | ||
| ~allPartitions: PartitionedFetchState.allPartitions, | ||
| ~currentBlockHeight, | ||
| ~executePartitionQuery, | ||
| ~waitForNewBlock, | ||
| ~onNewBlock, | ||
| ~maxPerChainQueueSize, | ||
| ~setMergedPartitions, | ||
| ~stateId, | ||
| ) => { |
There was a problem hiding this comment.
Even though it looks complicated, but now it has almost 100% test coverage, so I find it a drastic improvement.
| // DynamicContract | ||
| // fromBlock: 0 | ||
| // toBlock: 0 | ||
| await dispatchAllTasks() | ||
| // DynamicContract | ||
| // fromBlock: 0 | ||
| // toBlock: 3 | ||
| await dispatchAllTasks() | ||
| // DynamicContract | ||
| // fromBlock: 2 | ||
| // toBlock: 3 | ||
| await dispatchAllTasks() |
There was a problem hiding this comment.
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.
| 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, | ||
| ) |
There was a problem hiding this comment.
Redone the tests in SourceManager_test.res
| ~stateId, | ||
| ) => { | ||
| if stateId < sourceManager.currentStateId { | ||
| () |
There was a problem hiding this comment.
Maybe it's worth logging a trace or a warning for this case?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Out of interest does === vs == have any effect on strings?
There was a problem hiding this comment.
No, it's the same generated code, I just don't like using == both in Js and ReScript 😅
| | // For the case with currentBlockHeight=0 we should | ||
| // force getting the known chain block, even if there are no ready queries |
There was a problem hiding this comment.
Does this handle a chain with endblock?
There was a problem hiding this comment.
Yes. It filters out all completely fetched partitions
| } | ||
| true | ||
| } | ||
| let {chainConfig: {chainWorker}, logger, currentBlockHeight, fetchState} = chainFetcher |
There was a problem hiding this comment.
should probably rename fetchState to partitionedFetchState on the chain fetcher
JonoPrest
left a comment
There was a problem hiding this comment.
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
~5%up to~90%