Index import dependencies for local scope scheduling

This commit is contained in:
2026-10-25 14:41:07 +00:00
parent 64be8b4df5
commit 8dfaf44d1d
7 changed files with 135 additions and 16 deletions

View File

@@ -5,6 +5,29 @@ import (
"testing"
)
func TestImportSchedulingKeepsUnselectedSlotsAndScopeBoundaries(t *testing.T) {
dependent := importNodeKey{resourcePath: "a-dependent"}
external := importNodeKey{resourcePath: "external"}
prerequisite := importNodeKey{resourcePath: "z-prerequisite"}
graph := newImportInclusionGraph()
for _, key := range []importNodeKey{dependent, external, prerequisite} {
graph.addNode(importGraphNode{key: key})
}
graph.addEdge(dependent, prerequisite, Location{})
graph.addEdge(dependent, external, Location{})
graph.addEdge(external, prerequisite, Location{})
contributions := []acquiredContribution{
{identity: dependent}, {identity: external}, {identity: prerequisite},
}
scheduled := scheduleContributionSlots(contributions, []int{0, 2}, indexImportDependencies(graph))
if scheduled[0].identity != prerequisite || scheduled[1].identity != external || scheduled[2].identity != dependent {
t.Fatalf("scope schedule = %#v", scheduled)
}
if contributions[0].identity != dependent || contributions[2].identity != prerequisite {
t.Fatal("scheduling changed the input slots")
}
}
func TestImportAndInheritanceIntegrationsUseEquivalentReadySchedules(t *testing.T) {
names := []string{"a-base", "b-independent", "z-dependent"}
keys := make(map[string]importNodeKey, len(names))
@@ -33,7 +56,7 @@ func TestImportAndInheritanceIntegrationsUseEquivalentReadySchedules(t *testing.
}
graph.addEdge(keys["z-dependent"], keys["a-base"], Location{})
scheduled := scheduleContributionSlots(contributions, []int{0, 1, 2}, graph)
scheduled := scheduleContributionSlots(contributions, []int{0, 1, 2}, indexImportDependencies(graph))
got := make([]string, len(scheduled))
for index, contribution := range scheduled {
got[index] = contribution.identity.resourcePath[1:]

View File

@@ -178,7 +178,7 @@ func acquireImportedWithAccess(request importAcquisitionRequest, policy *acquisi
}
}
if len(diagnostics) == 0 {
state.scheduleAcquisitionScopes(&state.scope)
scheduleAcquisitionScopes(&state.scope, indexImportDependencies(state.importGraph))
}
closeErr := access.close()

View File

@@ -157,7 +157,20 @@ func (graph importInclusionGraph) scheduleCycle(closingEdge int, location Locati
}, true
}
func scheduleAcquisitionScope(scope acquisitionScope, graph importInclusionGraph) acquisitionScope {
// importDependencyIndex is built once per scheduling operation and shared by
// its scopes. Inclusion targets are grouped by importer, so local scheduling
// need not scan or reserve storage for unrelated graph edges.
type importDependencyIndex map[importNodeKey][]importNodeKey
func indexImportDependencies(graph importInclusionGraph) importDependencyIndex {
index := make(importDependencyIndex)
for _, edge := range graph.edges {
index[edge.importer] = append(index[edge.importer], edge.imported)
}
return index
}
func scheduleAcquisitionScope(scope acquisitionScope, dependencies importDependencyIndex) acquisitionScope {
slots := make([]int, 0, len(scope.contributions))
for index, contribution := range scope.contributions {
if contribution.scopeOwner != scope.owner {
@@ -166,7 +179,7 @@ func scheduleAcquisitionScope(scope acquisitionScope, graph importInclusionGraph
slots = append(slots, index)
}
scheduled := scope
scheduled.contributions = scheduleContributionSlots(scope.contributions, slots, graph)
scheduled.contributions = scheduleContributionSlots(scope.contributions, slots, dependencies)
return scheduled
}
@@ -179,10 +192,10 @@ func scheduleAcquisitionGroup(scopes []acquisitionScope, graph importInclusionGr
for index := range slots {
slots[index] = index
}
return acquisitionScope{contributions: scheduleContributionSlots(contributions, slots, graph)}
return acquisitionScope{contributions: scheduleContributionSlots(contributions, slots, indexImportDependencies(graph))}
}
func scheduleContributionSlots(contributions []acquiredContribution, slots []int, graph importInclusionGraph) []acquiredContribution {
func scheduleContributionSlots(contributions []acquiredContribution, slots []int, dependencies importDependencyIndex) []acquiredContribution {
if len(slots) < 2 {
return append([]acquiredContribution(nil), contributions...)
}
@@ -193,12 +206,12 @@ func scheduleContributionSlots(contributions []acquiredContribution, slots []int
positions[contribution.identity] = position
keys[position] = contribution.identity.dependencyKey()
}
edges := make([]dependencyEdge, 0, len(graph.edges))
for _, edge := range graph.edges {
importer, importerOK := positions[edge.importer]
imported, importedOK := positions[edge.imported]
if importerOK && importedOK {
edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer})
var edges []dependencyEdge
for importerKey, importer := range positions {
for _, importedKey := range dependencies[importerKey] {
if imported, exists := positions[importedKey]; exists {
edges = append(edges, dependencyEdge{prerequisite: imported, dependent: importer})
}
}
}
order, _ := scheduleDependencies(keys, edges)

View File

@@ -53,11 +53,11 @@ type importTraversal struct {
importGraph importInclusionGraph
}
func (s *importTraversal) scheduleAcquisitionScopes(scope *acquisitionScope) {
*scope = scheduleAcquisitionScope(*scope, s.importGraph)
func scheduleAcquisitionScopes(scope *acquisitionScope, dependencies importDependencyIndex) {
*scope = scheduleAcquisitionScope(*scope, dependencies)
for index := range scope.contributions {
if pending := scope.contributions[index].pendingItem; pending != nil {
s.scheduleAcquisitionScopes(pending)
scheduleAcquisitionScopes(pending, dependencies)
}
}
}

View File

@@ -1107,6 +1107,70 @@ id: item
})
}
func TestBroadItemScopesKeepHelperOriginsAndDestinations(t *testing.T) {
t.Parallel()
type item struct {
ID string `yaml:"id"`
Value string `yaml:"value"`
}
type config struct {
Items []item `yaml:"items"`
}
directory := t.TempDir()
const count = 32
var main strings.Builder
main.WriteString("configkit:\n imports:\n")
for index := count - 1; index >= 0; index-- {
name := strconv.Itoa(100 + index)
main.WriteString(" - path: item-" + name + ".yml\n destination: [items]\n placement: sequence-item\n")
writeImportFile(t, directory, "item-"+name+".yml", "configkit: {imports: [{path: helper-"+name+".yml}]}\nid: item-"+name+"\n")
writeImportFile(t, directory, "helper-"+name+".yml", "value: helper-"+name+"\n")
}
mainPath := writeImportFile(t, directory, "main.yml", main.String())
definition := Definition[config]{sequenceIdentityRegistrations: []sequenceIdentityRegistration{{
selector: sequenceIdentitySelector{literalSequenceSelector("items")}, identityFields: []string{"id"},
}}}
for _, layered := range []bool{false, true} {
t.Run(strconv.FormatBool(layered), func(t *testing.T) {
var loaded *Loaded[config]
var err error
if layered {
loaded, err = LoadLayered(Input{Source: NoSource()}, testSecretBinding(), LayeredOptions[config]{
Definition: definition,
Underlays: []Underlay{{Inputs: []Input{{Source: File(mainPath), Imports: true}}}},
})
} else {
loaded, err = Load(File(mainPath), testSecretBinding(), Options[config]{Imports: true, Definition: definition})
}
if err != nil {
t.Fatal(err)
}
result := testLoaded(loaded)
sources := result.Sources()
if len(result.Config.Items) != count || len(sources) != 2*count+1 {
t.Fatalf("item/source counts = %d/%d", len(result.Config.Items), len(sources))
}
metadata := result.Metadata()
for index, record := range result.Config.Items {
name := strconv.Itoa(100 + index)
if record.ID != "item-"+name || record.Value != "helper-"+name {
t.Fatalf("item %d = %#v", index, record)
}
// Acquisition remains authored (descending); final item order is
// dependency-ready (ascending), with helper evidence reprojected.
helper := sources[2*(count-index)]
field, found := metadata.Lookup(Keys("items").Index(index).Key("value"))
if !found || len(field.Origin.Locations) != 1 || field.Origin.Locations[0].SourceID != helper.ID ||
helper.Path != canonicalTestPath(t, filepath.Join(directory, "helper-"+name+".yml")) ||
helper.Destination.String() != Keys("items").Index(index).String() {
t.Fatalf("item %d helper evidence = %#v, %#v", index, field, helper)
}
}
})
}
}
func TestItemHelperScopeScheduleInterleavesUnequalBranches(t *testing.T) {
directory := t.TempDir()
mainPath := writeImportFile(t, directory, "main.yml", `configkit: