|
1 | 1 | # Indexer Core |
2 | 2 |
|
3 | | -This library provides the core functionality for building an indexer for VechainThor. It supports running multiple indexers concurrently with automatic dependency management and parallel processing. |
| 3 | +`indexer-core` provides the core building blocks for VeChainThor indexers. It supports fast log-based catch-up, full block processing when required, ABI and business-event decoding, dependency-aware execution, and rollback-safe reprocessing after reorgs. |
4 | 4 |
|
5 | | -## Features |
| 5 | +## Requirements |
6 | 6 |
|
7 | | -- **Parallel Processing**: Multiple indexers process blocks concurrently for maximum performance |
8 | | -- **Dependency Management**: Automatically orders indexers based on their dependencies using topological sorting |
9 | | -- **Retry Logic**: Built-in retry mechanisms for initialization, sync, and block processing |
10 | | -- **Block Buffering**: Configurable buffering to optimize throughput |
11 | | -- **Group Coordination**: Indexers with dependencies are organized into processing groups that maintain proper ordering |
| 7 | +- Java 21 |
| 8 | +- Access to a Thor API endpoint |
| 9 | +- A persistence layer that can store the last synced block and roll back on reorgs |
12 | 10 |
|
13 | | -## Example usage |
| 11 | +## Installation |
| 12 | + |
| 13 | +Gradle Kotlin DSL: |
14 | 14 |
|
15 | 15 | ```kotlin |
16 | | -@Configuration |
17 | | -open class IndexerConfig() { |
18 | | - |
19 | | - @Bean |
20 | | - open fun myPruner(): Pruner = PrunerService() |
21 | | - |
22 | | - @Bean |
23 | | - open fun blockIndexer(myPruner: Pruner): Indexer = |
24 | | - IndexerFactory() |
25 | | - .name("BlockIndexer") |
26 | | - .thorClient("https://mainnet.vechain.org", Pair("X-Project-Id", "my-indexer")) |
27 | | - .processor(blockProcessor) |
28 | | - .pruner(myPruner) |
29 | | - .startBlock(0) |
30 | | - .syncLoggerInterval(1000) |
31 | | - .abis("/abis") |
32 | | - .businessEvents("/business-events", "/abis") |
33 | | - .includeVetTransfers() |
34 | | - .build() |
35 | | - |
36 | | - @Bean |
37 | | - open fun transactionIndexer(blockIndexer: Indexer): Indexer = |
38 | | - IndexerFactory() |
39 | | - .name("TransactionIndexer") |
40 | | - .thorClient("https://mainnet.vechain.org") |
41 | | - .processor(txProcessor) |
42 | | - .dependsOn(blockIndexer) // Ensures BlockIndexer processes blocks first |
43 | | - .startBlock(0) |
44 | | - .build() |
45 | | - |
46 | | - @Bean |
47 | | - open fun indexerRunner( |
48 | | - scope: CoroutineScope, |
49 | | - thorClient: ThorClient, |
50 | | - blockIndexer: Indexer, |
51 | | - transactionIndexer: Indexer |
52 | | - ): Job { |
53 | | - return IndexerRunner.launch( |
54 | | - scope = scope, |
55 | | - thorClient = thorClient, |
56 | | - indexers = listOf(blockIndexer, transactionIndexer), |
57 | | - blockBatchSize = 10 // Buffer up to 10 blocks per group |
58 | | - ) |
59 | | - } |
| 16 | +dependencies { |
| 17 | + implementation("org.vechain:indexer-core:8.0.3") |
60 | 18 | } |
61 | 19 | ``` |
62 | 20 |
|
63 | | -The `IndexerFactory` can be used to configure your indexer. The only required parameters are the `name`, `thorClient` and the `processor`. |
64 | | -For details of the available configuration options, see the comments in the `IndexerFactory` class. |
65 | | - |
| 21 | +Gradle Groovy DSL: |
66 | 22 |
|
67 | | -## IndexerProcessor Implementation |
| 23 | +```groovy |
| 24 | +dependencies { |
| 25 | + implementation 'org.vechain:indexer-core:8.0.3' |
| 26 | +} |
| 27 | +``` |
68 | 28 |
|
69 | | -An example of an `IndexerProcessor` implementation: |
| 29 | +## Quick Start |
70 | 30 |
|
71 | 31 | ```kotlin |
72 | | -@Component |
73 | | -open class MyProcessor( |
74 | | - private val myService: Service, |
| 32 | +class MyProcessor( |
| 33 | + private val repository: MyRepository, |
75 | 34 | ) : IndexerProcessor { |
76 | | - override fun process(entry: IndexingResult) { |
77 | | - if (entry.events().isEmpty()) { |
78 | | - return |
79 | | - } |
80 | | - myService.processEvents(entry.events()) |
81 | | - } |
| 35 | + override fun getLastSyncedBlock(): BlockIdentifier? = repository.getLastSyncedBlock() |
82 | 36 |
|
83 | 37 | override fun rollback(blockNumber: Long) { |
84 | | - myService.rollback(blockNumber) |
| 38 | + repository.rollbackFrom(blockNumber) |
85 | 39 | } |
86 | 40 |
|
87 | | - override fun getLastSyncedBlock(): BlockIdentifier? { |
88 | | - myService.getLatestRecord()?.let { |
89 | | - return BlockIdentifier(number = it.blockNumber, id = it.blockId) |
90 | | - } |
91 | | - logger.info("No records found in repository, returning null") |
92 | | - return null |
| 41 | + override suspend fun process(entry: IndexingResult) { |
| 42 | + when (entry) { |
| 43 | + is IndexingResult.LogResult -> repository.storeEvents(entry.endBlock, entry.events) |
| 44 | + is IndexingResult.BlockResult -> |
| 45 | + repository.storeBlock(entry.block, entry.events, entry.callResults) |
| 46 | + } |
93 | 47 | } |
94 | 48 | } |
95 | | -``` |
96 | 49 |
|
97 | | -## Architecture |
| 50 | +val thorClient = DefaultThorClient("https://mainnet.vechain.org") |
| 51 | + |
| 52 | +val indexer = |
| 53 | + IndexerFactory() |
| 54 | + .name("example-indexer") |
| 55 | + .thorClient(thorClient) |
| 56 | + .processor(MyProcessor(repository)) |
| 57 | + .startBlock(19_000_000) |
| 58 | + .build() |
| 59 | + |
| 60 | +val job = |
| 61 | + IndexerRunner.launch( |
| 62 | + scope = scope, |
| 63 | + thorClient = thorClient, |
| 64 | + indexers = listOf(indexer), |
| 65 | + ) |
| 66 | +``` |
98 | 67 |
|
99 | | -### IndexerRunner |
| 68 | +## Choosing a Mode |
100 | 69 |
|
101 | | -The `IndexerRunner` coordinates multiple indexers: |
| 70 | +- Default `LogsIndexer`: best when you want the fastest catch-up path and only need decoded events or transfers. |
| 71 | +- `includeFullBlock()`: use when you need full block contents, reverted transactions, gas metadata, or clause inspection results. |
| 72 | +- `dependsOn(...)`: use when one indexer must finish a block before another processes that same block. |
102 | 73 |
|
103 | | -1. **Initialization Phase**: All indexers are initialized and fast-synced concurrently |
104 | | -2. **Execution Phase**: Indexers are organized into dependency groups and process blocks in parallel |
105 | | - - Groups are determined by topological sorting based on `dependsOn` relationships |
106 | | - - Within each group, indexers process the same block **sequentially** in list order |
107 | | - - Different groups can work on different blocks simultaneously (e.g., Group 2 on block N+1 while Group 1 on block N+2) |
| 74 | +Processors should handle both `IndexingResult.LogResult` and `IndexingResult.BlockResult`. |
108 | 75 |
|
109 | | -### Dependency Management |
| 76 | +## Documentation |
110 | 77 |
|
111 | | -The `IndexerOrderUtils` provides topological sorting to determine the correct processing order: |
| 78 | +The detailed documentation lives in [`docs/README.md`](docs/README.md) and is the canonical source of truth for library behavior. |
112 | 79 |
|
113 | | -```kotlin |
114 | | -val indexer1 = createIndexer("Base") |
115 | | -val indexer2 = createIndexer("DependentA", dependsOn = indexer1) |
116 | | -val indexer3 = createIndexer("DependentB", dependsOn = indexer1) |
117 | | -val indexer4 = createIndexer("FinalIndexer", dependsOn = indexer2) |
| 80 | +- [`docs/README.md`](docs/README.md): documentation index and navigation |
| 81 | +- [`docs/IndexerOverview.md`](docs/IndexerOverview.md): runtime model, lifecycle, runner behavior |
| 82 | +- [`docs/LogsIndexerOverview.md`](docs/LogsIndexerOverview.md): log-based indexing and fast sync |
| 83 | +- [`docs/EventsAndABIHandling.md`](docs/EventsAndABIHandling.md): ABI loading and event decoding |
| 84 | +- [`docs/BusinessEvents.md`](docs/BusinessEvents.md): business event definitions and matching |
| 85 | +- [`docs/MIGRATION-8.0.0.md`](docs/MIGRATION-8.0.0.md): 7.x to 8.x migration guide |
118 | 86 |
|
119 | | -// Results in groups: [[Base], [DependentA, DependentB], [FinalIndexer]] |
120 | | -// DependentA and DependentB can process blocks in parallel |
121 | | -``` |
| 87 | +## Documentation Model |
122 | 88 |
|
123 | | -## Java compatibility |
| 89 | +To reduce drift: |
124 | 90 |
|
125 | | -When using the indexer-core package in a Java project, prefer using `startInCoroutine()` to start your indexer implementation, |
126 | | -as the regular `start()` is a suspend function for which the caller has to supply a coroutine scope. |
127 | | -Otherwise, the package is Java compatible as is. |
| 91 | +- keep `README.md` short and focused on overview plus quick start |
| 92 | +- keep detailed technical docs in `docs/` |
| 93 | +- treat the repo docs as the canonical source |
| 94 | +- use Confluence as a landing page that links to the repo docs instead of duplicating them manually |
0 commit comments