| namespace App\Traits; | |
| use Illuminate\Http\Client\ConnectionException; | |
| use Throwable; | |
| trait Retryable | |
| { | |
| /** | |
| * Mencoba menjalankan sebuah callable dengan strategi retry exponential backoff. | |
| * | |
| * @param callable $callable Operasi yang akan dieksekusi. | |
| * @param int $maxAttempts Jumlah maksimal percobaan. | |
| * @param int $initialDelayMs Waktu tunda awal dalam milidetik. | |
| * @param float $jitterFactor Faktor untuk menambahkan waktu acak (0 sampai 1). | |
| * @param array $retryableExceptions Daftar exception yang akan memicu retry. Jika kosong, semua Throwable akan dicoba lagi. | |
| * @return mixed | |
| * @throws Throwable | |
| */ | |
| protected function retry( | |
| callable $callable, | |
| int $maxAttempts = 8, | |
| int $initialDelayMs = 1000, | |
| float $jitterFactor = 0.2, | |
| array $retryableExceptions = [ConnectionException::class] | |
| ) { | |
| $lastException = null; | |
| for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { | |
| try { | |
| return $callable(); | |
| } catch (Throwable $e) { | |
| $lastException = $e; | |
| if ($attempt >= $maxAttempts || !$this->shouldRetry($e, $retryableExceptions)) { | |
| throw $e; | |
| } | |
| $delay = (int) ($initialDelayMs * (2 ** ($attempt - 1))); | |
| $jitter = (int) ($delay * $jitterFactor * (random_int(0, 100) / 100)); | |
| $wait = $delay + $jitter; | |
| usleep($wait * 1000); // usleep menggunakan mikrosdetik | |
| } | |
| } | |
| throw $lastException ?? new \RuntimeException('Operasi retry gagal tanpa exception spesifik.'); | |
| } | |
| private function shouldRetry(Throwable $e, array $retryableExceptions): bool | |
| { | |
| if (empty($retryableExceptions)) { | |
| return true; // Selalu retry jika daftar kosong | |
| } | |
| foreach ($retryableExceptions as $retryable) { | |
| if ($e instanceof $retryable) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } |