Files
steampipe/pkg/interactive/autocomplete_suggestions.go
Nathan Wallace eb3f0305c9 Fix race condition in autoCompleteSuggestions.sort() with mutex
Add mutex synchronization to the autoCompleteSuggestions struct to
prevent concurrent modification of slice data during sorting operations.

The sort() method now acquires a lock before performing any sorting
operations, ensuring that only one goroutine can sort the suggestions
at a time. This prevents the data races detected when sort() was called
concurrently.

Changes:
- Add sync.Mutex field to autoCompleteSuggestions struct
- Change sort() receiver from value to pointer to support mutex
- Add Lock/Unlock calls around sorting operations

The test TestAutocompleteSuggestions_ConcurrentSort now passes with
the -race flag, and all existing tests continue to pass.

Fixes #4711

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-16 16:18:55 -05:00

98 lines
2.8 KiB
Go

package interactive
import (
"sort"
"sync"
"github.com/c-bata/go-prompt"
)
const (
// Maximum number of schemas/connections to store in suggestion maps
maxSchemasInSuggestions = 100
// Maximum number of tables per schema in suggestions
maxTablesPerSchema = 500
// Maximum number of queries per mod in suggestions
maxQueriesPerMod = 500
)
type autoCompleteSuggestions struct {
mu sync.Mutex
schemas []prompt.Suggest
unqualifiedTables []prompt.Suggest
unqualifiedQueries []prompt.Suggest
tablesBySchema map[string][]prompt.Suggest
queriesByMod map[string][]prompt.Suggest
mods []prompt.Suggest
}
func newAutocompleteSuggestions() *autoCompleteSuggestions {
return &autoCompleteSuggestions{
tablesBySchema: make(map[string][]prompt.Suggest),
queriesByMod: make(map[string][]prompt.Suggest),
}
}
// setTablesForSchema adds tables for a schema with size limits to prevent unbounded growth.
// If the schema count exceeds maxSchemasInSuggestions, the oldest schema is removed.
// If the table count exceeds maxTablesPerSchema, only the first maxTablesPerSchema are kept.
func (s *autoCompleteSuggestions) setTablesForSchema(schemaName string, tables []prompt.Suggest) {
// Enforce per-schema table limit
if len(tables) > maxTablesPerSchema {
tables = tables[:maxTablesPerSchema]
}
// Enforce global schema limit
if len(s.tablesBySchema) >= maxSchemasInSuggestions {
// Remove one schema to make room (simple eviction - remove first key found)
for k := range s.tablesBySchema {
delete(s.tablesBySchema, k)
break
}
}
s.tablesBySchema[schemaName] = tables
}
// setQueriesForMod adds queries for a mod with size limits to prevent unbounded growth.
// If the mod count exceeds maxSchemasInSuggestions, the oldest mod is removed.
// If the query count exceeds maxQueriesPerMod, only the first maxQueriesPerMod are kept.
func (s *autoCompleteSuggestions) setQueriesForMod(modName string, queries []prompt.Suggest) {
// Enforce per-mod query limit
if len(queries) > maxQueriesPerMod {
queries = queries[:maxQueriesPerMod]
}
// Enforce global mod limit
if len(s.queriesByMod) >= maxSchemasInSuggestions {
// Remove one mod to make room (simple eviction - remove first key found)
for k := range s.queriesByMod {
delete(s.queriesByMod, k)
break
}
}
s.queriesByMod[modName] = queries
}
func (s *autoCompleteSuggestions) sort() {
s.mu.Lock()
defer s.mu.Unlock()
sortSuggestions := func(s []prompt.Suggest) {
sort.Slice(s, func(i, j int) bool {
return s[i].Text < s[j].Text
})
}
sortSuggestions(s.schemas)
sortSuggestions(s.unqualifiedTables)
sortSuggestions(s.unqualifiedQueries)
for _, tables := range s.tablesBySchema {
sortSuggestions(tables)
}
for _, queries := range s.queriesByMod {
sortSuggestions(queries)
}
}