TimeoutConnector.php 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
  1. <?php
  2. namespace React\Socket;
  3. use React\EventLoop\Loop;
  4. use React\EventLoop\LoopInterface;
  5. use React\Promise\Promise;
  6. final class TimeoutConnector implements ConnectorInterface
  7. {
  8. private $connector;
  9. private $timeout;
  10. private $loop;
  11. /**
  12. * @param ConnectorInterface $connector
  13. * @param float $timeout
  14. * @param ?LoopInterface $loop
  15. */
  16. public function __construct(ConnectorInterface $connector, $timeout, $loop = null)
  17. {
  18. if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
  19. throw new \InvalidArgumentException('Argument #3 ($loop) expected null|React\EventLoop\LoopInterface');
  20. }
  21. $this->connector = $connector;
  22. $this->timeout = $timeout;
  23. $this->loop = $loop ?: Loop::get();
  24. }
  25. public function connect($uri)
  26. {
  27. $promise = $this->connector->connect($uri);
  28. $loop = $this->loop;
  29. $time = $this->timeout;
  30. return new Promise(function ($resolve, $reject) use ($loop, $time, $promise, $uri) {
  31. $timer = null;
  32. $promise = $promise->then(function ($v) use (&$timer, $loop, $resolve) {
  33. if ($timer) {
  34. $loop->cancelTimer($timer);
  35. }
  36. $timer = false;
  37. $resolve($v);
  38. }, function ($v) use (&$timer, $loop, $reject) {
  39. if ($timer) {
  40. $loop->cancelTimer($timer);
  41. }
  42. $timer = false;
  43. $reject($v);
  44. });
  45. // promise already resolved => no need to start timer
  46. if ($timer === false) {
  47. return;
  48. }
  49. // start timeout timer which will cancel the pending promise
  50. $timer = $loop->addTimer($time, function () use ($time, &$promise, $reject, $uri) {
  51. $reject(new \RuntimeException(
  52. 'Connection to ' . $uri . ' timed out after ' . $time . ' seconds (ETIMEDOUT)',
  53. \defined('SOCKET_ETIMEDOUT') ? \SOCKET_ETIMEDOUT : 110
  54. ));
  55. // Cancel pending connection to clean up any underlying resources and references.
  56. // Avoid garbage references in call stack by passing pending promise by reference.
  57. assert(\method_exists($promise, 'cancel'));
  58. $promise->cancel();
  59. $promise = null;
  60. });
  61. }, function () use (&$promise) {
  62. // Cancelling this promise will cancel the pending connection, thus triggering the rejection logic above.
  63. // Avoid garbage references in call stack by passing pending promise by reference.
  64. assert(\method_exists($promise, 'cancel'));
  65. $promise->cancel();
  66. $promise = null;
  67. });
  68. }
  69. }