Query history memory leak - unbounded growth closes #4811 (#4859)

* Add test for #4811: query history should have bounded size

* Fix #4811: Enforce bounded size for query history

Add enforceLimit() helper that ensures history never exceeds HistorySize.
Call it from Get(), Push(), and load() to prevent unbounded memory growth
even when history is pre-populated from file or direct manipulation.
This commit is contained in:
Nathan Wallace
2025-11-16 00:22:31 +08:00
committed by GitHub
parent e374540483
commit a23583dd91
2 changed files with 61 additions and 7 deletions

View File

@@ -38,14 +38,11 @@ func (q *QueryHistory) Push(query string) {
return
}
// limit the history length to HistorySize
historyLength := len(q.history)
if historyLength >= constants.HistorySize {
q.history = q.history[historyLength-constants.HistorySize+1:]
}
// append the new entry
q.history = append(q.history, query)
// enforce the size limit after adding
q.enforceLimit()
}
// Peek returns the last element of the history stack.
@@ -78,11 +75,22 @@ func (q *QueryHistory) Persist() error {
return jsonEncoder.Encode(q.history)
}
// Get returns the full history
// Get returns the full history, enforcing the size limit
func (q *QueryHistory) Get() []string {
// Ensure history doesn't exceed the limit before returning
q.enforceLimit()
return q.history
}
// enforceLimit ensures the history size doesn't exceed HistorySize
func (q *QueryHistory) enforceLimit() {
historyLength := len(q.history)
if historyLength > constants.HistorySize {
// Keep only the most recent HistorySize entries
q.history = q.history[historyLength-constants.HistorySize:]
}
}
// loads up the history from the file where it is persisted
func (q *QueryHistory) load() error {
path := filepath.Join(filepaths.EnsureInternalDir(), constants.HistoryFile)
@@ -103,5 +111,12 @@ func (q *QueryHistory) load() error {
if err == io.EOF {
return nil
}
// Enforce size limit after loading from file to prevent unbounded growth
// in case the file was corrupted or manually edited
if err == nil {
q.enforceLimit()
}
return err
}

View File

@@ -0,0 +1,39 @@
package queryhistory
import (
"fmt"
"testing"
"github.com/turbot/steampipe/v2/pkg/constants"
)
// TestQueryHistory_BoundedSize tests that query history doesn't grow unbounded.
// This test demonstrates bug #4811 where history could grow without limit in memory
// during a session, even though Push() limits new additions.
//
// Bug: #4811
func TestQueryHistory_BoundedSize(t *testing.T) {
// t.Skip("Test demonstrates bug #4811: query history grows unbounded in memory during session")
// Simulate a scenario where history is pre-populated (e.g., from a corrupted file or direct manipulation)
// This represents the in-memory history during a long-running session
oversizedHistory := make([]string, constants.HistorySize+100)
for i := 0; i < len(oversizedHistory); i++ {
oversizedHistory[i] = fmt.Sprintf("SELECT %d;", i)
}
history := &QueryHistory{history: oversizedHistory}
// Even with pre-existing oversized history, operations should enforce the limit
// Get() should never return more than HistorySize entries
retrieved := history.Get()
if len(retrieved) > constants.HistorySize {
t.Errorf("Get() returned %d entries, exceeds limit %d", len(retrieved), constants.HistorySize)
}
// After any operation, the internal history should be bounded
history.Push("SELECT new;")
if len(history.history) > constants.HistorySize {
t.Errorf("After Push(), history size %d exceeds limit %d", len(history.history), constants.HistorySize)
}
}