mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 15:57:53 +08:00
Ambiguous idempotency-store failures can occur after the account transaction commits. Scope durable recovery markers to the authenticated admin, retain the operation key across reloads, and recover only an already committed copy without rerunning active work. Constraint: Generic idempotent handlers may legitimately remain active while a recovery lookup is attempted Rejected: Reclaim or rerun an in-progress duplicate request | can execute account creation concurrently Rejected: Recover by source account and key alone | allows another admin to observe the committed copy Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep ambiguous-response recovery read-only and bind durable operation markers to the authenticated actor Tested: Full Go unit suite, go vet, server build, integration-test compilation; frontend lint, typecheck, 1,030 Vitest tests, and production build Not-tested: Docker-backed PostgreSQL integration runtime because Docker is unavailable Related: Wei-Shaw/sub2api#1379 Related: Wei-Shaw/sub2api#2928
119 lines
3.3 KiB
Go
119 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
|
|
}
|
|
|
|
return coordinator.Execute(c.Request.Context(), service.IdempotencyExecuteOptions{
|
|
Scope: scope,
|
|
ActorScope: adminActorScope(c),
|
|
Method: c.Request.Method,
|
|
Route: c.FullPath(),
|
|
IdempotencyKey: c.GetHeader("Idempotency-Key"),
|
|
Payload: payload,
|
|
RequireKey: true,
|
|
TTL: ttl,
|
|
}, execute)
|
|
}
|
|
|
|
func adminActorScope(c *gin.Context) string {
|
|
actorScope := "admin:0"
|
|
if subject, ok := middleware2.GetAuthSubjectFromContext(c); ok {
|
|
actorScope = "admin:" + strconv.FormatInt(subject.UserID, 10)
|
|
}
|
|
return actorScope
|
|
}
|
|
|
|
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)
|
|
}
|