From 19107a33b1fdfa0c07b316808adbe0bf824b9abb Mon Sep 17 00:00:00 2001 From: Maksim Ustinov Date: Thu, 22 Oct 2026 13:18:05 +0200 Subject: [PATCH 1/2] fix(index): avoid waiting for target indexing during component lookup Co-authored-by: OpenAI Codex --- index/find_component_external.go | 6 +-- index/find_component_pending_index_test.go | 43 ++++++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 index/find_component_pending_index_test.go diff --git a/index/find_component_external.go b/index/find_component_external.go index 1f224a3f..75ddcb92 100644 --- a/index/find_component_external.go +++ b/index/find_component_external.go @@ -52,9 +52,9 @@ func (index *SpecIndex) lookupRolodex(ctx context.Context, uri []string) *Refere "file", absoluteFileLocation) return nil } - if rFile.GetIndex() == nil && !IsFileBeingIndexed(ctx, absoluteFileLocation) { - rFile.WaitForIndexing() - } + // Looking up a component needs the parsed YAML, not the completed index. + // Waiting here can deadlock concurrently indexed documents that reference + // each other, even when the referenced schema graph itself is finite. if rFile.GetIndex() != nil { idx = rFile.GetIndex() } diff --git a/index/find_component_pending_index_test.go b/index/find_component_pending_index_test.go new file mode 100644 index 00000000..08a51d08 --- /dev/null +++ b/index/find_component_pending_index_test.go @@ -0,0 +1,43 @@ +package index + +import ( + "context" + "path/filepath" + "testing" + "time" + + "github.com/pb33f/go-yaml" + "github.com/pb33f/testify/assert" +) + +func TestLookupRolodexDoesNotWaitForTargetIndex(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "target.yaml") + cfg := CreateOpenAPIIndexConfig() + cfg.BasePath = dir + rolo := NewRolodex(cfg) + cfg.Rolodex = rolo + files, err := NewLocalFSWithConfig(&LocalFSConfig{BaseDirectory: dir, IndexConfig: cfg}) + assert.NoError(t, err) + pending := &LocalFile{ + fullPath: path, + data: []byte("components:\n schemas:\n Item:\n type: object\n"), + indexingComplete: make(chan struct{}), + } + files.Files.Store(path, pending) + rolo.AddLocalFS(dir, files) + defer close(pending.indexingComplete) + var root yaml.Node + assert.NoError(t, yaml.Unmarshal([]byte("components: {}"), &root)) + idx := NewSpecIndexWithConfig(&root, cfg) + result := make(chan *Reference, 1) + go func() { + result <- idx.lookupRolodex(context.Background(), []string{path, "components/schemas/Item"}) + }() + select { + case ref := <-result: + assert.NotNil(t, ref) + case <-time.After(time.Second): + t.Fatal("reference lookup waited for target indexing; mutually referencing files can deadlock") + } +} From 69a4cc3db0edce98d696810708dadf20dc7e48a2 Mon Sep 17 00:00:00 2001 From: Maksim Ustinov Date: Thu, 22 Oct 2026 14:56:03 +0200 Subject: [PATCH 2/2] test(index): preserve explicit indexing wait guarantees Cover the retained local/remote WaitForIndexing API directly after removing its use from component lookup, and verify an untracked context is not considered indexing. Co-authored-by: OpenAI Codex --- index/find_component_pending_index_test.go | 41 ++++++++++++++++++++++ index/search_index_test.go | 1 + 2 files changed, 42 insertions(+) diff --git a/index/find_component_pending_index_test.go b/index/find_component_pending_index_test.go index 08a51d08..b9cfdfc4 100644 --- a/index/find_component_pending_index_test.go +++ b/index/find_component_pending_index_test.go @@ -41,3 +41,44 @@ func TestLookupRolodexDoesNotWaitForTargetIndex(t *testing.T) { t.Fatal("reference lookup waited for target indexing; mutually referencing files can deadlock") } } + +func TestExplicitWaitForIndexingStillWaitsForCompletion(t *testing.T) { + for name, file := range map[string]func(chan struct{}) *rolodexFile{ + "local": func(done chan struct{}) *rolodexFile { + return &rolodexFile{localFile: &LocalFile{indexingComplete: done}} + }, + "remote": func(done chan struct{}) *rolodexFile { + return &rolodexFile{remoteFile: &RemoteFile{indexingComplete: done}} + }, + } { + t.Run(name, func(t *testing.T) { + indexed := make(chan struct{}) + t.Cleanup(func() { + select { + case <-indexed: + default: + close(indexed) + } + }) + returned := make(chan struct{}) + go func() { + file(indexed).WaitForIndexing() + close(returned) + }() + select { + case <-returned: + t.Fatal("explicit wait returned before indexing completed") + case <-time.After(10 * time.Millisecond): + } + close(indexed) + select { + case <-returned: + case <-time.After(time.Second): + t.Fatal("explicit wait did not return after indexing completed") + } + // Files without an indexing operation have nothing to wait for. + file(nil).WaitForIndexing() + }) + } + (&rolodexFile{}).WaitForIndexing() +} diff --git a/index/search_index_test.go b/index/search_index_test.go index 7252e1e8..865738e5 100644 --- a/index/search_index_test.go +++ b/index/search_index_test.go @@ -815,6 +815,7 @@ paths: {}` func TestIsFileBeingIndexed_HTTPPathMatch(t *testing.T) { // Test that HTTP paths match when the path portion is the same ctx := context.Background() + assert.False(t, IsFileBeingIndexed(ctx, "https://example.com/schemas/pet.yaml")) // Add an HTTP file to the indexing context files := make(map[string]bool)