111 lines
3.2 KiB
PHP
111 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use Closure;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
class IdempotencyMiddleware
|
|
{
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
$idempotencyKey = $request->header('X-Idempotency-Key');
|
|
|
|
if (! is_string($idempotencyKey) || ! Str::isUuid($idempotencyKey)) {
|
|
return response()->json([
|
|
'message' => 'X-Idempotency-Key MUST be a valid UUID.',
|
|
], Response::HTTP_BAD_REQUEST);
|
|
}
|
|
|
|
$cacheKey = $this->cacheKey($request, $idempotencyKey);
|
|
$lockKey = $this->lockKey($request, $idempotencyKey);
|
|
|
|
// Проверяем чтобы не выполнять код
|
|
$cached = Cache::get($cacheKey);
|
|
if (is_array($cached)) {
|
|
return $this->replayResponse($cached);
|
|
}
|
|
|
|
$lock = Cache::lock($lockKey, config('slots.idempotency_lock_seconds'));
|
|
if (! $lock->get()) {
|
|
return $this->waitForCachedResponse($cacheKey) ?? $this->inProgressResponse();
|
|
}
|
|
|
|
$response = $next($request);
|
|
|
|
try {
|
|
$cached = Cache::get($cacheKey);
|
|
if (is_array($cached)) {
|
|
return $this->replayResponse($cached);
|
|
}
|
|
|
|
if ($response instanceof JsonResponse) {
|
|
Cache::put($cacheKey, [
|
|
'status' => $response->getStatusCode(),
|
|
'body' => $response->getData(true),
|
|
], config('slots.idempotency_cache_ttl_seconds'));
|
|
}
|
|
|
|
return $response;
|
|
} finally {
|
|
$lock->release();
|
|
}
|
|
}
|
|
|
|
private function waitForCachedResponse(string $cacheKey): ?JsonResponse
|
|
{
|
|
$deadline = microtime(true) + config('slots.idempotency_wait_seconds');
|
|
|
|
while (microtime(true) < $deadline) {
|
|
$cached = Cache::get($cacheKey);
|
|
if (is_array($cached) && isset($cached['body'], $cached['status'])) {
|
|
return $this->replayResponse($cached);
|
|
}
|
|
|
|
usleep(100_000);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function inProgressResponse(): ?JsonResponse
|
|
{
|
|
return response()->json([
|
|
'message' => 'Conflict. Retry with same idempotency key',
|
|
], Response::HTTP_CONFLICT)
|
|
->header('X-Retry-After', 1);
|
|
}
|
|
|
|
/**
|
|
* @param array{status: int, body: string} $data
|
|
*/
|
|
private function replayResponse(array $data): JsonResponse
|
|
{
|
|
return response()->json($data['body'], (int) $data['status']);
|
|
}
|
|
|
|
private function cacheKey(Request $request, string $idempotencyKey): string
|
|
{
|
|
return sprintf(
|
|
'idempotency:response:%s:%s:%s',
|
|
strtoupper($request->method()),
|
|
$request->path(),
|
|
$idempotencyKey
|
|
);
|
|
}
|
|
|
|
private function lockKey(Request $request, string $idempotencyKey): string
|
|
{
|
|
return sprintf(
|
|
'idempotency:lock:%s:%s:%s',
|
|
strtoupper($request->method()),
|
|
$request->path(),
|
|
$idempotencyKey
|
|
);
|
|
}
|
|
}
|