Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,6 @@ go tool pprof mem.prof
Remember: You're Bolt, making switchAILocal lightning fast. But speed without correctness is useless. Measure, optimize, verify.

**If you can't find a clear performance win today, stop and do not create a PR.**
## $(date +%Y-%m-%d) - sync.Pool defer Cleanup Caution
**Learning:** When using `sync.Pool` to recycle objects inside middleware or wrapper functions like `ResponseWriterWrapper.Finalize`, returning the object inside multiple conditional return paths increases the risk of memory leaks if a new path is added later or double-free panics if cleanup logic becomes misaligned.
**Action:** Always structure `sync.Pool.Put` operations within a single `defer` block at the top of the function to guarantee cleanup across all execution paths, rather than duplicating the `Put` logic across multiple early return statements.
20 changes: 19 additions & 1 deletion internal/api/middleware/response_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"bytes"
"net/http"
"strings"
"sync"

"github.com/gin-gonic/gin"
"github.com/traylinx/switchAILocal/internal/interfaces"
Expand All @@ -28,6 +29,12 @@ type RequestInfo struct {

// ResponseWriterWrapper wraps the standard gin.ResponseWriter to intercept and log response data.
// It is designed to handle both standard and streaming responses, ensuring that logging operations do not block the client response.
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}

type ResponseWriterWrapper struct {
gin.ResponseWriter
body *bytes.Buffer // body is a buffer to store the response body for non-streaming responses.
Expand All @@ -53,9 +60,11 @@ type ResponseWriterWrapper struct {
// Returns:
// - A pointer to a new ResponseWriterWrapper.
func NewResponseWriterWrapper(w gin.ResponseWriter, logger logging.RequestLogger, requestInfo *RequestInfo) *ResponseWriterWrapper {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset()
return &ResponseWriterWrapper{
ResponseWriter: w,
body: &bytes.Buffer{},
body: buf,
logger: logger,
requestInfo: requestInfo,
headers: make(map[string][]string),
Expand Down Expand Up @@ -246,6 +255,15 @@ func (w *ResponseWriterWrapper) processStreamingChunks(done chan struct{}) {
// For non-streaming responses, it logs the complete request and response details,
// including any API-specific request/response data stored in the Gin context.
func (w *ResponseWriterWrapper) Finalize(c *gin.Context) error {
defer func() {
if w.body != nil {
if w.body.Cap() <= 128*1024 {
bufferPool.Put(w.body)
}
w.body = nil
}
}()

if w.logger == nil {
return nil
}
Expand Down
Loading