11package org.vechain.indexer
22
3+ import kotlin.time.Duration
4+ import kotlin.time.Duration.Companion.minutes
35import kotlin.time.TimeMark
46import kotlin.time.TimeSource
57import kotlinx.coroutines.CoroutineScope
@@ -17,6 +19,7 @@ import org.vechain.indexer.exception.ReorgException
1719import org.vechain.indexer.thor.client.ThorClient
1820import org.vechain.indexer.utils.ClauseIndexMapping
1921import org.vechain.indexer.utils.ClauseUtils.buildClauseListWithMapping
22+ import org.vechain.indexer.utils.IndexerOrderUtils.proximityGroups
2023import org.vechain.indexer.utils.IndexerOrderUtils.topologicalOrder
2124import org.vechain.indexer.utils.retryOnFailure
2225
@@ -28,7 +31,9 @@ class IndexerRunner(private val timeSource: TimeSource = TimeSource.Monotonic) {
2831 scope : CoroutineScope ,
2932 thorClient : ThorClient ,
3033 indexers : List <Indexer >,
31- blockBatchSize : Int = 1
34+ blockBatchSize : Int = 1,
35+ proximityThreshold : Long = 1_000_000L,
36+ reshuffleInterval : Duration = 5.minutes,
3237 ): Job {
3338 require(indexers.isNotEmpty()) { " At least one indexer is required" }
3439
@@ -39,6 +44,8 @@ class IndexerRunner(private val timeSource: TimeSource = TimeSource.Monotonic) {
3944 indexers = indexers,
4045 batchSize = blockBatchSize,
4146 thorClient = thorClient,
47+ proximityThreshold = proximityThreshold,
48+ reshuffleInterval = reshuffleInterval,
4249 )
4350 }
4451 }
@@ -48,6 +55,8 @@ class IndexerRunner(private val timeSource: TimeSource = TimeSource.Monotonic) {
4855 indexers : List <Indexer >,
4956 batchSize : Int ,
5057 thorClient : ThorClient ,
58+ proximityThreshold : Long = 1_000_000L,
59+ reshuffleInterval : Duration = 5.minutes,
5160 ): Unit = coroutineScope {
5261 require(indexers.isNotEmpty()) { " At least one indexer is required" }
5362
@@ -63,11 +72,19 @@ class IndexerRunner(private val timeSource: TimeSource = TimeSource.Monotonic) {
6372 nonFastSyncable,
6473 thorClient,
6574 batchSize,
75+ proximityThreshold,
76+ reshuffleInterval,
6677 )
6778 // Re-initialise non-fast-syncable indexers to recover from potential
6879 // mid-block cancellation during the intermediate run
6980 initialise(nonFastSyncable)
70- runIndexers(indexers, thorClient, batchSize)
81+ runWithProximityGroups(
82+ indexers,
83+ thorClient,
84+ batchSize,
85+ proximityThreshold,
86+ reshuffleInterval,
87+ )
7188 } catch (e: ReorgException ) {
7289 logger.error(" Reorg detected, restarting all indexers" , e)
7390 // Exception caught, job will complete normally and loop will restart
@@ -85,6 +102,8 @@ class IndexerRunner(private val timeSource: TimeSource = TimeSource.Monotonic) {
85102 nonFastSyncable : List <Indexer >,
86103 thorClient : ThorClient ,
87104 batchSize : Int ,
105+ proximityThreshold : Long = 1_000_000L,
106+ reshuffleInterval : Duration = 5.minutes,
88107 ) {
89108 if (fastSyncable.isEmpty()) {
90109 initialise(nonFastSyncable)
@@ -100,7 +119,15 @@ class IndexerRunner(private val timeSource: TimeSource = TimeSource.Monotonic) {
100119 val nonFastJob: Job ? =
101120 if (independentNonFast.isNotEmpty()) {
102121 initialise(independentNonFast)
103- launch { runIndexers(independentNonFast, thorClient, batchSize) }
122+ launch {
123+ runWithProximityGroups(
124+ independentNonFast,
125+ thorClient,
126+ batchSize,
127+ proximityThreshold,
128+ reshuffleInterval,
129+ )
130+ }
104131 } else null
105132
106133 initialiseAndSync(fastSyncable)
@@ -149,6 +176,44 @@ class IndexerRunner(private val timeSource: TimeSource = TimeSource.Monotonic) {
149176 }
150177 }
151178
179+ suspend fun runWithProximityGroups (
180+ indexers : List <Indexer >,
181+ thorClient : ThorClient ,
182+ batchSize : Int ,
183+ proximityThreshold : Long ,
184+ reshuffleInterval : Duration ,
185+ ) {
186+ while (true ) {
187+ val groups = proximityGroups(indexers, proximityThreshold)
188+ if (groups.size <= 1 ) {
189+ // Steady state — single group, no deadline
190+ runIndexers(indexers, thorClient, batchSize)
191+ return
192+ }
193+
194+ val groupSummary = buildString {
195+ appendLine(
196+ " Proximity groups: ${groups.size} groups, ${indexers.size} indexers, threshold=$proximityThreshold "
197+ )
198+ groups.forEachIndexed { i, g ->
199+ val blockRange =
200+ " ${g.minOf { it.getCurrentBlockNumber() }} ..${g.maxOf { it.getCurrentBlockNumber() }} "
201+ appendLine(
202+ " Group ${i + 1 } (${g.size} indexers, blocks $blockRange ): ${g.map { it.name }} "
203+ )
204+ }
205+ }
206+ logger.info(groupSummary.trimEnd())
207+ val deadlineMark = timeSource.markNow() + reshuffleInterval
208+ coroutineScope {
209+ groups.forEach { group ->
210+ launch { runIndexers(group, thorClient, batchSize, deadlineMark) }
211+ }
212+ }
213+ // All groups completed naturally when deadline passed; loop to reshuffle
214+ }
215+ }
216+
152217 suspend fun runIndexers (
153218 indexers : List <Indexer >,
154219 thorClient : ThorClient ,
0 commit comments