137 lines
4.5 KiB
PHP
137 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace App\BusProNet\Traits;
|
|
|
|
use App\BusProNet\Exception\ApiClientException;
|
|
use App\BusProNet\Exception\TimeoutException;
|
|
|
|
trait ApiClientTrait
|
|
{
|
|
private float $operationStartTime;
|
|
|
|
/**
|
|
* @throws ApiClientException
|
|
* @throws TimeoutException
|
|
*/
|
|
private function connect(string $host, int $port, int $maxRetries = 25, int $connectionTimeout = 5, int $streamTimeout = 30, int $totalTimeout = 45)
|
|
{
|
|
$this->operationStartTime = microtime(true);
|
|
|
|
$tries = 1;
|
|
$errNo = $errStr = '';
|
|
$errorCodesForRetry = [
|
|
SOCKET_ECONNREFUSED,
|
|
SOCKET_EBADF,
|
|
];
|
|
|
|
$openSocket = function (&$errNo, &$errStr) use ($host, $port, $connectionTimeout) {
|
|
return @fsockopen(
|
|
$host,
|
|
$port,
|
|
$errNo,
|
|
$errStr,
|
|
$connectionTimeout
|
|
);
|
|
};
|
|
|
|
$socket = $openSocket($errNo, $errStr);
|
|
|
|
while (false === $socket && true === in_array($errNo, $errorCodesForRetry) && $maxRetries > $tries) {
|
|
// Check if we've exceeded total timeout during retries
|
|
if (microtime(true) - $this->operationStartTime > $totalTimeout) {
|
|
$this->logger->error('Connection retry timeout exceeded', [
|
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
|
'total_timeout' => $totalTimeout,
|
|
]);
|
|
throw new TimeoutException('Connection timeout exceeded during retries');
|
|
}
|
|
|
|
$this->logger->warning('Could not connect to socket, retrying', [
|
|
'error_message' => $errStr,
|
|
'error_number' => $errNo,
|
|
'attempt' => $tries,
|
|
]);
|
|
++$tries;
|
|
sleep(1);
|
|
$socket = $openSocket($errNo, $errStr);
|
|
}
|
|
|
|
if (false !== $socket) {
|
|
stream_set_timeout($socket, $streamTimeout);
|
|
} else {
|
|
$this->logger->error('Unable to open socket', [
|
|
'error_message' => $errStr,
|
|
'error_number' => $errNo,
|
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
|
]);
|
|
throw new ApiClientException('Unable to open socket');
|
|
}
|
|
|
|
return $socket;
|
|
}
|
|
|
|
/**
|
|
* @throws TimeoutException
|
|
*/
|
|
private function send($socket, string $data, int $totalTimeout): void
|
|
{
|
|
// Check total timeout before sending
|
|
if (microtime(true) - $this->operationStartTime > $totalTimeout) {
|
|
$this->logger->error('Total timeout exceeded before send', [
|
|
'elapsed_time' => microtime(true) - $this->operationStartTime,
|
|
]);
|
|
throw new TimeoutException('Total operation timeout exceeded before send');
|
|
}
|
|
|
|
// message length is prepended to actual message
|
|
$send = sprintf('%010s', strlen($data)).$data;
|
|
fwrite($socket, $send);
|
|
}
|
|
|
|
/**
|
|
* @throws TimeoutException
|
|
*/
|
|
private function receive($socket, int $totalTimeout): string
|
|
{
|
|
$response = '';
|
|
$readAttempts = 0;
|
|
|
|
while (false === feof($socket)) {
|
|
// Check total timeout before each read
|
|
$elapsedTime = microtime(true) - $this->operationStartTime;
|
|
if ($elapsedTime > $totalTimeout) {
|
|
$this->logger->error('Total timeout exceeded during receive', [
|
|
'elapsed_time' => $elapsedTime,
|
|
'total_timeout' => $totalTimeout,
|
|
'bytes_received' => strlen($response),
|
|
'read_attempts' => $readAttempts,
|
|
]);
|
|
throw new TimeoutException('Total operation timeout exceeded while receiving data');
|
|
}
|
|
|
|
$chunk = fread($socket, 4096);
|
|
++$readAttempts;
|
|
|
|
// Check if stream timed out on this specific read
|
|
$metadata = stream_get_meta_data($socket);
|
|
if (true === $metadata['timed_out']) {
|
|
$this->logger->error('Stream read timeout detected', [
|
|
'elapsed_time' => $elapsedTime,
|
|
'bytes_received' => strlen($response),
|
|
'read_attempts' => $readAttempts,
|
|
]);
|
|
throw new TimeoutException('Stream timeout while reading from socket');
|
|
}
|
|
|
|
$response .= $chunk;
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
|
|
private function disconnect($socket): void
|
|
{
|
|
@fclose($socket);
|
|
}
|
|
}
|