Skip to content

Commit 13cdf5e

Browse files
committed
timeout interceptor to prevent hanging driver promises
1 parent ac3326a commit 13cdf5e

File tree

6 files changed

+136
-64
lines changed

6 files changed

+136
-64
lines changed

README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ use React\EventLoop\Factory as LoopFactory;
5454
use React\Socket\Connector as SocketConnector;
5555
use React\Http\Browser;
5656
use Itnelo\React\WebDriver\Client\W3CClient;
57+
use Itnelo\React\WebDriver\Timeout\Interceptor as TimeoutInterceptor;
5758
use Itnelo\React\WebDriver\SeleniumHubDriver;
5859

5960
$loop = LoopFactory::create();
@@ -82,14 +83,12 @@ $hubClient = new W3CClient(
8283
]
8384
);
8485

86+
$timeoutInterceptor = new TimeoutInterceptor($loop, 30);
87+
8588
$webDriver = new SeleniumHubDriver(
8689
$loop,
8790
$hubClient,
88-
[
89-
'command' => [
90-
'timeout' => 30,
91-
],
92-
]
91+
$timeoutInterceptor
9392
);
9493
```
9594

src/ClientInterface.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public function getSessionIdentifiers(): PromiseInterface;
4343
* Usage example:
4444
*
4545
* ```
46-
* $sessionIdentifierPromise = $webdriver->createSession();
46+
* $sessionIdentifierPromise = $webDriver->createSession();
4747
*
4848
* $sessionIdentifierPromise->then(
4949
* function (string $sessionIdentifier) {

src/SeleniumHubDriver.php

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,17 @@
1515

1616
namespace Itnelo\React\WebDriver;
1717

18+
use Itnelo\React\WebDriver\Timeout\Interceptor as TimeoutInterceptor;
1819
use React\EventLoop\LoopInterface;
1920
use React\Promise\PromiseInterface;
2021
use RuntimeException;
2122
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface as ConfigurationExceptionInterface;
2223
use Symfony\Component\OptionsResolver\OptionsResolver;
23-
use Throwable;
2424
use function React\Promise\reject;
25-
use function React\Promise\Timer\timeout;
2625

26+
/**
27+
* Sends action requests to the Selenium Grid server (hub) and controls their async execution
28+
*/
2729
class SeleniumHubDriver implements WebDriverInterface
2830
{
2931
/**
@@ -40,6 +42,13 @@ class SeleniumHubDriver implements WebDriverInterface
4042
*/
4143
private ClientInterface $hubClient;
4244

45+
/**
46+
* Cancels a driver promise if it isn't resolved within the specified amount of time
47+
*
48+
* @var TimeoutInterceptor
49+
*/
50+
private TimeoutInterceptor $timeoutInterceptor;
51+
4352
/**
4453
* Array of options for the driver
4554
*
@@ -50,38 +59,26 @@ class SeleniumHubDriver implements WebDriverInterface
5059
/**
5160
* SeleniumHubDriver constructor.
5261
*
53-
* @param LoopInterface $loop The event loop reference to manage promise timeouts and other async routines
54-
* @param ClientInterface $hubClient Base client implementation for sending commands to the remote hub server
55-
* @param array $options Array of options for the driver
62+
* @param LoopInterface $loop The event loop reference to manage promise timeouts
63+
* @param ClientInterface $hubClient Base client implementation for sending commands to the server
64+
* @param TimeoutInterceptor $timeoutInterceptor Cancels a driver promise if it isn't resolved for too long
65+
* @param array $options Array of options for the driver
5666
*
5767
* @throws ConfigurationExceptionInterface Whenever an error has been occurred during driver configuration
5868
*/
59-
public function __construct(LoopInterface $loop, ClientInterface $hubClient, array $options = [])
60-
{
69+
public function __construct(
70+
LoopInterface $loop,
71+
ClientInterface $hubClient,
72+
TimeoutInterceptor $timeoutInterceptor,
73+
array $options = []
74+
) {
6175
$optionsResolver = new OptionsResolver();
6276

63-
$optionsResolver
64-
->define('command')
65-
->info('Options to control behavior of the commands, which will be executed on the remote server')
66-
->default(
67-
function (OptionsResolver $requestOptionsResolver) {
68-
$requestOptionsResolver
69-
->define('timeout')
70-
->info(
71-
'Maximum time to wait (in seconds) for command execution '
72-
. '(do not correlate with HTTP timeouts)'
73-
)
74-
->allowedTypes('int')
75-
->default(30)
76-
;
77-
}
78-
)
79-
;
80-
8177
$this->_options = $optionsResolver->resolve($options);
8278

83-
$this->loop = $loop;
84-
$this->hubClient = $hubClient;
79+
$this->loop = $loop;
80+
$this->hubClient = $hubClient;
81+
$this->timeoutInterceptor = $timeoutInterceptor;
8582
}
8683

8784
/**
@@ -111,20 +108,10 @@ public function createSession(): PromiseInterface
111108
{
112109
$sessionIdentifierPromise = $this->hubClient->createSession();
113110

114-
// applying command timeout.
115-
$commandTimeoutInSeconds = $this->_options['command']['timeout'];
116-
117-
// global rejection handler for all internal side effects (timeout inclusive).
118-
// todo: move to the separate service
119-
$sessionIdentifierTimedPromise = timeout($sessionIdentifierPromise, $commandTimeoutInSeconds, $this->loop);
120-
121-
$sessionIdentifierTimedPromise = $sessionIdentifierTimedPromise->otherwise(
122-
function (Throwable $rejectionReason) {
123-
throw new RuntimeException('Unable to finish a session create command.', 0, $rejectionReason);
124-
}
111+
return $this->timeoutInterceptor->applyTimeout(
112+
$sessionIdentifierPromise,
113+
'Unable to complete a session create command.'
125114
);
126-
127-
return $sessionIdentifierTimedPromise;
128115
}
129116

130117
/**

src/Timeout/Interceptor.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the ReactPHP WebDriver <https://github.com/itnelo/reactphp-webdriver>.
5+
*
6+
* (c) 2020 Pavel Petrov <[email protected]>.
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*
11+
* @license https://opensource.org/licenses/mit MIT
12+
*/
13+
14+
declare(strict_types=1);
15+
16+
namespace Itnelo\React\WebDriver\Timeout;
17+
18+
use React\EventLoop\LoopInterface;
19+
use React\Promise\PromiseInterface;
20+
use RuntimeException;
21+
use Throwable;
22+
use function React\Promise\Timer\timeout;
23+
24+
/**
25+
* Cancels a driver promise if it isn't resolved within the specified amount of time
26+
*/
27+
class Interceptor
28+
{
29+
/**
30+
* The event loop reference to track time
31+
*
32+
* @var LoopInterface
33+
*/
34+
private LoopInterface $loop;
35+
36+
/**
37+
* Time (in seconds) to wait until promise will be rejected
38+
*
39+
* @var float
40+
*/
41+
private float $timeout;
42+
43+
/**
44+
* Interceptor constructor.
45+
*
46+
* @param LoopInterface $loop The event loop reference to track time
47+
* @param float $timeout Time (in seconds) to wait until promise will be rejected
48+
*/
49+
public function __construct(LoopInterface $loop, float $timeout)
50+
{
51+
$this->loop = $loop;
52+
$this->timeout = $timeout;
53+
}
54+
55+
/**
56+
* Applies a timeout logic for the given promise
57+
*
58+
* @param PromiseInterface $promise Promise to be timed out
59+
* @param string $rejectionMessage Message for rejection reason
60+
*
61+
* @return PromiseInterface<mixed>
62+
*/
63+
public function applyTimeout(
64+
PromiseInterface $promise,
65+
string $rejectionMessage = 'Unable to complete a command.'
66+
): PromiseInterface {
67+
$timedPromise = timeout($promise, $this->timeout, $this->loop);
68+
69+
$timedPromise = $timedPromise->then(
70+
null,
71+
function (Throwable $rejectionReason) use ($rejectionMessage) {
72+
throw new RuntimeException($rejectionMessage, 0, $rejectionReason);
73+
}
74+
);
75+
76+
return $timedPromise;
77+
}
78+
}

src/WebDriverFactory.php

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
namespace Itnelo\React\WebDriver;
1717

1818
use Itnelo\React\WebDriver\Client\W3CClient;
19+
use Itnelo\React\WebDriver\Timeout\Interceptor as TimeoutInterceptor;
1920
use React\EventLoop\LoopInterface;
2021
use React\Http\Browser;
2122
use React\Socket\Connector as SocketConnector;
@@ -101,7 +102,15 @@ function (OptionsResolver $hubOptionsResolver) {
101102
->info('Options to control behavior of the commands, which will be executed on the remote server')
102103
->default(
103104
function (OptionsResolver $commandOptionsResolver) {
104-
$commandOptionsResolver->setDefined(['timeout']);
105+
$commandOptionsResolver
106+
->define('timeout')
107+
->info(
108+
'Maximum time to wait (in seconds) for command execution '
109+
. '(do not correlate with HTTP timeouts)'
110+
)
111+
->allowedTypes('int')
112+
->default(30)
113+
;
105114
}
106115
)
107116
;
@@ -111,10 +120,11 @@ function (OptionsResolver $commandOptionsResolver) {
111120
$socketConnector = new SocketConnector($loop, $optionsResolved['browser']);
112121
$httpClient = new Browser($loop, $socketConnector);
113122

114-
$hubClient = new W3CClient($httpClient, ['server' => $optionsResolved['hub']]);
123+
$hubClient = new W3CClient($httpClient, ['server' => $optionsResolved['hub']]);
124+
$timeoutInterceptor = new TimeoutInterceptor($loop, $optionsResolved['command']['timeout']);
115125

116-
$webDriverOptions = array_replace_recursive([], ['command' => $optionsResolved['command']]);
117-
$webDriver = new SeleniumHubDriver($loop, $hubClient, $webDriverOptions);
126+
$webDriverOptions = [];
127+
$webDriver = new SeleniumHubDriver($loop, $hubClient, $timeoutInterceptor, $webDriverOptions);
118128

119129
return $webDriver;
120130
}

src/WebDriverInterface.php

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,26 +31,26 @@ interface WebDriverInterface extends ClientInterface
3131
* Usage example:
3232
*
3333
* ```
34-
* $navigationPromise = $webdriver->openUri('https://github.com/itnelo');
34+
* $navigationPromise = $webDriver->openUri('https://github.com/itnelo');
3535
*
3636
* $elementIdentifierPromise = $navigationPromise->then(
37-
* function () use ($webdriver) {
37+
* function () use ($webDriver) {
3838
* // try-catch
39-
* $timeHasComePromise = $webdriver->wait(5.0);
39+
* $timeHasComePromise = $webDriver->wait(5.0);
4040
*
4141
* return $timeHasComePromise->then(
42-
* function () use ($webdriver) {
43-
* return $webdriver->getElementIdentifier('sessionIdentifier', 'xpathQuery');
42+
* function () use ($webDriver) {
43+
* return $webDriver->getElementIdentifier('sessionIdentifier', 'xpathQuery');
4444
* }
4545
* );
4646
* }
4747
* // handle rejection reason (e.g. a connection timeout due to unexpected rate limiting)
4848
* );
4949
*
5050
* $elementClickPromise = $elementIdentifierPromise->then(
51-
* function (string $elementIdentifier) use ($webdriver) {
51+
* function (string $elementIdentifier) use ($webDriver) {
5252
* // try-catch
53-
* return $webdriver->clickElement('sessionIdentifier', $elementIdentifier);
53+
* return $webDriver->clickElement('sessionIdentifier', $elementIdentifier);
5454
* }
5555
* // handle rejection reason (e.g. invalid xpath or element not found error)
5656
* );
@@ -78,27 +78,25 @@ public function wait(float $time): PromiseInterface;
7878
* Usage example:
7979
*
8080
* ```
81-
* $becomeVisiblePromise = $webdriver->waitUntil(
81+
* $becomeVisiblePromise = $webDriver->waitUntil(
8282
* 15.5,
83-
* function () use ($webdriver) {
84-
* $visibilityStatePromise = $webdriver->getElementVisibility(...);
83+
* function () use ($webDriver) {
84+
* $visibilityStatePromise = $webDriver->getElementVisibility(...);
8585
*
8686
* return $visibilityStatePromise->then(
8787
* function (bool $isVisible) {
8888
* if (!$isVisible) {
8989
* throw new RuntimeException("Not visible yet! Let's retry!");
9090
* }
91-
*
92-
* return true;
9391
* }
9492
* );
9593
* }
9694
* );
9795
*
9896
* $becomeVisiblePromise->then(
99-
* function () use ($webdriver) {
97+
* function () use ($webDriver) {
10098
* // try-catch
101-
* $webdriver->clickElement(...); // sending a click command only if we are sure the target is visible.
99+
* $webDriver->clickElement(...); // sending a click command only if we are sure the target is visible.
102100
* }
103101
* // handle case when the element is not visible on the page
104102
* );

0 commit comments

Comments
 (0)