diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php index 21b7422..890a196 100644 --- a/src/BusProNet/ApiClient.php +++ b/src/BusProNet/ApiClient.php @@ -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); diff --git a/src/BusProNet/Exception/ImmediateConnectionCloseException.php b/src/BusProNet/Exception/ImmediateConnectionCloseException.php new file mode 100644 index 0000000..aaaf065 --- /dev/null +++ b/src/BusProNet/Exception/ImmediateConnectionCloseException.php @@ -0,0 +1,15 @@ +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),