76 lines
2.7 KiB
Go
76 lines
2.7 KiB
Go
package roomapi
|
||
|
||
import (
|
||
"context"
|
||
"net/http"
|
||
"strconv"
|
||
"time"
|
||
|
||
"hyapp/pkg/appcode"
|
||
"hyapp/services/gateway-service/internal/auth"
|
||
"hyapp/services/gateway-service/internal/transport/http/giftlimit"
|
||
"hyapp/services/gateway-service/internal/transport/http/httpkit"
|
||
)
|
||
|
||
const giftCapacityReleaseTimeout = time.Second
|
||
|
||
type giftRateLimitedData struct {
|
||
RetryAfterMS int64 `json:"retry_after_ms"`
|
||
}
|
||
|
||
// acquireGiftCapacity 在任何用户资料或 room-service RPC 前占用容量。
|
||
// 返回的 lease 必须 defer 释放;令牌代表本次请求已经给下游造成的成本,不因后续业务失败退还。
|
||
func (h *Handler) acquireGiftCapacity(writer http.ResponseWriter, request *http.Request, roomID string, totalGiftUnits int) (giftlimit.Lease, bool) {
|
||
if !h.giftCapacityEnabled {
|
||
return giftlimit.Lease{}, true
|
||
}
|
||
if h.giftCapacityLimiter == nil {
|
||
// 生产配置要求保护默认开启;后端缺失时 fail-closed,不能在 Redis/装配故障时静默放开洪峰。
|
||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||
return giftlimit.Lease{}, false
|
||
}
|
||
if totalGiftUnits < 1 {
|
||
totalGiftUnits = 1
|
||
}
|
||
|
||
decision, err := h.giftCapacityLimiter.Acquire(request.Context(), giftlimit.Input{
|
||
AppCode: appcode.FromContext(request.Context()),
|
||
UserID: auth.UserIDFromContext(request.Context()),
|
||
RoomID: roomID,
|
||
RequestID: httpkit.ResponseRequestID(request),
|
||
Weight: totalGiftUnits,
|
||
})
|
||
if err != nil {
|
||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||
return giftlimit.Lease{}, false
|
||
}
|
||
if decision.Allowed {
|
||
return decision.Lease, true
|
||
}
|
||
|
||
retryAfter := decision.RetryAfter
|
||
if retryAfter < time.Millisecond {
|
||
retryAfter = time.Millisecond
|
||
}
|
||
retryAfterMS := retryAfter.Milliseconds()
|
||
// Retry-After 是标准 HTTP 秒级提示;data.retry_after_ms 保留连击送礼所需的毫秒精度。
|
||
writer.Header().Set("Retry-After", strconv.FormatInt((retryAfterMS+999)/1000, 10))
|
||
httpkit.WriteEnvelope(writer, http.StatusTooManyRequests, httpkit.ResponseEnvelope{
|
||
Code: httpkit.CodeRateLimited,
|
||
Message: "rate limited",
|
||
RequestID: httpkit.ResponseRequestID(request),
|
||
Data: giftRateLimitedData{RetryAfterMS: retryAfterMS},
|
||
})
|
||
return giftlimit.Lease{}, false
|
||
}
|
||
|
||
// releaseGiftCapacity 使用与客户端断连解耦的短 context;Redis 暂时失败时租约 TTL 仍会兜底回收。
|
||
func (h *Handler) releaseGiftCapacity(lease giftlimit.Lease) {
|
||
if h.giftCapacityLimiter == nil {
|
||
return
|
||
}
|
||
ctx, cancel := context.WithTimeout(context.Background(), giftCapacityReleaseTimeout)
|
||
defer cancel()
|
||
_ = h.giftCapacityLimiter.Release(ctx, lease)
|
||
}
|