diff --git a/app/auth/jwt.go b/app/auth/jwt.go index 5a904c6..4459a68 100644 --- a/app/auth/jwt.go +++ b/app/auth/jwt.go @@ -78,6 +78,10 @@ var nubesAPIEndpoints = []string{ "https://lk-api-gateway-dev.ngcloud.ru/api/v1/svc", } +// nubesHTTPClient — общий клиент для валидации токенов: переиспользует +// соединения (keep-alive) вместо создания нового клиента на каждый запрос. +var nubesHTTPClient = &http.Client{Timeout: 10 * time.Second} + // PingNubesAPI — проверяет валидность токена перебором стендов Nubes API. // endpoint (если задан через NUBES_ENDPOINT) пробуется первым. // Логика: токен принимается ТОЛЬКО при явном HTTP 2xx от стенда. @@ -104,8 +108,7 @@ func PingNubesAPI(ctx context.Context, endpoint, token string) error { req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("User-Agent", "Mozilla/5.0") - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Do(req) + resp, err := nubesHTTPClient.Do(req) if err != nil { lastErr = fmt.Errorf("nubes API unreachable at %s: %w", ep, err) continue diff --git a/app/tenant/tenant_store.go b/app/tenant/tenant_store.go index 70a921b..56db184 100644 --- a/app/tenant/tenant_store.go +++ b/app/tenant/tenant_store.go @@ -86,6 +86,12 @@ func (s *TenantStore) Create(name string, maxQueues int) (*Tenant, error) { } s.mu.Lock() + // Повторная проверка лимита ПОД Lock: ранняя проверка выше была под RLock, + // между ней и взятием Lock другой поток мог создать тенанта (TOCTOU). + if len(s.byID) >= MaxTenantsGlobal { + s.mu.Unlock() + return nil, fmt.Errorf("global tenant limit reached (%d)", MaxTenantsGlobal) + } s.byID[t.ID] = t s.byAccessKey[t.AccessKey] = t if t.Email != "" { @@ -221,15 +227,21 @@ func (s *TenantStore) CreateFromJWT(sub, email string, maxQueues int) (*Tenant, // Идемпотентность: тенант с таким email уже есть — возвращаем как есть, // креды НЕ пересчитываем и НЕ меняем (они случайны и неизменны). if existing, ok := s.byEmail[email]; ok { - if sub != "" { + // Персистим только если что-то реально изменилось: иначе лишние записи + // в Redis на каждый логин, а обновлённый NubesSub раньше вообще терялся + // (сохранялся только внутри ветки смены MaxQueues). + changed := false + if sub != "" && existing.NubesSub != sub { existing.NubesSub = sub + changed = true } // Обновляем лимит очередей на актуальное значение из кода при каждом - // логине и персистим в Redis — иначе после рестарта пода из Redis - // восстановится старый лимит (было: тенант навсегда оставался с лимитом, - // заданным при первом логине). + // логине — иначе после рестарта пода из Redis восстановится старый лимит. if existing.MaxQueues != maxQueues { existing.MaxQueues = maxQueues + changed = true + } + if changed { if data, err := json.Marshal(existing); err == nil { persistence.SaveTenantRaw(existing.ID, data) }