From bae09b602430124050e4a9979c7267abd24dd9e7 Mon Sep 17 00:00:00 2001 From: Nanook Date: Sun, 10 May 2026 22:03:35 +0000 Subject: [PATCH 1/2] fix: clean up expired OAuth codes to prevent memory leak --- havril/internal/api/handlers/oauth_handler.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/havril/internal/api/handlers/oauth_handler.go b/havril/internal/api/handlers/oauth_handler.go index 08bb818..9cba932 100644 --- a/havril/internal/api/handlers/oauth_handler.go +++ b/havril/internal/api/handlers/oauth_handler.go @@ -30,7 +30,18 @@ type codeEntry struct { } func NewOAuthHandler(baseURL string, userRepo *user.Repository) *OAuthHandler { - return &OAuthHandler{baseURL: baseURL, userRepo: userRepo} + h := &OAuthHandler{baseURL: baseURL, userRepo: userRepo} + go func() { + for range time.Tick(5 * time.Minute) { + h.codes.Range(func(k, v any) bool { + if time.Now().After(v.(*codeEntry).expiresAt) { + h.codes.Delete(k) + } + return true + }) + } + }() + return h } // Metadata handles GET /.well-known/oauth-authorization-server From da042c6a3dd5a5de2ef942d4346ef01922f8440d Mon Sep 17 00:00:00 2001 From: Nanook Date: Wed, 13 May 2026 16:37:55 +0000 Subject: [PATCH 2/2] fix: stop OAuth cleanup worker on context cancel Signed-off-by: Nanook --- havril/internal/api/handlers/oauth_handler.go | 36 +++++++++++++------ havril/main.go | 6 ++-- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/havril/internal/api/handlers/oauth_handler.go b/havril/internal/api/handlers/oauth_handler.go index 9cba932..6f5a09f 100644 --- a/havril/internal/api/handlers/oauth_handler.go +++ b/havril/internal/api/handlers/oauth_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "context" "crypto/rand" "crypto/sha256" "encoding/base64" @@ -29,21 +30,34 @@ type codeEntry struct { expiresAt time.Time } -func NewOAuthHandler(baseURL string, userRepo *user.Repository) *OAuthHandler { +func NewOAuthHandler(ctx context.Context, baseURL string, userRepo *user.Repository) *OAuthHandler { h := &OAuthHandler{baseURL: baseURL, userRepo: userRepo} - go func() { - for range time.Tick(5 * time.Minute) { - h.codes.Range(func(k, v any) bool { - if time.Now().After(v.(*codeEntry).expiresAt) { - h.codes.Delete(k) - } - return true - }) - } - }() + go h.cleanupExpiredCodes(ctx, 5*time.Minute) return h } +func (h *OAuthHandler) cleanupExpiredCodes(ctx context.Context, interval time.Duration) { + ticker := time.NewTicker(interval) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + h.deleteExpiredCodes(time.Now()) + } + } +} + +func (h *OAuthHandler) deleteExpiredCodes(now time.Time) { + h.codes.Range(func(k, v any) bool { + if now.After(v.(*codeEntry).expiresAt) { + h.codes.Delete(k) + } + return true + }) +} + // Metadata handles GET /.well-known/oauth-authorization-server func (h *OAuthHandler) Metadata(w http.ResponseWriter, r *http.Request) { base := requestBaseURL(r) diff --git a/havril/main.go b/havril/main.go index dba69d8..74acebd 100644 --- a/havril/main.go +++ b/havril/main.go @@ -69,11 +69,13 @@ func main() { } goth.UseProviders(providers...) + ctx := context.Background() + // Dependency injection userRepo := user.NewRepository(db) userSvc := user.NewService(userRepo) authHandler := handlers.NewAuthHandler(userSvc) - oauthHandler := handlers.NewOAuthHandler(baseURL, userRepo) + oauthHandler := handlers.NewOAuthHandler(ctx, baseURL, userRepo) authMid := middleware.NewAuthMiddleware(userSvc) modelRepo := user.NewModelRepository(db) modelSvc := user.NewModelService(modelRepo) @@ -84,7 +86,7 @@ func main() { if err != nil { log.Fatalf("connect qdrant: %v", err) } - if err := store.EnsureCollection(context.Background(), qdrantHost, qdrantAPIKey); err != nil { + if err := store.EnsureCollection(ctx, qdrantHost, qdrantAPIKey); err != nil { log.Fatalf("failed to ensure qdrant collection: %v", err) } embedder, err := embedding.New(embedding.Config{