diff --git a/java/server/src/org/openqa/selenium/grid/distributor/gridmodel/redis/RedisGridModel.java b/java/server/src/org/openqa/selenium/grid/distributor/gridmodel/redis/RedisGridModel.java index 79209d3f80054..38beb0a6f2bee 100644 --- a/java/server/src/org/openqa/selenium/grid/distributor/gridmodel/redis/RedisGridModel.java +++ b/java/server/src/org/openqa/selenium/grid/distributor/gridmodel/redis/RedisGridModel.java @@ -445,4 +445,7 @@ public NodeStatus rewrite(NodeStatus status, Availability availability) { status.getOsInfo()); } + public GridRedisClient getRedisClient() { + return redisClient; + } } diff --git a/java/server/test/org/openqa/selenium/grid/distributor/gridmodel/RedisGridModelTest.java b/java/server/test/org/openqa/selenium/grid/distributor/gridmodel/RedisGridModelTest.java index 18259b250e5b2..54a7c98c392f0 100644 --- a/java/server/test/org/openqa/selenium/grid/distributor/gridmodel/RedisGridModelTest.java +++ b/java/server/test/org/openqa/selenium/grid/distributor/gridmodel/RedisGridModelTest.java @@ -128,10 +128,6 @@ public void setUp() throws URISyntaxException { .build(); } - @After - public void cleanUp() { - - } @AfterClass public static void tearDownRedisServer() { safelyCall(() -> server.stop()); diff --git a/java/server/test/org/openqa/selenium/grid/distributor/gridmodel/RedisLocalDistributorTest.java b/java/server/test/org/openqa/selenium/grid/distributor/gridmodel/RedisLocalDistributorTest.java new file mode 100644 index 0000000000000..bfde6c242eaf9 --- /dev/null +++ b/java/server/test/org/openqa/selenium/grid/distributor/gridmodel/RedisLocalDistributorTest.java @@ -0,0 +1,407 @@ +// 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.gridmodel; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.ImmutableCapabilities; +import org.openqa.selenium.SessionNotCreatedException; +import org.openqa.selenium.events.EventBus; +import org.openqa.selenium.events.local.GuavaEventBus; +import org.openqa.selenium.grid.data.CreateSessionResponse; +import org.openqa.selenium.grid.data.DefaultSlotMatcher; +import org.openqa.selenium.grid.data.DistributorStatus; +import org.openqa.selenium.grid.data.NodeStatus; +import org.openqa.selenium.grid.data.NodeStatusEvent; +import org.openqa.selenium.grid.data.RequestId; +import org.openqa.selenium.grid.data.Session; +import org.openqa.selenium.grid.distributor.Distributor; +import org.openqa.selenium.grid.distributor.GridModel; +import org.openqa.selenium.grid.distributor.gridmodel.redis.RedisGridModel; +import org.openqa.selenium.grid.distributor.local.LocalDistributor; +import org.openqa.selenium.grid.distributor.selector.DefaultSlotSelector; +import org.openqa.selenium.grid.node.Node; +import org.openqa.selenium.grid.node.local.LocalNode; +import org.openqa.selenium.grid.security.Secret; +import org.openqa.selenium.grid.sessionmap.local.LocalSessionMap; +import org.openqa.selenium.grid.sessionqueue.NewSessionQueue; +import org.openqa.selenium.grid.data.SessionRequest; +import org.openqa.selenium.grid.sessionqueue.local.LocalNewSessionQueue; +import org.openqa.selenium.grid.testing.TestSessionFactory; +import org.openqa.selenium.internal.Either; +import org.openqa.selenium.net.PortProber; +import org.openqa.selenium.remote.HttpSessionId; +import org.openqa.selenium.remote.SessionId; +import org.openqa.selenium.remote.http.HttpClient; +import org.openqa.selenium.remote.http.HttpHandler; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; +import org.openqa.selenium.remote.tracing.DefaultTestTracer; +import org.openqa.selenium.remote.tracing.Tracer; +import redis.embedded.RedisServer; + +import java.io.UncheckedIOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.openqa.selenium.grid.data.Availability.DRAINING; +import static org.openqa.selenium.remote.Dialect.W3C; +import static org.openqa.selenium.remote.http.HttpMethod.GET; +import static org.openqa.selenium.testing.Safely.safelyCall; + +public class RedisLocalDistributorTest { + + private final Secret registrationSecret = new Secret("bavarian smoked"); + private RedisServer server; + private Tracer tracer; + private EventBus bus; + private HttpClient.Factory clientFactory; + private URI uri; + private URI redisUri; + private Node localNode; + private RedisGridModel gridModel; + + @Before + public void setUp() throws URISyntaxException { + redisUri = new URI("redis://localhost:" + PortProber.findFreePort()); + server = RedisServer.builder().port(redisUri.getPort()).build(); + server.start(); + + tracer = DefaultTestTracer.createTracer(); + bus = new GuavaEventBus(); + clientFactory = HttpClient.Factory.createDefault(); + + Capabilities caps = new ImmutableCapabilities("browserName", "cheese"); + uri = new URI("http://localhost:1234"); + localNode = LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add(caps, new TestSessionFactory((id, c) -> new Handler(c))) + .maximumConcurrentSessions(2) + .build(); + + gridModel = new RedisGridModel(bus, redisUri); + } + + @After + public void tearDownRedisServer() { + gridModel.getRedisClient().close(); + safelyCall(() -> server.stop()); + } + + @Test + public void testAddNodeToDistributor() { + NewSessionQueue queue = new LocalNewSessionQueue( + tracer, + bus, + new DefaultSlotMatcher(), + Duration.ofSeconds(2), + Duration.ofSeconds(2), + registrationSecret); + Distributor distributor = new LocalDistributor( + tracer, + bus, + clientFactory, + new LocalSessionMap(tracer, bus), + queue, + gridModel, + new DefaultSlotSelector(), + registrationSecret, + Duration.ofMinutes(5), + false); + distributor.add(localNode); + DistributorStatus status = distributor.getStatus(); + + //Check the size + final Set nodes = status.getNodes(); + assertThat(nodes.size()).isEqualTo(1); + + //Check a couple attributes + NodeStatus distributorNode = nodes.iterator().next(); + assertThat(distributorNode.getNodeId()).isEqualByComparingTo(localNode.getId()); + assertThat(distributorNode.getExternalUri()).isEqualTo(uri); + } + + @Test + public void testShouldNotAddNodeWithWrongSecret() { + Secret secret = new Secret("my_secret"); + NewSessionQueue queue = new LocalNewSessionQueue( + tracer, + bus, + new DefaultSlotMatcher(), + Duration.ofSeconds(2), + Duration.ofSeconds(2), + registrationSecret); + Distributor secretDistributor = new LocalDistributor( + tracer, + bus, + clientFactory, + new LocalSessionMap(tracer, bus), + queue, + gridModel, + new DefaultSlotSelector(), + secret, + Duration.ofMinutes(5), + false); + bus.fire(new NodeStatusEvent(localNode.getStatus())); + DistributorStatus status = secretDistributor.getStatus(); + + //Check the size + final Set nodes = status.getNodes(); + assertThat(nodes.size()).isEqualTo(0); + } + + @Test + public void testRemoveNodeFromDistributor() { + NewSessionQueue queue = new LocalNewSessionQueue( + tracer, + bus, + new DefaultSlotMatcher(), + Duration.ofSeconds(2), + Duration.ofSeconds(2), + registrationSecret); + Distributor distributor = new LocalDistributor( + tracer, + bus, + clientFactory, + new LocalSessionMap(tracer, bus), + queue, + gridModel, + new DefaultSlotSelector(), + registrationSecret, + Duration.ofMinutes(5), + false); + distributor.add(localNode); + + //Check the size + DistributorStatus statusBefore = distributor.getStatus(); + final Set nodesBefore = statusBefore.getNodes(); + assertThat(nodesBefore.size()).isEqualTo(1); + + //Recheck the status--should be zero + distributor.remove(localNode.getId()); + DistributorStatus statusAfter = distributor.getStatus(); + final Set nodesAfter = statusAfter.getNodes(); + assertThat(nodesAfter.size()).isEqualTo(0); + } + + @Test + public void testAddSameNodeTwice() { + NewSessionQueue queue = new LocalNewSessionQueue( + tracer, + bus, + new DefaultSlotMatcher(), + Duration.ofSeconds(2), + Duration.ofSeconds(2), + registrationSecret); + Distributor distributor = new LocalDistributor( + tracer, + bus, + clientFactory, + new LocalSessionMap(tracer, bus), + queue, + gridModel, + new DefaultSlotSelector(), + registrationSecret, + Duration.ofMinutes(5), + false); + distributor.add(localNode); + distributor.add(localNode); + DistributorStatus status = distributor.getStatus(); + + //Should only be one node after dupe check + final Set nodes = status.getNodes(); + assertThat(nodes.size()).isEqualTo(1); + } + + @Test + public void shouldBeAbleToAddMultipleSessionsConcurrently() throws Exception { + NewSessionQueue queue = new LocalNewSessionQueue( + tracer, + bus, + new DefaultSlotMatcher(), + Duration.ofSeconds(2), + Duration.ofSeconds(2), + registrationSecret); + LocalDistributor distributor = new LocalDistributor( + tracer, + bus, + clientFactory, + new LocalSessionMap(tracer, bus), + queue, + gridModel, + new DefaultSlotSelector(), + registrationSecret, + Duration.ofMinutes(5), + false); + + // Add one node to ensure that everything is created in that. + Capabilities caps = new ImmutableCapabilities("browserName", "cheese"); + + class VerifyingHandler extends Session implements HttpHandler { + private VerifyingHandler(SessionId id, Capabilities capabilities) { + super(id, uri, new ImmutableCapabilities(), capabilities, Instant.now()); + } + + @Override + public HttpResponse execute(HttpRequest req) { + Optional id = HttpSessionId.getSessionId(req.getUri()).map(SessionId::new); + assertThat(id).isEqualTo(Optional.of(getId())); + return new HttpResponse(); + } + } + + // Only use one node. + Node node = LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add(caps, new TestSessionFactory(VerifyingHandler::new)) + .add(caps, new TestSessionFactory(VerifyingHandler::new)) + .add(caps, new TestSessionFactory(VerifyingHandler::new)) + .maximumConcurrentSessions(3) + .build(); + distributor.add(node); + + SessionRequest sessionRequest = + new SessionRequest( + new RequestId(UUID.randomUUID()), + Instant.now(), + Set.of(W3C), + Set.of(new ImmutableCapabilities("browserName", "cheese")), + Map.of(), + Map.of()); + + List> callables = new ArrayList<>(); + for (int i = 0; i < 3; i++) { + callables.add(() -> { + Either result = + distributor.newSession(sessionRequest); + if (result.isRight()) { + CreateSessionResponse res = result.right(); + assertThat(res.getSession().getCapabilities().getBrowserName()).isEqualTo("cheese"); + return res.getSession().getId(); + } else { + fail("Session creation failed", result.left()); + } + return null; + }); + } + + List> futures = Executors.newFixedThreadPool(3).invokeAll(callables); + + for (Future future : futures) { + SessionId id = future.get(2, TimeUnit.SECONDS); + + // Now send a random command. + HttpResponse res = node.execute(new HttpRequest(GET, String.format("/session/%s/url", id))); + assertThat(res.isSuccessful()).isTrue(); + } + } + + + @Test + public void testDrainNodeFromDistributor() { + NewSessionQueue queue = new LocalNewSessionQueue( + tracer, + bus, + new DefaultSlotMatcher(), + Duration.ofSeconds(2), + Duration.ofSeconds(2), + registrationSecret); + Distributor distributor = new LocalDistributor( + tracer, + bus, + clientFactory, + new LocalSessionMap(tracer, bus), + queue, + gridModel, + new DefaultSlotSelector(), + registrationSecret, + Duration.ofMinutes(5), + false); + distributor.add(localNode); + assertThat(localNode.isDraining()).isFalse(); + + //Check the size - there should be one node + DistributorStatus statusBefore = distributor.getStatus(); + Set nodesBefore = statusBefore.getNodes(); + assertThat(nodesBefore.size()).isEqualTo(1); + NodeStatus nodeBefore = nodesBefore.iterator().next(); + assertThat(nodeBefore.getAvailability()).isNotEqualTo(DRAINING); + + distributor.drain(localNode.getId()); + assertThat(localNode.isDraining()).isTrue(); + + //Recheck the status - there should still be no node, it is removed + DistributorStatus statusAfter = distributor.getStatus(); + Set nodesAfter = statusAfter.getNodes(); + assertThat(nodesAfter.size()).isEqualTo(0); + } + + @Test + public void testDrainNodeFromNode() { + assertThat(localNode.isDraining()).isFalse(); + + NewSessionQueue queue = new LocalNewSessionQueue( + tracer, + bus, + new DefaultSlotMatcher(), + Duration.ofSeconds(2), + Duration.ofSeconds(2), + registrationSecret); + Distributor distributor = new LocalDistributor( + tracer, + bus, + clientFactory, + new LocalSessionMap(tracer, bus), + queue, + gridModel, + new DefaultSlotSelector(), + registrationSecret, + Duration.ofMinutes(5), + false); + distributor.add(localNode); + + localNode.drain(); + assertThat(localNode.isDraining()).isTrue(); + } + + private class Handler extends Session implements HttpHandler { + + private Handler(Capabilities capabilities) { + super(new SessionId(UUID.randomUUID()), uri, new ImmutableCapabilities(), capabilities, Instant.now()); + } + + @Override + public HttpResponse execute(HttpRequest req) throws UncheckedIOException { + return new HttpResponse(); + } + } +}