idempotency_helper.go 1.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465
  1. package handler
  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. func executeUserIdempotentJSON(
  14. c *gin.Context,
  15. scope string,
  16. payload any,
  17. ttl time.Duration,
  18. execute func(context.Context) (any, error),
  19. ) {
  20. coordinator := service.DefaultIdempotencyCoordinator()
  21. if coordinator == nil {
  22. data, err := execute(c.Request.Context())
  23. if err != nil {
  24. response.ErrorFrom(c, err)
  25. return
  26. }
  27. response.Success(c, data)
  28. return
  29. }
  30. actorScope := "user:0"
  31. if subject, ok := middleware2.GetAuthSubjectFromContext(c); ok {
  32. actorScope = "user:" + strconv.FormatInt(subject.UserID, 10)
  33. }
  34. result, err := coordinator.Execute(c.Request.Context(), service.IdempotencyExecuteOptions{
  35. Scope: scope,
  36. ActorScope: actorScope,
  37. Method: c.Request.Method,
  38. Route: c.FullPath(),
  39. IdempotencyKey: c.GetHeader("Idempotency-Key"),
  40. Payload: payload,
  41. RequireKey: true,
  42. TTL: ttl,
  43. }, execute)
  44. if err != nil {
  45. if infraerrors.Code(err) == infraerrors.Code(service.ErrIdempotencyStoreUnavail) {
  46. service.RecordIdempotencyStoreUnavailable(c.FullPath(), scope, "handler_fail_close")
  47. logger.LegacyPrintf("handler.idempotency", "[Idempotency] store unavailable: method=%s route=%s scope=%s strategy=fail_close", c.Request.Method, c.FullPath(), scope)
  48. }
  49. if retryAfter := service.RetryAfterSecondsFromError(err); retryAfter > 0 {
  50. c.Header("Retry-After", strconv.Itoa(retryAfter))
  51. }
  52. response.ErrorFrom(c, err)
  53. return
  54. }
  55. if result != nil && result.Replayed {
  56. c.Header("X-Idempotency-Replayed", "true")
  57. }
  58. response.Success(c, result.Data)
  59. }