-
-
Notifications
You must be signed in to change notification settings - Fork 8.6k
make augmentation of HasBiDi/HasDevTools lazy-loaded #16338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Conversation
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
7bf31b3
to
6607c45
Compare
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
6607c45
to
9e23bc1
Compare
PR Reviewer Guide 🔍Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Explore these optional code suggestions:
|
9e23bc1
to
7e7bcf8
Compare
I have some concerns about this PR:
|
@joerg1985
No, it should not. The goal of augmentation is NOT to check all the connectivity issues. The goal is to CAST
But the augmentation is NOT automatically called on startup. AND if I really want to check BiDi connectivity, I would explicitly call some BiDi method on startup. One argument for this PR:
public HasDebugger getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) {
return () -> { ... ALL THE WORK ONLY HERE ...}
}
// or
public HasPermissions getImplementation(Capabilities capabilities, ExecuteMethod executeMethod) {
return new HasPermissions() {
... ALL THE WORK ONLY HERE ...
};
}
|
@asolntsev i only wanted to point to this change in behaviour, i think having the same behaviour for all augmentations is a good idea. So i only have one comment left, all other areas rely on external synchronizations in case multiple threads are involved. |
If I understand your question correctly, then yes, the synchronization is needed in this case. @Test void augmenterThreadSafety() {
RemoteWebDriver driver = new RemoteWebDriver(gridUrl(), new ChromeOptions());
WebDriver webDriver = new Augmenter().augment(driver);
HasDevTools devTools = (HasDevTools) webDriver;
for (int i = 0; i < 10; i++) {
new Thread(() -> {
devTools.getDevTools().getDomains(); // would create multiple instances of `DevTools` without synchronization
}).start();
}
} |
@asolntsev in my mind the client code has to take care about synchronization, in your example:
Other areas of the selenium code do not expect the client code will use different threads. e.g. the code below will end in chaos without synchronization in the client code:
|
Yes, maybe this code wouldn't do anything reasonable, but it's safe. I mean, it doesn't cause connection leak etc. |
@asolntsev lets see what others think about this |
Easy, should be thread-safe. |
@nvborisenko Great! Then can we already merge this PR? |
Sorry, I am not from Java world. How I see the situation how it should be:
If this is what you propose, then I approve PR verbally :) |
Yes, this is exactly my proposal. :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, this approach also avoids resource leakage. Sorry, I approved approach, but not code.. let's wait experts.
I don't have time to dig into the details here, but the current mechanism for avoiding problems with inaccessible devtools is to use I'd say more magic is bad, but Augmenter is already a lot of magic, so... :) |
Good point. Indeed, adding But still, I believe this PR is not about magic at all. It just makes two tiny object lazy-loaded. Absolutely reasonable. P.S. Ironically, capability named "cdp" ("se:cdpEnabled") does NOT affect CDP. |
|
||
@Override | ||
public HasBiDi getImplementation(Capabilities caps, ExecuteMethod executeMethod) { | ||
return new HasBiDi() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not 100% sure about the BiDi part because the user is expressing the intention to use BiDi by setting webSocketUrl: true
. I think it makes sense for CDP because we are doing that implicitly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's right: users can enable or disable BiDi.
But it doesn't matter. Anyway, it's not needed to establish BiDi connection while no BiDi methods are called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get that, but if the user has set the capability, then it means they want to use BiDi methods. I would prefer to avoid this change. The CDP one seems fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wait, why? If users want to use BiDi methods, they will use BiDi methods.
I still don't see any reasons to perform side effects during augmentation.
- Augmenter should only augment (create instance of interfaces).
- Augmenter should not perform any other actions (establish CDP connection, establish BiDi connection etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if the user has set the capability, then it means they want to use BiDi methods
This is big mistake. If I set some capability, it means I set capability. I don't know who is BiDi.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@nvborisenko Diego meant a very specific capability "se:cdpEnabled" which allows to disable the BiDi connection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not 100% sure about the BiDi part because the user is expressing the intention to use BiDi by setting
webSocketUrl: true
. I think it makes sense for CDP because we are doing that implicitly.
@diemol What can I do to get this PR merged?
At least you agree that it makes sense for CDP, right?
Do you want me to revert the BiDiProvider part and keep only DevToolsProvider change? If I do so, will you merge the PR?
(just be worried that then BiDiProvider and DevToolsProvider will behave differently: one would be lazy-loaded and the other eager. One has side effect, and all the other providers don't.)
7e7bcf8
to
b056fad
Compare
b056fad
to
ff739c7
Compare
Otherwise, command `new Augmenter().augment(remoteWebDriver)` fails immediately (even if I don't want to use CDP or BiDi). * Augmenter should only augment (create instance of interfaces). * Augmenter should not perform any other actions (establish CDP connection, establish BiDi connection etc.)
ff739c7
to
f36cdfa
Compare
1. method `getBiDi`/`getDevTools` should establish a connection, BUT 1. method `maybeGet*()` returns connection only if the connection is already established, but should NOT establish a new connection. It's because method `maybeGet*()` is used from `WebDriver.close()` and `WebDriver.quite()`. At this moment, we don't want a new connection, we only want to close the existing connection.
Please. Please? Please! @mkurz @diemol @iampopovich @VietND96 @cgoldberg @joerg1985 @navin772 @vicky-iv @giulong @pujagani |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you don't have socket support...
By default webSocketUrl
is false, so BiDi is not augmented
CDP is on by default, so you can toggle it off with "se:cdpEnabled": false
Please explain why a user should not have to set the capabilities to match the behavior they get?
As for the code itself, I'm a little concerned about doing synchronization things inside this method, but not in other methods. I'd have to think through the implications. If Diego or Joerg say it is fine, then I'll go with their expertise, but in general I'd say not to add the extra code if it isn't needed.
Look, we care about simplicity for our users, right? Honestly, I don't understand why are you trying to make your users more work than needed.
I don't see problems here. Synchronized are exactly those methods which establish a new connection. Exactly as it should be. |
.NET binding never establishes new network connection if it is not required. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if we decide to lazy load DevTools we should do the same for BiDi. Eventually that's going to be default.
I took at deeper look at code and noted some things.
Maybe we can log more info when getDevTools / getBiDi fail
Let's at least log that we're lazy loading since it's a change.
Can we add a test for these?
I'm still deferring to @diemol if he says no. 😄
|
||
return () -> devTools; | ||
CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new); | ||
this.devTools = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
catch & close connection if this has runtime issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've analyzed this code, and came to a conclusion that the only case when this code can throw an exception is when there is no CDP connection. This, there is nothing to close.
Once CDP connection gets established, the following code is just primitive constructor which creates a bunch of simple objets like new v139Events()
, new v139Javascript()
etc.
P.S. Anyway, the current code in trunk
behaves the same.
return () -> devTools; | ||
CdpInfo info = new CdpVersionFinder().match(version).orElseGet(NoOpCdpInfo::new); | ||
this.devTools = | ||
SeleniumCdpConnection.create(caps) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it better for this to be the only thing inside the synchronized block? Caps won't change, so no need to lock when checking them
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we move other lines outside of "synchronized", the only thing that changes is:
if multiple parallel threads call getDevTools()
at the same moment, they will execute these lines multiple times. I don't see any benefits from it.
private final Object lock = new Object(); | ||
|
||
@Override | ||
public Optional<BiDi> maybeGetBiDi() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't backwards compatible; users could be doing (maybeGetBiDi().isPresent()) { … }
which would now break
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@titusfortner This would not fail, this would just return false
.
And this is totally reasonable.
Think what happened before: expression maybeGetBiDi().isPresent()
always opened a BiDi connection, and always returned true. Wasn't it nonce? ;)
P.S. In reality, this method is always used only when closing the webdriver:
if (maybeGetBiDi().isPresent()) bidi.close();
private final Object lock = new Object(); | ||
|
||
@Override | ||
public Optional<DevTools> maybeGetDevTools() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't backwards compatible; users could be doing (maybeGetDevTools().isPresent()) { … }
which would now break
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as before: isPresent
would not fail, it would just return false
(if no connection is open). Used in WebDriver.close()
to close the connection if it had been opened.
Connection connection = new Connection(wsClient, wsUri.toString()); | ||
|
||
biDi = Optional.of(new BiDi(connection)); | ||
biDi = new BiDi(connection); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should make sure we close the connection if there's a problem in the constructor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@titusfortner Do you mean exception in this constructor?
new BiDi(connection)
Then no problems, this constructor cannot throw exceptions. It's a trivial one-liner. It only throws NPE if there is no connection (thus, nothing to close).
.map(conn -> new DevTools(info::getDomains, conn)); | ||
SeleniumCdpConnection.create(caps) | ||
.map(conn -> new DevTools(info::getDomains, conn)) | ||
.orElseThrow(() -> new DevToolsException("Unable to create DevTools connection"));; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: 2 semicolons; our linter should have caught this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also this throws, but doesn't close the connection if it was created and then failure in the map
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- 2 semicolons: ops, fixed. (about linter: maybe it doesn't fail because it's not a critical problem?)
- this throws: again, this code can throw an exception only when CDP connection could not be opened. This, it's notting to close.
User description
When CDP url is not accessible, the command
new Augmenter().augment(remoteWebDriver)
fails immediately - even if I don't want to use CDP or BiDi.🔗 Related Issues
This PR fixes the problem described in #9803, #10132, selenide/selenide#2797, selenide/selenide#3107 etc.
💥 What does this PR do?
This PR fixes
DevToolsProvider
andBiDiProvider
, so that augmentation ofHasBiDi
andHasDevTools
interfaces is now lazy-loaded.Context:
JavascriptExecutor
:This code fails because of CDP connectivity issue during augmentation:
🔄 Types of changes
PR Type
Bug fix
Description
Make
HasBiDi
andHasDevTools
augmentation lazy-loadedFix connection failures when CDP/BiDi URLs are inaccessible
Enable augmentation to succeed even without CDP/BiDi access
Implement double-checked locking pattern for thread safety
Diagram Walkthrough
File Walkthrough
BiDiProvider.java
Implement lazy BiDi connection initialization
java/src/org/openqa/selenium/bidi/BiDiProvider.java
implementation
maybeGetBiDi()
methodgetBiDiUrl()
method staticDevToolsProvider.java
Implement lazy DevTools connection initialization
java/src/org/openqa/selenium/devtools/DevToolsProvider.java
implementation
maybeGetDevTools()
method