fix: concurrency safety and API correctness from code review
Some checks failed
CI / test (push) Has been cancelled
CI / golangci-lint (push) Has been cancelled
Security Scan / backend-security (push) Has been cancelled
Security Scan / frontend-security (push) Has been cancelled

B3 (HIGH): sora_generation_service.go - Add panic recovery to parallel
S3 URL fetching goroutines. Without recovery, a panic in GetAccessURL
would skip wg.Done() causing wg.Wait() to hang indefinitely.

B2 (MEDIUM): subscription_service.go:549 - Replace bare goroutine
with safego.Go() for consistent panic recovery pattern. All other async
calls in this file already use safego.

B4 (MEDIUM): admin/sora_handler.go - Change ClearUserStorage response
from 200 no-op to 410 Gone. The per-user storage quota was fully removed;
returning success was misleading to callers.
This commit is contained in:
User
2026-04-18 13:16:05 +08:00
parent 820b7099ab
commit 3a0ca7f57f
3 changed files with 25 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
package admin package admin
import ( import (
"net/http"
"strconv" "strconv"
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination" "github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
@@ -167,8 +168,9 @@ func (h *SoraHandler) ListGenerations(c *gin.Context) {
// ClearUserStorage 清除用户的 Sora 存储空间(已弃用)。 // ClearUserStorage 清除用户的 Sora 存储空间(已弃用)。
// //
// Deprecated: Per-user storage tracking has been removed. // Deprecated: Per-user storage tracking has been removed.
// This endpoint now returns a success no-op. It will be removed in a future version. // This endpoint now returns 410 Gone. Per-user Sora storage quota tracking was
// Clients should stop calling this endpoint. // fully removed in the Sora storage refactoring. Storage management is now
// handled at the system-default level via SoraQuotaService.
// //
// DELETE /api/v1/admin/sora/users/:id/storage // DELETE /api/v1/admin/sora/users/:id/storage
func (h *SoraHandler) ClearUserStorage(c *gin.Context) { func (h *SoraHandler) ClearUserStorage(c *gin.Context) {
@@ -178,18 +180,20 @@ func (h *SoraHandler) ClearUserStorage(c *gin.Context) {
return return
} }
// 重置用户的存储使用量 // Verify user exists before responding
// NOTE: Per-user SoraStorageUsedBytes field removed. ctx := c.Request.Context()
// Storage clearing now handled at the SoraGenerationService level if needed. if _, err := h.userRepo.GetByID(ctx, userID); err != nil {
_, err = h.userRepo.GetByID(c.Request.Context(), userID)
if err != nil {
response.ErrorFrom(c, err) response.ErrorFrom(c, err)
return return
} }
// TODO: Implement storage cleanup via SoraGenerationService
c.Header("Deprecation", "true") c.Header("Deprecation", "true")
c.Header("Sunset", "2026-12-31") c.Header("Sunset", "2026-12-31")
c.Header("Warning", `299 - "Deprecated API: use SoraGenerationService for storage management"`) c.Header("Warning", `299 - "Gone: per-user storage tracking removed, see SoraQuotaService"`)
response.Success(c, gin.H{"message": "User Sora storage cleared (no-op: per-user tracking removed)", "deprecated": true}) c.JSON(http.StatusGone, gin.H{
"error": "This endpoint is no longer available",
"message": "Per-user Sora storage quota tracking has been removed. Storage is now managed at system level.",
"sunset": "2026-12-31",
"deprecated": true,
})
} }

View File

@@ -308,6 +308,15 @@ func (s *SoraGenerationService) ResolveMediaURLs(ctx context.Context, gen *SoraG
wg.Add(1) wg.Add(1)
go func(i int, objectKey string) { go func(i int, objectKey string) {
defer wg.Done() defer wg.Done()
defer func() {
if r := recover(); r != nil {
errMu.Lock()
if firstErr == nil {
firstErr = fmt.Errorf("goroutine panic fetching S3 URL: %v", r)
}
errMu.Unlock()
}
}()
url, err := s.s3Storage.GetAccessURL(ctx, objectKey) url, err := s.s3Storage.GetAccessURL(ctx, objectKey)
if err != nil { if err != nil {
errMu.Lock() errMu.Lock()

View File

@@ -546,11 +546,11 @@ func (s *SubscriptionService) ExtendSubscription(ctx context.Context, subscripti
s.InvalidateSubCache(sub.UserID, sub.GroupID) s.InvalidateSubCache(sub.UserID, sub.GroupID)
if s.billingCacheService != nil { if s.billingCacheService != nil {
userID, groupID := sub.UserID, sub.GroupID userID, groupID := sub.UserID, sub.GroupID
go func() { safego.Go("service.billing-cache-invalidate", func() {
cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) cacheCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
_ = s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID) _ = s.billingCacheService.InvalidateSubscription(cacheCtx, userID, groupID)
}() }, nil)
} }
return s.userSubRepo.GetByID(ctx, subscriptionID) return s.userSubRepo.GetByID(ctx, subscriptionID)