fix(growth): harden AI jobs and cost controls

This commit is contained in:
2026-08-21 01:48:47 +03:30
parent 86e9d7c5f9
commit a15c04cac1
13 changed files with 366 additions and 78 deletions

View File

@@ -131,6 +131,17 @@ func (s *Server) growthReportEvents(w http.ResponseWriter, r *http.Request) {
return
}
merchantID := sessionFrom(r.Context()).MerchantID
slots := s.growthSSESlots
if slots == nil {
slots = defaultGrowthSSESlots
}
select {
case slots <- struct{}{}:
defer func() { <-slots }()
default:
s.problem(w, r, http.StatusTooManyRequests, "RATE_LIMITED", "Too many growth report streams")
return
}
record, err := s.growth.Get(r.Context(), merchantID, id)
if errors.Is(err, growthreport.ErrNotFound) {
s.problem(w, r, 404, "RESOURCE_NOT_FOUND", "Resource not found")
@@ -148,7 +159,17 @@ func (s *Server) growthReportEvents(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream; charset=utf-8")
w.Header().Set("Cache-Control", "no-cache, no-transform")
w.Header().Set("X-Accel-Buffering", "no")
ticker := time.NewTicker(2 * time.Second)
maxDuration := s.growthSSEMaxDuration
if maxDuration <= 0 {
maxDuration = defaultGrowthSSEMaxDuration
}
pollInterval := s.growthSSEPollInterval
if pollInterval <= 0 {
pollInterval = defaultGrowthSSEPollInterval
}
streamContext, cancel := context.WithTimeout(r.Context(), maxDuration)
defer cancel()
ticker := time.NewTicker(pollInterval)
defer ticker.Stop()
heartbeat := time.NewTicker(15 * time.Second)
defer heartbeat.Stop()
@@ -165,10 +186,10 @@ func (s *Server) growthReportEvents(w http.ResponseWriter, r *http.Request) {
return
}
select {
case <-r.Context().Done():
case <-streamContext.Done():
return
case <-ticker.C:
record, err = s.growth.Get(r.Context(), merchantID, id)
record, err = s.growth.Get(streamContext, merchantID, id)
if err != nil {
return
}