From 1884d3f0ad74f1d2d50163bd513a6d1523b85b7b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:19:02 +0530 Subject: [PATCH 1/7] [java] Update dependency com.google.googlejavaformat:google-java-format to v1.25.2 (#14978) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Selenium CI Bot Co-authored-by: Puja Jagani --- MODULE.bazel | 2 +- java/maven_install.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 11488524b1e01..48d22460488a4 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -177,7 +177,7 @@ maven.install( "com.google.auto:auto-common:1.2.2", "com.google.auto.service:auto-service:1.1.1", "com.google.auto.service:auto-service-annotations:1.1.1", - "com.google.googlejavaformat:google-java-format:jar:1.25.0", + "com.google.googlejavaformat:google-java-format:1.25.2:1.25.0", "com.graphql-java:graphql-java:22.3", "dev.failsafe:failsafe:3.3.2", "io.grpc:grpc-context:1.68.1", diff --git a/java/maven_install.json b/java/maven_install.json index 25b1acef66bdc..3b0014768889a 100644 --- a/java/maven_install.json +++ b/java/maven_install.json @@ -1,6 +1,6 @@ { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": 818842380, + "__INPUT_ARTIFACTS_HASH": 509061279, "__RESOLVED_ARTIFACTS_HASH": 1188602649, "artifacts": { "com.beust:jcommander": { From c6c60a710a41a271fe778d1d03f820b8e538f5df Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 13:34:42 +0530 Subject: [PATCH 2/7] [java] Update dependency org.redisson:redisson to v3.41.0 (#14983) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Selenium CI Bot Co-authored-by: Puja Jagani --- MODULE.bazel | 2 +- java/maven_install.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 48d22460488a4..5f5ba19e515bf 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -217,7 +217,7 @@ maven.install( "org.junit.platform:junit-platform-commons", "org.junit.platform:junit-platform-engine", "org.mockito:mockito-core:5.14.2", - "org.redisson:redisson:3.39.0", + "org.redisson:redisson:3.41.0", "org.slf4j:slf4j-api:2.0.16", "org.slf4j:slf4j-jdk14:2.0.16", "org.tomlj:tomlj:1.1.1", diff --git a/java/maven_install.json b/java/maven_install.json index 3b0014768889a..36fb57e9a1c4c 100644 --- a/java/maven_install.json +++ b/java/maven_install.json @@ -1,7 +1,7 @@ { "__AUTOGENERATED_FILE_DO_NOT_MODIFY_THIS_FILE_MANUALLY": "THERE_IS_NO_DATA_ONLY_ZUUL", - "__INPUT_ARTIFACTS_HASH": 509061279, - "__RESOLVED_ARTIFACTS_HASH": 1188602649, + "__INPUT_ARTIFACTS_HASH": 267304004, + "__RESOLVED_ARTIFACTS_HASH": -1966274188, "artifacts": { "com.beust:jcommander": { "shasums": { @@ -739,10 +739,10 @@ }, "org.redisson:redisson": { "shasums": { - "jar": "6e905eabe16b5b53ee021293ac1fc4b56db2296ecdb341f6e2e27e93e8fefd22", - "sources": "654232025331ac2191a5e522f82fdf56253976c17ea54a269cf0cbcdb738d2d9" + "jar": "c0198bb5963c65ad0216b0eaee2532d6a9730322ee35891ff45f1d8bcc921d14", + "sources": "67145b1a703580f00509eeea2d68bebbc27628be146f3bdd0dec2e14e5d7327f" }, - "version": "3.39.0" + "version": "3.41.0" }, "org.slf4j:slf4j-api": { "shasums": { From 759bd6a8734d4a51a653ed2e63f0785dafceef42 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 14:01:48 +0530 Subject: [PATCH 3/7] [java] Update dependency rules_java to v7.12.4 (#14984) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- MODULE.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MODULE.bazel b/MODULE.bazel index 5f5ba19e515bf..0868026bec668 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -18,7 +18,7 @@ bazel_dep(name = "protobuf", version = "29.2", dev_dependency = True, repo_name bazel_dep(name = "rules_cc", version = "0.0.9", dev_dependency = True) bazel_dep(name = "rules_dotnet", version = "0.17.5") -bazel_dep(name = "rules_java", version = "7.11.1") +bazel_dep(name = "rules_java", version = "7.12.4") bazel_dep(name = "rules_jvm_external", version = "6.6") bazel_dep(name = "rules_nodejs", version = "6.3.2") bazel_dep(name = "rules_oci", version = "1.7.6") From c826fecfec9509e85ef569dd4cf2a59512cbd203 Mon Sep 17 00:00:00 2001 From: joerg1985 <16140691+joerg1985@users.noreply.github.com> Date: Tue, 31 Dec 2024 12:02:39 +0100 Subject: [PATCH 4/7] [grid] ensure --drain-after-session-count is respected with a lot of sessions in the queue (#14987) --- .../selenium/grid/node/local/LocalNode.java | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java index fc5ab455f0439..832732f7a2714 100644 --- a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java +++ b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java @@ -59,9 +59,11 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -133,7 +135,7 @@ public class LocalNode extends Node implements Closeable { private final int connectionLimitPerSession; private final boolean bidiEnabled; - private final AtomicBoolean drainAfterSessions = new AtomicBoolean(); + private final boolean drainAfterSessions; private final List factories; private final Cache currentSessions; private final Cache uploadsTempFileSystem; @@ -142,6 +144,7 @@ public class LocalNode extends Node implements Closeable { private final AtomicInteger pendingSessions = new AtomicInteger(); private final AtomicInteger sessionCount = new AtomicInteger(); private final Runnable shutdown; + private final ReadWriteLock drainLock = new ReentrantReadWriteLock(); protected LocalNode( Tracer tracer, @@ -177,7 +180,7 @@ protected LocalNode( this.factories = ImmutableList.copyOf(factories); Require.nonNull("Registration secret", registrationSecret); this.configuredSessionCount = drainAfterSessionCount; - this.drainAfterSessions.set(this.configuredSessionCount > 0); + this.drainAfterSessions = this.configuredSessionCount > 0; this.sessionCount.set(drainAfterSessionCount); this.cdpEnabled = cdpEnabled; this.bidiEnabled = bidiEnabled; @@ -443,6 +446,9 @@ public Either newSession( CreateSessionRequest sessionRequest) { Require.nonNull("Session request", sessionRequest); + Lock lock = drainLock.readLock(); + lock.lock(); + try (Span span = tracer.getCurrentContext().createSpan("node.new_session")) { AttributeMap attributeMap = tracer.createAttributeMap(); attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(), getClass().getName()); @@ -455,13 +461,14 @@ public Either newSession( span.setAttribute("current.session.count", currentSessionCount); attributeMap.put("current.session.count", currentSessionCount); - if (getCurrentSessionCount() >= maxSessionCount) { + if (currentSessionCount >= maxSessionCount) { span.setAttribute(AttributeKey.ERROR.getKey(), true); span.setStatus(Status.RESOURCE_EXHAUSTED); attributeMap.put("max.session.count", maxSessionCount); span.addEvent("Max session count reached", attributeMap); return Either.left(new RetrySessionRequestException("Max session count reached.")); } + if (isDraining()) { span.setStatus( Status.UNAVAILABLE.withDescription( @@ -492,6 +499,15 @@ public Either newSession( new RetrySessionRequestException("No slot matched the requested capabilities.")); } + if (!decrementSessionCount()) { + slotToUse.release(); + span.setAttribute(AttributeKey.ERROR.getKey(), true); + span.setStatus(Status.RESOURCE_EXHAUSTED); + attributeMap.put("drain.after.session.count", configuredSessionCount); + span.addEvent("Drain after session count reached", attributeMap); + return Either.left(new RetrySessionRequestException("Drain after session count reached.")); + } + UUID uuidForSessionDownloads = UUID.randomUUID(); Capabilities desiredCapabilities = sessionRequest.getDesiredCapabilities(); if (managedDownloadsRequested(desiredCapabilities)) { @@ -548,6 +564,7 @@ public Either newSession( return Either.left(possibleSession.left()); } } finally { + lock.unlock(); checkSessionCount(); } } @@ -1020,20 +1037,40 @@ public void drain() { } private void checkSessionCount() { - if (this.drainAfterSessions.get()) { + if (this.drainAfterSessions) { + Lock lock = drainLock.writeLock(); + if (!lock.tryLock()) { + // in case we can't get a write lock another thread does hold a read lock and will call + // checkSessionCount as soon as he releases the read lock. So we do not need to wait here + // for the other session to start and release the lock, just continue and let the other + // session start to drain the node. + return; + } + try { + int remainingSessions = this.sessionCount.get(); + if (remainingSessions <= 0) { + LOG.info( + String.format( + "Draining Node, configured sessions value (%s) has been reached.", + this.configuredSessionCount)); + drain(); + } + } finally { + lock.unlock(); + } + } + } + + private boolean decrementSessionCount() { + if (this.drainAfterSessions) { int remainingSessions = this.sessionCount.decrementAndGet(); LOG.log( Debug.getDebugLogLevel(), "{0} remaining sessions before draining Node", remainingSessions); - if (remainingSessions <= 0) { - LOG.info( - String.format( - "Draining Node, configured sessions value (%s) has been reached.", - this.configuredSessionCount)); - drain(); - } + return remainingSessions >= 0; } + return true; } private Map toJson() { From 7983769dc25c92d09e699e61506a4e375d4c9958 Mon Sep 17 00:00:00 2001 From: Swastik Baranwal Date: Wed, 1 Jan 2025 15:17:25 +0530 Subject: [PATCH 5/7] [py] add doc for driver_path_env_key (#14997) * Revert "add alias attr to all edge structs" This reverts commit f17dd08dd32998a310510f0abc44e5b484202a4d. * Reapply "add alias attr to all edge structs" This reverts commit cc16e3fe18971d5ecb37e26a37bcda76cda7c48b. oops * [py] add doc for driver_path_env_key [skip ci] --- py/selenium/webdriver/chromium/service.py | 1 + py/selenium/webdriver/edge/service.py | 1 + py/selenium/webdriver/firefox/service.py | 1 + py/selenium/webdriver/safari/service.py | 1 + 4 files changed, 4 insertions(+) diff --git a/py/selenium/webdriver/chromium/service.py b/py/selenium/webdriver/chromium/service.py index f6c71e3ba719a..621b86051a886 100644 --- a/py/selenium/webdriver/chromium/service.py +++ b/py/selenium/webdriver/chromium/service.py @@ -32,6 +32,7 @@ class ChromiumService(service.Service): :param service_args: (Optional) List of args to be passed to the subprocess when launching the executable. :param log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file. :param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`. + :param driver_path_env_key: (Optional) Environment variable to use to get the path to the driver executable. """ def __init__( diff --git a/py/selenium/webdriver/edge/service.py b/py/selenium/webdriver/edge/service.py index ea49d8c5ca3a0..8d7b6b2fbc371 100644 --- a/py/selenium/webdriver/edge/service.py +++ b/py/selenium/webdriver/edge/service.py @@ -32,6 +32,7 @@ class Service(service.ChromiumService): :param log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file. :param service_args: (Optional) List of args to be passed to the subprocess when launching the executable. :param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`. + :param driver_path_env_key: (Optional) Environment variable to use to get the path to the driver executable. """ def __init__( diff --git a/py/selenium/webdriver/firefox/service.py b/py/selenium/webdriver/firefox/service.py index 59c8c18058b3b..d9a715a5d22e0 100644 --- a/py/selenium/webdriver/firefox/service.py +++ b/py/selenium/webdriver/firefox/service.py @@ -32,6 +32,7 @@ class Service(service.Service): :param service_args: (Optional) List of args to be passed to the subprocess when launching the executable. :param log_output: (Optional) int representation of STDOUT/DEVNULL, any IO instance or String path to file. :param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`. + :param driver_path_env_key: (Optional) Environment variable to use to get the path to the driver executable. """ def __init__( diff --git a/py/selenium/webdriver/safari/service.py b/py/selenium/webdriver/safari/service.py index c20e2ec85df05..79448b9789a26 100644 --- a/py/selenium/webdriver/safari/service.py +++ b/py/selenium/webdriver/safari/service.py @@ -32,6 +32,7 @@ class Service(service.Service): :param service_args: (Optional) List of args to be passed to the subprocess when launching the executable. :param env: (Optional) Mapping of environment variables for the new process, defaults to `os.environ`. :param enable_logging: (Optional) Enable logging of the service. Logs can be located at `~/Library/Logs/com.apple.WebDriver/` + :param driver_path_env_key: (Optional) Environment variable to use to get the path to the driver executable. """ def __init__( From 8d5ac5389619ae48db73d6b84e9269c660606f9a Mon Sep 17 00:00:00 2001 From: joerg1985 <16140691+joerg1985@users.noreply.github.com> Date: Wed, 1 Jan 2025 19:19:17 +0100 Subject: [PATCH 6/7] [grid] retry if no node does support the Capabilities (#14986) * [grid] retry if no node does support the Capabilities * [grid] add some unit tests to provoke the failure * [grid] increase the timeout of the tests * [grid] increase the timeouts of the tests * [grid] disable one of the tests for now --------- Co-authored-by: Viet Nguyen Duc --- .../distributor/local/LocalDistributor.java | 5 + .../selenium/grid/node/httpd/NodeServer.java | 9 +- .../selenium/grid/distributor/BUILD.bazel | 49 +++- .../selenium/grid/distributor/DrainTest.java | 248 ++++++++++++++++++ 4 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 java/test/org/openqa/selenium/grid/distributor/DrainTest.java diff --git a/java/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java b/java/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java index 9bf2f880a56ad..b6db25c5dbfab 100644 --- a/java/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java +++ b/java/src/org/openqa/selenium/grid/distributor/local/LocalDistributor.java @@ -586,6 +586,11 @@ public Either newSession( new SessionNotCreatedException("Unable to create new session"); for (Capabilities caps : request.getDesiredCapabilities()) { if (isNotSupported(caps)) { + // e.g. the last node drained, we have to wait for a new to register + lastFailure = + new SessionNotCreatedException( + "Unable to find a node supporting the desired capabilities"); + retry = true; continue; } diff --git a/java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java b/java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java index f1ac54bb62ea7..17123e23cbbf0 100644 --- a/java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java +++ b/java/src/org/openqa/selenium/grid/node/httpd/NodeServer.java @@ -17,7 +17,7 @@ package org.openqa.selenium.grid.node.httpd; -import static java.net.HttpURLConnection.HTTP_NO_CONTENT; +import static java.net.HttpURLConnection.HTTP_OK; import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; import static org.openqa.selenium.grid.config.StandardGridRoles.EVENT_BUS_ROLE; import static org.openqa.selenium.grid.config.StandardGridRoles.HTTPD_ROLE; @@ -131,13 +131,16 @@ protected Handlers createHandlers(Config config) { HttpHandler readinessCheck = req -> { if (node.getStatus().hasCapacity()) { - return new HttpResponse().setStatus(HTTP_NO_CONTENT); + return new HttpResponse() + .setStatus(HTTP_OK) + .setHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString()) + .setContent(Contents.utf8String("Node has capacity available")); } return new HttpResponse() .setStatus(HTTP_UNAVAILABLE) .setHeader("Content-Type", MediaType.PLAIN_TEXT_UTF_8.toString()) - .setContent(Contents.utf8String("No capacity available")); + .setContent(Contents.utf8String("Node has no capacity available")); }; bus.addListener( diff --git a/java/test/org/openqa/selenium/grid/distributor/BUILD.bazel b/java/test/org/openqa/selenium/grid/distributor/BUILD.bazel index fd9ac64e97005..33f21dd8c5ee5 100644 --- a/java/test/org/openqa/selenium/grid/distributor/BUILD.bazel +++ b/java/test/org/openqa/selenium/grid/distributor/BUILD.bazel @@ -1,11 +1,56 @@ load("@rules_jvm_external//:defs.bzl", "artifact") -load("//java:defs.bzl", "JUNIT5_DEPS", "java_test_suite") +load("//java:defs.bzl", "JUNIT5_DEPS", "java_selenium_test_suite", "java_test_suite") load("//java:version.bzl", "TOOLS_JAVA_VERSION") +LARGE_TESTS = [ + "DrainTest.java", +] + +java_selenium_test_suite( + name = "large-tests", + size = "large", + srcs = LARGE_TESTS, + browsers = [ + "chrome", + "firefox", + "edge", + ], + javacopts = [ + "--release", + TOOLS_JAVA_VERSION, + ], + tags = [ + "selenium-remote", + ], + deps = [ + "//java/src/org/openqa/selenium/chrome", + "//java/src/org/openqa/selenium/firefox", + "//java/src/org/openqa/selenium/grid", + "//java/src/org/openqa/selenium/grid/config", + "//java/src/org/openqa/selenium/grid/distributor", + "//java/src/org/openqa/selenium/json", + "//java/src/org/openqa/selenium/remote", + "//java/src/org/openqa/selenium/support", + "//java/test/org/openqa/selenium/environment", + "//java/test/org/openqa/selenium/grid/testing", + "//java/test/org/openqa/selenium/remote/tracing:tracing-support", + "//java/test/org/openqa/selenium/testing:annotations", + "//java/test/org/openqa/selenium/testing:test-base", + artifact("org.junit.jupiter:junit-jupiter-api"), + artifact("org.junit.jupiter:junit-jupiter-params"), + artifact("org.assertj:assertj-core"), + "//java/src/org/openqa/selenium:core", + "//java/src/org/openqa/selenium/remote/http", + ] + JUNIT5_DEPS, +) + java_test_suite( name = "medium-tests", size = "medium", - srcs = glob(["*.java"]), + srcs = glob( + ["*.java"], + exclude = LARGE_TESTS, + ), javacopts = [ "--release", TOOLS_JAVA_VERSION, diff --git a/java/test/org/openqa/selenium/grid/distributor/DrainTest.java b/java/test/org/openqa/selenium/grid/distributor/DrainTest.java new file mode 100644 index 0000000000000..fb8f13d01ce1d --- /dev/null +++ b/java/test/org/openqa/selenium/grid/distributor/DrainTest.java @@ -0,0 +1,248 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.selenium.grid.distributor; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.StringReader; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.grid.commands.Hub; +import org.openqa.selenium.grid.config.CompoundConfig; +import org.openqa.selenium.grid.config.Config; +import org.openqa.selenium.grid.config.MapConfig; +import org.openqa.selenium.grid.config.MemoizedConfig; +import org.openqa.selenium.grid.config.TomlConfig; +import org.openqa.selenium.grid.node.httpd.NodeServer; +import org.openqa.selenium.grid.server.Server; +import org.openqa.selenium.net.PortProber; +import org.openqa.selenium.net.UrlChecker; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.testing.Safely; +import org.openqa.selenium.testing.drivers.Browser; + +class DrainTest { + + private final Browser browser = Objects.requireNonNull(Browser.detect()); + + @Disabled("the Node is terminated calling System.exit, this should be reworked in the future") + @Test + void nodeDoesNotTakeTooManySessions() throws Exception { + String[] rawConfig = + new String[] { + "[events]", + "publish = \"tcp://localhost:" + PortProber.findFreePort() + "\"", + "subscribe = \"tcp://localhost:" + PortProber.findFreePort() + "\"", + "", + "[server]", + "registration-secret = \"feta\"" + }; + + Config baseConfig = + new MemoizedConfig(new TomlConfig(new StringReader(String.join("\n", rawConfig)))); + + Server hub = startHub(baseConfig); + try (AutoCloseable stopHub = () -> Safely.safelyCall(hub::stop); ) { + UrlChecker urlChecker = new UrlChecker(); + urlChecker.waitUntilAvailable( + 5, TimeUnit.SECONDS, hub.getUrl().toURI().resolve("readyz").toURL()); + + // the CI has not enough CPUs so use a fixed number here + int nThreads = 4 * 3; + ExecutorService executor = Executors.newFixedThreadPool(nThreads); + + try { + List> pendingSessions = new ArrayList<>(); + CountDownLatch allPending = new CountDownLatch(nThreads); + + for (int i = 0; i < nThreads; i++) { + CompletableFuture future = + CompletableFuture.supplyAsync( + () -> { + allPending.countDown(); + + return RemoteWebDriver.builder() + .oneOf(browser.getCapabilities()) + .address(hub.getUrl()) + .build(); + }, + executor); + + pendingSessions.add(future); + } + + // ensure all sessions are in the queue + Assertions.assertThat(allPending.await(8, TimeUnit.SECONDS)).isTrue(); + + for (int i = 0; i < nThreads; i += 3) { + // remove all completed futures + assertThat(pendingSessions.removeIf(CompletableFuture::isDone)).isEqualTo(i != 0); + + // start a node draining after 3 sessions + Server node = startNode(baseConfig, hub, 6, 3); + + urlChecker.waitUntilAvailable( + 60, TimeUnit.SECONDS, node.getUrl().toURI().resolve("readyz").toURL()); + + // use nano time to avoid issues with a jumping clock e.g. on WSL2 or due to time-sync + long started = System.nanoTime(); + + // wait for the first to start + CompletableFuture.anyOf(pendingSessions.toArray(CompletableFuture[]::new)) + .get(120, TimeUnit.SECONDS); + + // we want to check not more than 3 are started, polling won't help here + Thread.sleep(Duration.ofNanos(System.nanoTime() - started).multipliedBy(2).toMillis()); + + int stopped = 0; + + for (CompletableFuture future : pendingSessions) { + if (future.isDone()) { + stopped++; + future.get().quit(); + } + } + + // the node should only pick 3 sessions to start, then starts to drain + Assertions.assertThat(stopped).isEqualTo(3); + + // check the node stopped + urlChecker.waitUntilUnavailable( + 40, TimeUnit.SECONDS, node.getUrl().toURI().resolve("readyz").toURL()); + } + } finally { + executor.shutdownNow(); + } + } + } + + @Test + void sessionIsNotRejectedWhenNodeDrains() throws Exception { + String[] rawConfig = + new String[] { + "[events]", + "publish = \"tcp://localhost:" + PortProber.findFreePort() + "\"", + "subscribe = \"tcp://localhost:" + PortProber.findFreePort() + "\"", + "", + "[server]", + "registration-secret = \"feta\"" + }; + + Config baseConfig = + new MemoizedConfig(new TomlConfig(new StringReader(String.join("\n", rawConfig)))); + + Server hub = startHub(baseConfig); + try (AutoCloseable stopHub = () -> Safely.safelyCall(hub::stop); ) { + UrlChecker urlChecker = new UrlChecker(); + urlChecker.waitUntilAvailable( + 5, TimeUnit.SECONDS, hub.getUrl().toURI().resolve("readyz").toURL()); + + ExecutorService executor = Executors.newFixedThreadPool(2); + + try { + Supplier> newDriver = + () -> + CompletableFuture.supplyAsync( + () -> + RemoteWebDriver.builder() + .oneOf(browser.getCapabilities()) + .address(hub.getUrl()) + .build(), + executor); + + CompletableFuture pendingA = newDriver.get(); + CompletableFuture pendingB = newDriver.get(); + + for (int i = 0; i < 16; i++) { + // the node should drain automatically, covered by other tests + startNode(baseConfig, hub, 6, 1); + + // wait for one to start + CompletableFuture.anyOf(pendingA, pendingB).get(80, TimeUnit.SECONDS); + + if (pendingA.isDone() && pendingB.isDone()) { + pendingA.get().quit(); + pendingB.get().quit(); + + throw new IllegalStateException("only one should be started"); + } else if (pendingA.isDone()) { + pendingA.get().quit(); + pendingA = newDriver.get(); + } else if (pendingB.isDone()) { + pendingB.get().quit(); + pendingB = newDriver.get(); + } + } + } finally { + executor.shutdownNow(); + } + } + } + + Server startHub(Config baseConfig) { + Config hubConfig = + new MemoizedConfig( + new CompoundConfig( + new MapConfig( + Map.of( + "server", + Map.of("port", PortProber.findFreePort()), + "events", + Map.of("bind", true), + "distributor", + Map.of("newsession-threadpool-size", "6"))), + baseConfig)); + + return new Hub().asServer(hubConfig).start(); + } + + Server startNode(Config baseConfig, Server hub, int maxSessions, int drainAfter) { + MapConfig additionalNodeConfig = + new MapConfig( + Map.of( + "server", Map.of("port", PortProber.findFreePort()), + "node", + Map.of( + "hub", + hub.getUrl(), + "driver-implementation", + browser.displayName(), + "override-max-sessions", + "true", + "max-sessions", + Integer.toString(maxSessions), + "drain-after-session-count", + drainAfter))); + + Config nodeConfig = new MemoizedConfig(new CompoundConfig(additionalNodeConfig, baseConfig)); + return new NodeServer().asServer(nodeConfig).start(); + } +} From 513c2210edc0104bca1c55b72914a4b71a333e10 Mon Sep 17 00:00:00 2001 From: Swastik Baranwal Date: Thu, 2 Jan 2025 11:57:05 +0530 Subject: [PATCH 7/7] [py] remove xfail_remote for cookie test (#14995) * Revert "add alias attr to all edge structs" This reverts commit f17dd08dd32998a310510f0abc44e5b484202a4d. * [py] remove xfail_remote for cookie test * revert --------- Co-authored-by: Diego Molina --- py/test/selenium/webdriver/common/cookie_tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/py/test/selenium/webdriver/common/cookie_tests.py b/py/test/selenium/webdriver/common/cookie_tests.py index b8e4b479e9305..b3aadebe420b1 100644 --- a/py/test/selenium/webdriver/common/cookie_tests.py +++ b/py/test/selenium/webdriver/common/cookie_tests.py @@ -80,21 +80,18 @@ def test_add_cookie(cookie, driver): assert cookie["name"] in returned -@pytest.mark.xfail_remote(reason="sameSite cookie attribute not implemented") def test_add_cookie_same_site_strict(same_site_cookie_strict, driver): driver.add_cookie(same_site_cookie_strict) returned = driver.get_cookie("foo") assert "sameSite" in returned and returned["sameSite"] == "Strict" -@pytest.mark.xfail_remote(reason="sameSite cookie attribute not implemented") def test_add_cookie_same_site_lax(same_site_cookie_lax, driver): driver.add_cookie(same_site_cookie_lax) returned = driver.get_cookie("foo") assert "sameSite" in returned and returned["sameSite"] == "Lax" -@pytest.mark.xfail_remote(reason="sameSite cookie attribute not implemented") def test_add_cookie_same_site_none(same_site_cookie_none, driver): driver.add_cookie(same_site_cookie_none) # Note that insecure sites (http:) can't set cookies with the Secure directive.