Skip to content

Commit 380aec7

Browse files
authored
llms: fix memory and goroutine leaks in GoogleAI/Vertex and OpenAI streaming (#1364)
* llms: fix memory leaks by adding Close methods and improving goroutine handling Add Close methods to GoogleAI and Vertex clients to properly clean up resources and prevent memory leaks from underlying gRPC connections. Update examples to demonstrate proper client cleanup with defer statements. Improve goroutine management in OpenAI streaming response handling with proper context cancellation and non-blocking channel operations to prevent goroutine leaks in error scenarios. * examples: add replace directives for testing memory leak fixes Add temporary replace directives to GoogleAI and Vertex example go.mod files to test the Close() method changes from the previous commit. These directives allow the examples to use the local version of the library with the memory leak fixes before they're included in the next release.
1 parent 9d4df6f commit 380aec7

7 files changed

Lines changed: 57 additions & 3 deletions

File tree

examples/googleai-completion-example/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ go 1.24.3
44

55
require github.com/tmc/langchaingo v0.1.14-pre.0
66

7+
// Temporary replace directive for testing Close() method changes
8+
// TODO: Remove after next release includes the Close() methods
9+
replace github.com/tmc/langchaingo => ../..
10+
711
require (
812
cloud.google.com/go v0.116.0 // indirect
913
cloud.google.com/go/ai v0.7.0 // indirect

examples/googleai-completion-example/googleai-completion-example.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func main() {
1818
if err != nil {
1919
log.Fatal(err)
2020
}
21+
defer llm.Close() // Clean up client when done
2122

2223
prompt := "Who was the second person to walk on the moon?"
2324
answer, err := llms.GenerateFromSinglePrompt(ctx, llm, prompt)

examples/vertex-completion-example/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ go 1.24.3
44

55
require github.com/tmc/langchaingo v0.1.14-pre.0
66

7+
// Temporary replace directive for testing Close() method changes
8+
// TODO: Remove after next release includes the Close() methods
9+
replace github.com/tmc/langchaingo => ../..
10+
711
require (
812
cloud.google.com/go v0.116.0 // indirect
913
cloud.google.com/go/ai v0.7.0 // indirect

examples/vertex-completion-example/vertex-completion-example.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ func main() {
3030
if err != nil {
3131
log.Fatal(err)
3232
}
33+
defer llm.Close() // Clean up client when done
3334

3435
prompt := "Who was the second person to walk on the moon?"
3536
answer, err := llms.GenerateFromSinglePrompt(ctx, llm, prompt)

llms/googleai/new.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,13 @@ func New(ctx context.Context, opts ...Option) (*GoogleAI, error) {
3939
gi.client = client
4040
return gi, nil
4141
}
42+
43+
// Close closes the underlying genai client.
44+
// This should be called when the GoogleAI instance is no longer needed
45+
// to prevent memory leaks from the underlying gRPC connections.
46+
func (g *GoogleAI) Close() error {
47+
if g.client != nil {
48+
return g.client.Close()
49+
}
50+
return nil
51+
}

llms/googleai/vertex/new.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,15 @@ func New(ctx context.Context, opts ...googleai.Option) (*Vertex, error) {
5858
}
5959
return v, nil
6060
}
61+
62+
// Close closes the underlying genai and palm clients.
63+
// This should be called when the Vertex instance is no longer needed
64+
// to prevent memory leaks from the underlying gRPC connections.
65+
func (v *Vertex) Close() error {
66+
var err error
67+
if v.client != nil {
68+
err = v.client.Close()
69+
}
70+
// Note: palmClient doesn't have a Close method based on the codebase
71+
return err
72+
}

llms/openai/internal/openaiclient/chat.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,21 @@ func parseStreamingChatResponse(ctx context.Context, r *http.Response, payload *
438438
) { //nolint:cyclop,lll
439439
scanner := bufio.NewScanner(r.Body)
440440
responseChan := make(chan StreamedChatResponsePayload)
441+
442+
// Create a context that can be cancelled to stop the goroutine
443+
readerCtx, cancelReader := context.WithCancel(ctx)
444+
defer cancelReader()
445+
441446
go func() {
442447
defer close(responseChan)
443448
for scanner.Scan() {
449+
// Check if context is cancelled
450+
select {
451+
case <-readerCtx.Done():
452+
return
453+
default:
454+
}
455+
444456
line := scanner.Text()
445457
if line == "" {
446458
continue
@@ -459,15 +471,25 @@ func parseStreamingChatResponse(ctx context.Context, r *http.Response, payload *
459471
// This allows the stream to continue processing valid JSON chunks
460472
continue
461473
}
462-
responseChan <- streamPayload
474+
475+
// Non-blocking send with context check
476+
select {
477+
case <-readerCtx.Done():
478+
return
479+
case responseChan <- streamPayload:
480+
}
463481
}
464482
if err := scanner.Err(); err != nil {
465-
responseChan <- StreamedChatResponsePayload{Error: fmt.Errorf("error reading streaming response: %w", err)}
483+
select {
484+
case <-readerCtx.Done():
485+
return
486+
case responseChan <- StreamedChatResponsePayload{Error: fmt.Errorf("error reading streaming response: %w", err)}:
487+
}
466488
return
467489
}
468490
}()
469491
// Combine response
470-
return combineStreamingChatResponse(ctx, payload, responseChan)
492+
return combineStreamingChatResponse(readerCtx, payload, responseChan)
471493
}
472494

473495
func combineStreamingChatResponse(

0 commit comments

Comments
 (0)