Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions src/main/kotlin/org.vechain.indexer/utils/IndexerOrderUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,39 @@ internal object IndexerOrderUtils {

/**
* Orders indexers into groups based on their dependencies. Each group contains indexers that
* are ordered sequentially (dependencies before dependents). Groups can be processed in
* parallel with each other.
*
* The algorithm creates a single group that contains all indexers in topological order,
* ensuring that: 1. Dependencies always appear before their dependents 2. The order is stable
* and deterministic 3. Circular dependencies are detected and rejected
* are ordered sequentially (dependencies before dependents). Independent indexers (no shared
* dependency chain) are placed in separate groups so they can be processed in parallel.
*
* @param indexers The list of indexers to order
* @return A list containing a single group with all indexers in dependency order
* @return A list of groups, each containing indexers in dependency order
* @throws IllegalStateException if a circular dependency is detected
* @throws IllegalArgumentException if a dependency is not in the provided indexers list
*/
fun topologicalOrder(indexers: List<Indexer>): List<List<Indexer>> {
if (indexers.isEmpty()) return emptyList()

val ordered = dependencySort(indexers)
val components = connectedComponents(indexers)

// Group by component, preserving topological order from `ordered`
val groups = mutableMapOf<Indexer, MutableList<Indexer>>()
for (indexer in ordered) {
groups.getOrPut(components.getValue(indexer)) { mutableListOf() }.add(indexer)
}

return groups.values.toList()
}

/**
* Topologically sorts indexers so dependencies appear before dependents. Also validates that
* all dependencies exist in the provided list and detects circular dependencies.
*
* @param indexers The list of indexers to sort
* @return Indexers in dependency-first order
* @throws IllegalStateException if a circular dependency is detected
* @throws IllegalArgumentException if a dependency is not in the provided indexers list
*/
fun dependencySort(indexers: List<Indexer>): List<Indexer> {
val indexerSet = indexers.toSet()
val visitState = mutableMapOf<Indexer, VisitState>()
val ordered = mutableListOf<Indexer>()
Expand Down Expand Up @@ -50,11 +68,48 @@ internal object IndexerOrderUtils {
}
}

// Visit all indexers in the order they were provided
indexers.forEach { visit(it) }
return ordered
}

/**
* Partitions indexers into connected components based on dependency relationships. Indexers
* that share a dependency chain belong to the same component. Independent indexers each get
* their own component.
*
* @param indexers The list of indexers to partition
* @return A map from each indexer to its component root
*/
fun connectedComponents(indexers: List<Indexer>): Map<Indexer, Indexer> {
val indexerSet = indexers.toSet()
val parent = mutableMapOf<Indexer, Indexer>()

fun find(x: Indexer): Indexer {
var root = x
while (parent.getOrDefault(root, root) != root) root = parent.getValue(root)
var curr = x
while (curr != root) {
val next = parent.getOrDefault(curr, curr)
parent[curr] = root
curr = next
}
return root
}

fun union(a: Indexer, b: Indexer) {
parent[find(a)] = find(b)
}

for (indexer in indexers) {
parent.putIfAbsent(indexer, indexer)
val dep = indexer.dependsOn
if (dep != null && dep in indexerSet) {
parent.putIfAbsent(dep, dep)
union(indexer, dep)
}
}

// Return all indexers in a single group, properly ordered
return listOf(ordered)
return indexers.associateWith { find(it) }
}

/**
Expand Down
126 changes: 118 additions & 8 deletions src/test/kotlin/org/vechain/indexer/utils/IndexerOrderUtilsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import strikt.assertions.containsExactly
import strikt.assertions.hasSize
import strikt.assertions.isEmpty
import strikt.assertions.isEqualTo
import strikt.assertions.isNotEqualTo

internal class IndexerOrderUtilsTest {

Expand Down Expand Up @@ -47,15 +48,17 @@ internal class IndexerOrderUtilsTest {
}

@Test
fun `should return single group for multiple indexers with no dependencies`() {
fun `should return separate groups for multiple indexers with no dependencies`() {
val indexer1 = createMockIndexer("indexer1")
val indexer2 = createMockIndexer("indexer2")
val indexer3 = createMockIndexer("indexer3")

val result = IndexerOrderUtils.topologicalOrder(listOf(indexer1, indexer2, indexer3))

expectThat(result.size).isEqualTo(1)
expectThat(result[0]).containsExactly(indexer1, indexer2, indexer3)
expectThat(result.size).isEqualTo(3)
expectThat(result[0]).containsExactly(indexer1)
expectThat(result[1]).containsExactly(indexer2)
expectThat(result[2]).containsExactly(indexer3)
}

@Test
Expand Down Expand Up @@ -107,9 +110,10 @@ internal class IndexerOrderUtilsTest {
listOf(indexer1, indexer2, indexer3, indexer4, indexer5, indexer6)
)

expectThat(result.size).isEqualTo(1)
expectThat(result[0])
.containsExactly(indexer1, indexer2, indexer3, indexer4, indexer5, indexer6)
// Two connected components: {1,3,5,6} and {2,4}
expectThat(result.size).isEqualTo(2)
expectThat(result[0]).containsExactly(indexer1, indexer3, indexer5, indexer6)
expectThat(result[1]).containsExactly(indexer2, indexer4)
}

@Test
Expand Down Expand Up @@ -208,8 +212,11 @@ internal class IndexerOrderUtilsTest {
val result =
IndexerOrderUtils.topologicalOrder(listOf(indexer1, indexer2, indexer3, indexer4))

expectThat(result.size).isEqualTo(1)
expectThat(result[0]).containsExactly(indexer1, indexer2, indexer3, indexer4)
// Three groups: {1,3} chain, {2} independent, {4} independent
expectThat(result.size).isEqualTo(3)
expectThat(result[0]).containsExactly(indexer1, indexer3)
expectThat(result[1]).containsExactly(indexer2)
expectThat(result[2]).containsExactly(indexer4)
}

@Test
Expand Down Expand Up @@ -250,6 +257,109 @@ internal class IndexerOrderUtilsTest {
}
}

@Nested
inner class DependencySort {

@Test
fun `should return empty list for empty input`() {
val result = IndexerOrderUtils.dependencySort(emptyList())
expectThat(result).isEmpty()
}

@Test
fun `should return single indexer unchanged`() {
val indexer1 = createMockIndexer("indexer1")
val result = IndexerOrderUtils.dependencySort(listOf(indexer1))
expectThat(result).containsExactly(indexer1)
}

@Test
fun `should order dependencies before dependents`() {
val indexer1 = createMockIndexer("indexer1")
val indexer2 = createMockIndexer("indexer2", dependsOn = indexer1)
val indexer3 = createMockIndexer("indexer3", dependsOn = indexer2)

val result = IndexerOrderUtils.dependencySort(listOf(indexer3, indexer2, indexer1))
expectThat(result).containsExactly(indexer1, indexer2, indexer3)
}

@Test
fun `should detect circular dependency`() {
val indexer1 = createMockIndexer("indexer1")
val indexer2 = createMockIndexer("indexer2", dependsOn = indexer1)
every { indexer1.dependsOn } returns indexer2

assertThrows<IllegalStateException> {
IndexerOrderUtils.dependencySort(listOf(indexer1, indexer2))
}
}

@Test
fun `should reject missing dependency`() {
val external = createMockIndexer("external")
val indexer1 = createMockIndexer("indexer1", dependsOn = external)

assertThrows<IllegalArgumentException> {
IndexerOrderUtils.dependencySort(listOf(indexer1))
}
}
}

@Nested
inner class ConnectedComponents {

@Test
fun `should assign each independent indexer its own component`() {
val a = createMockIndexer("a")
val b = createMockIndexer("b")
val c = createMockIndexer("c")

val result = IndexerOrderUtils.connectedComponents(listOf(a, b, c))

val roots = result.values.toSet()
expectThat(roots).hasSize(3)
}

@Test
fun `should group dependency chain into one component`() {
val a = createMockIndexer("a")
val b = createMockIndexer("b", dependsOn = a)
val c = createMockIndexer("c", dependsOn = b)

val result = IndexerOrderUtils.connectedComponents(listOf(a, b, c))

expectThat(result.getValue(a)).isEqualTo(result.getValue(b))
expectThat(result.getValue(b)).isEqualTo(result.getValue(c))
}

@Test
fun `should separate independent chains`() {
val a = createMockIndexer("a")
val b = createMockIndexer("b", dependsOn = a)
val c = createMockIndexer("c")
val d = createMockIndexer("d", dependsOn = c)

val result = IndexerOrderUtils.connectedComponents(listOf(a, b, c, d))

expectThat(result.getValue(a)).isEqualTo(result.getValue(b))
expectThat(result.getValue(c)).isEqualTo(result.getValue(d))
expectThat(result.getValue(a)).isNotEqualTo(result.getValue(c))
}

@Test
fun `should group diamond pattern into one component`() {
val root = createMockIndexer("root")
val left = createMockIndexer("left", dependsOn = root)
val right = createMockIndexer("right", dependsOn = root)
val bottom = createMockIndexer("bottom", dependsOn = left)

val result = IndexerOrderUtils.connectedComponents(listOf(root, left, right, bottom))

val roots = result.values.toSet()
expectThat(roots).hasSize(1)
}
}

@Nested
inner class ProximityGroups {

Expand Down