feat: retry api requests when closed immediately due to overload
This commit is contained in:
@@ -4,17 +4,18 @@ namespace App\BusProNet;
|
||||
|
||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
use App\BusProNet\Model\BaseData;
|
||||
use App\BusProNet\Model\Booking;
|
||||
use App\BusProNet\Model\BookingResponse;
|
||||
use App\BusProNet\Model\BookingUpdate;
|
||||
use App\BusProNet\Model\RegistrationResponse;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Model\PromoVoucher;
|
||||
use App\BusProNet\Model\PurchaseVoucher;
|
||||
use App\BusProNet\Model\RegistrationResponse;
|
||||
use App\BusProNet\Model\ServiceAvailabilityResponse;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\Traits\ApiClientTrait;
|
||||
@@ -489,10 +490,11 @@ class ApiClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends raw XML to the BPN API with key regeneration.
|
||||
* Sends raw XML to the BPN API with key regeneration and automatic retry.
|
||||
*
|
||||
* Parses the XML to extract the request type, regenerates the authentication key
|
||||
* with the current date, and sends the request. Returns the raw XML response.
|
||||
* Automatically retries on immediate connection close (server busy).
|
||||
*
|
||||
* @param string $xml The raw XML request body
|
||||
* @param bool $debug Enable debug mode (XML dumps)
|
||||
@@ -502,6 +504,42 @@ class ApiClient
|
||||
* @throws ApiClientException If the request fails or XML is invalid
|
||||
*/
|
||||
public function sendRawXml(string $xml, bool $debug = false): string
|
||||
{
|
||||
$maxAttempts = $this->config['busy_retry_attempts'];
|
||||
$retryDelay = $this->config['busy_retry_delay'];
|
||||
$lastException = null;
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) {
|
||||
try {
|
||||
return $this->doSendRawXml($xml, $debug);
|
||||
} catch (ImmediateConnectionCloseException $e) {
|
||||
$lastException = $e;
|
||||
|
||||
if ($attempt < $maxAttempts) {
|
||||
$this->logger->warning('BPN server busy, retrying raw XML request', [
|
||||
'attempt' => $attempt,
|
||||
'maxAttempts' => $maxAttempts,
|
||||
'retryDelay' => $retryDelay,
|
||||
]);
|
||||
sleep($retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->error('BPN server busy after all retry attempts (raw XML)', [
|
||||
'attempts' => $maxAttempts,
|
||||
]);
|
||||
|
||||
throw $lastException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual raw XML request to the BPN API.
|
||||
*
|
||||
* @throws ApiClientException
|
||||
* @throws ImmediateConnectionCloseException
|
||||
*/
|
||||
private function doSendRawXml(string $xml, bool $debug = false): string
|
||||
{
|
||||
$requestId = date(DATE_ATOM).uniqid();
|
||||
|
||||
@@ -565,9 +603,52 @@ class ApiClient
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the BPN API with automatic retry on immediate connection close.
|
||||
*
|
||||
* When the BPN server is busy, it may close connections immediately without responding.
|
||||
* This method detects such conditions and automatically retries after a short delay.
|
||||
*
|
||||
* @throws ApiClientException
|
||||
*/
|
||||
private function sendRequest(string $type, array $data, array $additionalArgs = [], bool $debug = false): mixed
|
||||
{
|
||||
$maxAttempts = $this->config['busy_retry_attempts'];
|
||||
$retryDelay = $this->config['busy_retry_delay'];
|
||||
$lastException = null;
|
||||
|
||||
for ($attempt = 1; $attempt <= $maxAttempts; ++$attempt) {
|
||||
try {
|
||||
return $this->doSendRequest($type, $data, $additionalArgs, $debug);
|
||||
} catch (ImmediateConnectionCloseException $e) {
|
||||
$lastException = $e;
|
||||
|
||||
if ($attempt < $maxAttempts) {
|
||||
$this->logger->warning('BPN server busy, retrying request', [
|
||||
'attempt' => $attempt,
|
||||
'maxAttempts' => $maxAttempts,
|
||||
'retryDelay' => $retryDelay,
|
||||
'type' => $type,
|
||||
]);
|
||||
sleep($retryDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->error('BPN server busy after all retry attempts', [
|
||||
'attempts' => $maxAttempts,
|
||||
'type' => $type,
|
||||
]);
|
||||
|
||||
throw $lastException;
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the actual request to the BPN API.
|
||||
*
|
||||
* @throws ApiClientException
|
||||
* @throws ImmediateConnectionCloseException
|
||||
*/
|
||||
private function doSendRequest(string $type, array $data, array $additionalArgs = [], bool $debug = false): mixed
|
||||
{
|
||||
$requestId = date(DATE_ATOM).uniqid();
|
||||
|
||||
@@ -650,6 +731,8 @@ class ApiClient
|
||||
'connection_timeout' => 5,
|
||||
'stream_timeout' => 30,
|
||||
'total_timeout' => 45,
|
||||
'busy_retry_attempts' => 3,
|
||||
'busy_retry_delay' => 1,
|
||||
]);
|
||||
|
||||
return $optionsResolver->resolve($options);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\BusProNet\Exception;
|
||||
|
||||
/**
|
||||
* Thrown when the BPN server closes the connection immediately without sending data.
|
||||
*
|
||||
* This typically indicates the server is busy processing other requests.
|
||||
* Unlike a true timeout, this is a transient condition that may succeed on retry.
|
||||
*/
|
||||
class ImmediateConnectionCloseException extends ApiClientException
|
||||
{
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\BusProNet\Traits;
|
||||
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ImmediateConnectionCloseException;
|
||||
use App\BusProNet\Exception\TimeoutException;
|
||||
|
||||
trait ApiClientTrait
|
||||
@@ -90,6 +91,7 @@ trait ApiClientTrait
|
||||
|
||||
/**
|
||||
* @throws TimeoutException
|
||||
* @throws ImmediateConnectionCloseException
|
||||
*/
|
||||
private function receive($socket, int $totalTimeout): string
|
||||
{
|
||||
@@ -115,6 +117,17 @@ trait ApiClientTrait
|
||||
// Check if stream timed out on this specific read
|
||||
$metadata = stream_get_meta_data($socket);
|
||||
if (true === $metadata['timed_out']) {
|
||||
// Check if this is an immediate rejection (0 bytes, < 1 second)
|
||||
// This indicates the server is busy rather than a true timeout
|
||||
if (0 === strlen($response) && $elapsedTime < 1.0) {
|
||||
$this->logger->warning('Server closed connection immediately', [
|
||||
'elapsed_time' => $elapsedTime,
|
||||
'bytes_received' => 0,
|
||||
'read_attempts' => $readAttempts,
|
||||
]);
|
||||
throw new ImmediateConnectionCloseException('Server closed connection immediately - server may be busy');
|
||||
}
|
||||
|
||||
$this->logger->error('Stream read timeout detected', [
|
||||
'elapsed_time' => $elapsedTime,
|
||||
'bytes_received' => strlen($response),
|
||||
|
||||
Reference in New Issue
Block a user