116 lines
3.3 KiB
Go
116 lines
3.3 KiB
Go
package admin
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"time"
|
|
|
|
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/logger"
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
|
|
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type idempotencyStoreUnavailableMode int
|
|
|
|
const (
|
|
idempotencyStoreUnavailableFailClose idempotencyStoreUnavailableMode = iota
|
|
idempotencyStoreUnavailableFailOpen
|
|
)
|
|
|
|
func executeAdminIdempotent(
|
|
c *gin.Context,
|
|
scope string,
|
|
payload any,
|
|
ttl time.Duration,
|
|
execute func(context.Context) (any, error),
|
|
) (*service.IdempotencyExecuteResult, error) {
|
|
coordinator := service.DefaultIdempotencyCoordinator()
|
|
if coordinator == nil {
|
|
data, err := execute(c.Request.Context())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &service.IdempotencyExecuteResult{Data: data}, nil
|
|
}
|
|
|
|
actorScope := "admin:0"
|
|
if subject, ok := middleware2.GetAuthSubjectFromContext(c); ok {
|
|
actorScope = "admin:" + strconv.FormatInt(subject.UserID, 10)
|
|
}
|
|
|
|
return coordinator.Execute(c.Request.Context(), service.IdempotencyExecuteOptions{
|
|
Scope: scope,
|
|
ActorScope: actorScope,
|
|
Method: c.Request.Method,
|
|
Route: c.FullPath(),
|
|
IdempotencyKey: c.GetHeader("Idempotency-Key"),
|
|
Payload: payload,
|
|
RequireKey: true,
|
|
TTL: ttl,
|
|
}, execute)
|
|
}
|
|
|
|
func executeAdminIdempotentJSON(
|
|
c *gin.Context,
|
|
scope string,
|
|
payload any,
|
|
ttl time.Duration,
|
|
execute func(context.Context) (any, error),
|
|
) {
|
|
executeAdminIdempotentJSONWithMode(c, scope, payload, ttl, idempotencyStoreUnavailableFailClose, execute)
|
|
}
|
|
|
|
func executeAdminIdempotentJSONFailOpenOnStoreUnavailable(
|
|
c *gin.Context,
|
|
scope string,
|
|
payload any,
|
|
ttl time.Duration,
|
|
execute func(context.Context) (any, error),
|
|
) {
|
|
executeAdminIdempotentJSONWithMode(c, scope, payload, ttl, idempotencyStoreUnavailableFailOpen, execute)
|
|
}
|
|
|
|
func executeAdminIdempotentJSONWithMode(
|
|
c *gin.Context,
|
|
scope string,
|
|
payload any,
|
|
ttl time.Duration,
|
|
mode idempotencyStoreUnavailableMode,
|
|
execute func(context.Context) (any, error),
|
|
) {
|
|
result, err := executeAdminIdempotent(c, scope, payload, ttl, execute)
|
|
if err != nil {
|
|
if infraerrors.Code(err) == infraerrors.Code(service.ErrIdempotencyStoreUnavail) {
|
|
strategy := "fail_close"
|
|
if mode == idempotencyStoreUnavailableFailOpen {
|
|
strategy = "fail_open"
|
|
}
|
|
service.RecordIdempotencyStoreUnavailable(c.FullPath(), scope, "handler_"+strategy)
|
|
logger.LegacyPrintf("handler.idempotency", "[Idempotency] store unavailable: method=%s route=%s scope=%s strategy=%s", c.Request.Method, c.FullPath(), scope, strategy)
|
|
if mode == idempotencyStoreUnavailableFailOpen {
|
|
data, fallbackErr := execute(c.Request.Context())
|
|
if fallbackErr != nil {
|
|
response.ErrorFrom(c, fallbackErr)
|
|
return
|
|
}
|
|
c.Header("X-Idempotency-Degraded", "store-unavailable")
|
|
response.Success(c, data)
|
|
return
|
|
}
|
|
}
|
|
if retryAfter := service.RetryAfterSecondsFromError(err); retryAfter > 0 {
|
|
c.Header("Retry-After", strconv.Itoa(retryAfter))
|
|
}
|
|
response.ErrorFrom(c, err)
|
|
return
|
|
}
|
|
if result != nil && result.Replayed {
|
|
c.Header("X-Idempotency-Replayed", "true")
|
|
}
|
|
response.Success(c, result.Data)
|
|
}
|