Files
steampipe/pkg/interactive/autocomplete_suggestions.go
Nathan Wallace 114ac22dea Fix #4716: Add synchronization to autoCompleteSuggestions.sort() (#4737)
* Add test for #4716: sort() should be safe for concurrent calls

* Fix #4716: Add mutex protection to autoCompleteSuggestions.sort()

Adds sync.RWMutex to prevent data race during concurrent sort() calls.
Changes sort() from value receiver to pointer receiver to support locking.

The mutex ensures thread-safe access when multiple goroutines call sort()
simultaneously during autocomplete initialization.
2025-11-16 13:59:43 -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.RWMutex
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)
}
}