| 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) |
| } |
|
|