idempotency_helper.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. package admin
  2. import (
  3. "context"
  4. "strconv"
  5. "time"
  6. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  7. "github.com/Wei-Shaw/sub2api/internal/pkg/logger"
  8. "github.com/Wei-Shaw/sub2api/internal/pkg/response"
  9. middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. )
  13. type idempotencyStoreUnavailableMode int
  14. const (
  15. idempotencyStoreUnavailableFailClose idempotencyStoreUnavailableMode = iota
  16. idempotencyStoreUnavailableFailOpen
  17. )
  18. func executeAdminIdempotent(
  19. c *gin.Context,
  20. scope string,
  21. payload any,
  22. ttl time.Duration,
  23. execute func(context.Context) (any, error),
  24. ) (*service.IdempotencyExecuteResult, error) {
  25. coordinator := service.DefaultIdempotencyCoordinator()
  26. if coordinator == nil {
  27. data, err := execute(c.Request.Context())
  28. if err != nil {
  29. return nil, err
  30. }
  31. return &service.IdempotencyExecuteResult{Data: data}, nil
  32. }
  33. return coordinator.Execute(c.Request.Context(), service.IdempotencyExecuteOptions{
  34. Scope: scope,
  35. ActorScope: adminActorScope(c),
  36. Method: c.Request.Method,
  37. Route: c.FullPath(),
  38. IdempotencyKey: c.GetHeader("Idempotency-Key"),
  39. Payload: payload,
  40. RequireKey: true,
  41. TTL: ttl,
  42. }, execute)
  43. }
  44. func adminActorScope(c *gin.Context) string {
  45. actorScope := "admin:0"
  46. if subject, ok := middleware2.GetAuthSubjectFromContext(c); ok {
  47. actorScope = "admin:" + strconv.FormatInt(subject.UserID, 10)
  48. }
  49. return actorScope
  50. }
  51. func executeAdminIdempotentJSON(
  52. c *gin.Context,
  53. scope string,
  54. payload any,
  55. ttl time.Duration,
  56. execute func(context.Context) (any, error),
  57. ) {
  58. executeAdminIdempotentJSONWithMode(c, scope, payload, ttl, idempotencyStoreUnavailableFailClose, execute)
  59. }
  60. func executeAdminIdempotentJSONFailOpenOnStoreUnavailable(
  61. c *gin.Context,
  62. scope string,
  63. payload any,
  64. ttl time.Duration,
  65. execute func(context.Context) (any, error),
  66. ) {
  67. executeAdminIdempotentJSONWithMode(c, scope, payload, ttl, idempotencyStoreUnavailableFailOpen, execute)
  68. }
  69. func executeAdminIdempotentJSONWithMode(
  70. c *gin.Context,
  71. scope string,
  72. payload any,
  73. ttl time.Duration,
  74. mode idempotencyStoreUnavailableMode,
  75. execute func(context.Context) (any, error),
  76. ) {
  77. result, err := executeAdminIdempotent(c, scope, payload, ttl, execute)
  78. if err != nil {
  79. if infraerrors.Code(err) == infraerrors.Code(service.ErrIdempotencyStoreUnavail) {
  80. strategy := "fail_close"
  81. if mode == idempotencyStoreUnavailableFailOpen {
  82. strategy = "fail_open"
  83. }
  84. service.RecordIdempotencyStoreUnavailable(c.FullPath(), scope, "handler_"+strategy)
  85. logger.LegacyPrintf("handler.idempotency", "[Idempotency] store unavailable: method=%s route=%s scope=%s strategy=%s", c.Request.Method, c.FullPath(), scope, strategy)
  86. if mode == idempotencyStoreUnavailableFailOpen {
  87. data, fallbackErr := execute(c.Request.Context())
  88. if fallbackErr != nil {
  89. response.ErrorFrom(c, fallbackErr)
  90. return
  91. }
  92. c.Header("X-Idempotency-Degraded", "store-unavailable")
  93. response.Success(c, data)
  94. return
  95. }
  96. }
  97. if retryAfter := service.RetryAfterSecondsFromError(err); retryAfter > 0 {
  98. c.Header("Retry-After", strconv.Itoa(retryAfter))
  99. }
  100. response.ErrorFrom(c, err)
  101. return
  102. }
  103. if result != nil && result.Replayed {
  104. c.Header("X-Idempotency-Replayed", "true")
  105. }
  106. response.Success(c, result.Data)
  107. }