From 20da7a8d07ddb4cefd0a11e196b4cff28ae20038 Mon Sep 17 00:00:00 2001 From: erio Date: Mon, 13 Apr 2026 16:57:53 +0800 Subject: [PATCH] perf: run balance/quota notification checks async Move notifyBalanceLow and notifyAccountQuota to goroutines so the threshold checking logic (DB settings reads, threshold calculation, crossing detection) no longer blocks the request thread. Both are fire-and-forget with panic recovery and log-only error handling. --- backend/cmd/server/VERSION | 2 +- backend/internal/service/gateway_service.go | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/backend/cmd/server/VERSION b/backend/cmd/server/VERSION index e52af70615..74956299aa 100644 --- a/backend/cmd/server/VERSION +++ b/backend/cmd/server/VERSION @@ -1 +1 @@ -0.1.110.28 +0.1.110.29 diff --git a/backend/internal/service/gateway_service.go b/backend/internal/service/gateway_service.go index 32c96de22d..a4571db28d 100644 --- a/backend/internal/service/gateway_service.go +++ b/backend/internal/service/gateway_service.go @@ -7529,14 +7529,21 @@ func finalizePostUsageBilling(p *postUsageBillingParams, deps *billingDeps, resu deps.deferredService.ScheduleLastUsedUpdate(p.Account.ID) - notifyBalanceLow(p, deps, result) - notifyAccountQuota(p, deps, result) + // Notification checks run async — all parameters are already captured, + // no dependency on the request context or upstream connection. + go notifyBalanceLow(p, deps, result) + go notifyAccountQuota(p, deps, result) } // notifyBalanceLow sends balance low notification after deduction. // When result.NewBalance is available (from DB transaction RETURNING), it is used directly // to reconstruct oldBalance, avoiding stale Redis reads and concurrent-deduction races. func notifyBalanceLow(p *postUsageBillingParams, deps *billingDeps, result *UsageBillingApplyResult) { + defer func() { + if r := recover(); r != nil { + slog.Error("panic in notifyBalanceLow", "recover", r) + } + }() if p.IsSubscriptionBill || p.Cost.ActualCost <= 0 || p.User == nil || deps.balanceNotifyService == nil { slog.Debug("notifyBalanceLow: skipped", "is_subscription", p.IsSubscriptionBill, @@ -7573,6 +7580,11 @@ func resolveOldBalance(p *postUsageBillingParams, result *UsageBillingApplyResul // When result.QuotaState is available (from DB transaction RETURNING), it is passed directly // to avoid a separate DB read that may see stale or concurrently-modified data. func notifyAccountQuota(p *postUsageBillingParams, deps *billingDeps, result *UsageBillingApplyResult) { + defer func() { + if r := recover(); r != nil { + slog.Error("panic in notifyAccountQuota", "recover", r) + } + }() if p.Cost.TotalCost <= 0 || p.Account == nil || !p.Account.IsAPIKeyOrBedrock() || deps.balanceNotifyService == nil { slog.Debug("notifyAccountQuota: skipped", "total_cost", p.Cost.TotalCost,