From 2cfda03924bdf073f559a52ee1ee58179d290559 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 10:12:37 +0000 Subject: [PATCH 1/2] fix(daemon): resolve CPU spin and memory bloat in daemon process - Fix socket accept busy loop: replace non-blocking select/default with blocking Accept(), checking stopChan only on error to prevent 100% CPU spin when Accept() returns transient errors - Replace go-git worktree.Status() with native git CLI commands (rev-parse, status --porcelain) with 2s timeout, eliminating ~500MB memory from pure-Go tree walking on large repos - Add 5s read deadline on socket connections to prevent goroutine leaks from clients that connect but never send data - Guard fetchRateLimit goroutine with TryLock to prevent unbounded goroutine accumulation when HTTP requests exceed 3s tick interval - Add exponential backoff (100/200/400ms) and max retry (3) to pub/sub Nack handling to prevent CPU-burning retry loops on persistent failures https://claude.ai/code/session_0128SDwXB8JYiSP8kyRjmSgw --- daemon/cc_info_timer.go | 23 +++++++++++++++++++++-- daemon/chan.go | 21 +++++++++++++++++++-- daemon/git.go | 36 +++++++++++++++++++----------------- daemon/socket.go | 17 +++++++++-------- 4 files changed, 68 insertions(+), 29 deletions(-) diff --git a/daemon/cc_info_timer.go b/daemon/cc_info_timer.go index 31c0f13..43d7c3f 100644 --- a/daemon/cc_info_timer.go +++ b/daemon/cc_info_timer.go @@ -45,6 +45,9 @@ type CCInfoTimerService struct { stopChan chan struct{} wg sync.WaitGroup + // Guards concurrent fetchRateLimit goroutines + rateLimitFetchMu sync.Mutex + // Git info cache (per working directory) gitCache map[string]*GitCacheEntry @@ -160,7 +163,15 @@ func (s *CCInfoTimerService) timerLoop() { // Fetch immediately on start s.fetchActiveRanges(context.Background()) s.fetchGitInfo() - go s.fetchRateLimit(context.Background()) + go func() { + if !s.rateLimitFetchMu.TryLock() { + return + } + defer s.rateLimitFetchMu.Unlock() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + s.fetchRateLimit(ctx) + }() go s.fetchUserProfile(context.Background()) for { @@ -175,7 +186,15 @@ func (s *CCInfoTimerService) timerLoop() { } s.fetchActiveRanges(context.Background()) s.fetchGitInfo() - go s.fetchRateLimit(context.Background()) + go func() { + if !s.rateLimitFetchMu.TryLock() { + return + } + defer s.rateLimitFetchMu.Unlock() + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + s.fetchRateLimit(ctx) + }() case <-s.stopChan: return diff --git a/daemon/chan.go b/daemon/chan.go index f6a1f67..78d16e7 100644 --- a/daemon/chan.go +++ b/daemon/chan.go @@ -2,7 +2,9 @@ package daemon import ( "context" + "fmt" "sync" + "time" "github.com/lithammer/shortuuid/v3" "github.com/pkg/errors" @@ -344,6 +346,9 @@ func (s *subscriber) sendMessageToSubscriber(msg *message.Message, logFields wat ctx, cancelCtx := context.WithCancel(s.ctx) defer cancelCtx() + const maxRetries = 3 + retryCount := 0 + SendToSubscriber: for { // copy the message to prevent ack/nack propagation to other consumers @@ -371,8 +376,20 @@ SendToSubscriber: s.logger.Trace("Message acked", logFields) return case <-msgToSend.Nacked(): - s.logger.Trace("Nack received, resending message", logFields) - continue SendToSubscriber + retryCount++ + if retryCount >= maxRetries { + s.logger.Error("Max retries reached, dropping message", logFields) + return + } + backoff := time.Duration(100< 0 } return info diff --git a/daemon/socket.go b/daemon/socket.go index e7cfde1..25874f8 100644 --- a/daemon/socket.go +++ b/daemon/socket.go @@ -127,21 +127,22 @@ func (p *SocketHandler) Stop() { func (p *SocketHandler) acceptConnections() { for { - select { - case <-p.stopChan: - return - default: - conn, err := p.listener.Accept() - if err != nil { - continue + conn, err := p.listener.Accept() + if err != nil { + select { + case <-p.stopChan: + return + default: } - go p.handleConnection(conn) + continue } + go p.handleConnection(conn) } } func (p *SocketHandler) handleConnection(conn net.Conn) { defer conn.Close() + conn.SetDeadline(time.Now().Add(5 * time.Second)) decoder := json.NewDecoder(conn) var msg SocketMessage if err := decoder.Decode(&msg); err != nil { From dcea8f34a1f848b1f5be34609f57bb8fdb0b0321 Mon Sep 17 00:00:00 2001 From: AnnatarHe Date: Fri, 13 Feb 2026 21:33:01 +0800 Subject: [PATCH 2/2] fix(daemon): correct retry limit and error logging in message handler Fix off-by-one in max retries check and add missing error argument to logger. Co-Authored-By: Claude Opus 4.6 --- daemon/chan.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/daemon/chan.go b/daemon/chan.go index 78d16e7..2a89874 100644 --- a/daemon/chan.go +++ b/daemon/chan.go @@ -377,8 +377,8 @@ SendToSubscriber: return case <-msgToSend.Nacked(): retryCount++ - if retryCount >= maxRetries { - s.logger.Error("Max retries reached, dropping message", logFields) + if retryCount > maxRetries { + s.logger.Error("Max retries reached, dropping message", errors.New("max retries reached"), logFields) return } backoff := time.Duration(100<