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); } }