diff --git a/java/src/org/openqa/selenium/grid/BUILD.bazel b/java/src/org/openqa/selenium/grid/BUILD.bazel index a357e5c01153d..b6d562b351bc7 100644 --- a/java/src/org/openqa/selenium/grid/BUILD.bazel +++ b/java/src/org/openqa/selenium/grid/BUILD.bazel @@ -200,6 +200,7 @@ java_binary( ], runtime_deps = [ ":grid", + "//java/src/org/openqa/selenium/grid/sessionmap/redis", artifact("org.slf4j:slf4j-jdk14"), ], ) diff --git a/java/src/org/openqa/selenium/grid/commands/sessionmaps.txt b/java/src/org/openqa/selenium/grid/commands/sessionmaps.txt index 40f24412d4eca..921c4b33615d9 100644 --- a/java/src/org/openqa/selenium/grid/commands/sessionmaps.txt +++ b/java/src/org/openqa/selenium/grid/commands/sessionmaps.txt @@ -5,14 +5,14 @@ following types of data store. ## RedisBackedSessionMap -`RedisBackedSessionMap` uses Redis as datastore for key, value store. To start it, run the following command: +`RedisBackedSessionMap` uses Redis as datastore for key, value store. It is built-in bundled by default, so no `--ext` flag required. To start it, run the following command: ``` -SESSIONS_IMPLEMENTATION=org.openqa.selenium.grid.sessionmap.redis.RedisBackedSessionMap \ -SESSIONS_SCHEME=redis \ -java -jar selenium.jar \ - --ext $(coursier fetch -p org.seleniumhq.selenium:selenium-session-map-redis:latest.release) \ - sessions --sessions-host "" --sessions-port +java -jar selenium.jar sessions \ + --sessions-implementation org.openqa.selenium.grid.sessionmap.redis.RedisBackedSessionMap \ + --sessions-scheme redis \ + --sessions-host "" \ + --sessions-port ``` ## JdbcBackedSessionMap diff --git a/java/src/org/openqa/selenium/grid/sessionmap/config/BUILD.bazel b/java/src/org/openqa/selenium/grid/sessionmap/config/BUILD.bazel index 052be637fdab7..0cfc55b1b15c1 100644 --- a/java/src/org/openqa/selenium/grid/sessionmap/config/BUILD.bazel +++ b/java/src/org/openqa/selenium/grid/sessionmap/config/BUILD.bazel @@ -6,6 +6,7 @@ java_library( srcs = glob(["*.java"]), visibility = [ "//java/src/org/openqa/selenium/grid:__subpackages__", + "//java/test/org/openqa/selenium/grid/sessionmap/config:__pkg__", ], deps = [ "//java:auto-service", diff --git a/java/src/org/openqa/selenium/grid/sessionmap/config/SessionMapFlags.java b/java/src/org/openqa/selenium/grid/sessionmap/config/SessionMapFlags.java index ca8b117ee1edf..3e290ac879b2a 100644 --- a/java/src/org/openqa/selenium/grid/sessionmap/config/SessionMapFlags.java +++ b/java/src/org/openqa/selenium/grid/sessionmap/config/SessionMapFlags.java @@ -49,6 +49,21 @@ public class SessionMapFlags implements HasRoles { @ConfigValue(section = "sessions", name = "hostname", example = "\"localhost\"") private String sessionServerHost; + @Parameter( + names = "--sessions-scheme", + description = "URI scheme for the session map server (e.g. \"redis\", \"http\").") + @ConfigValue(section = "sessions", name = "scheme", example = "\"redis\"") + private String sessionServerScheme; + + @Parameter( + names = "--sessions-implementation", + description = "Full classname of the non-default session map implementation.") + @ConfigValue( + section = "sessions", + name = "implementation", + example = "\"org.openqa.selenium.grid.sessionmap.redis.RedisBackedSessionMap\"") + private String sessionMapImplementation; + @Override public Set getRoles() { return Collections.singleton(SESSION_MAP_ROLE); diff --git a/java/src/org/openqa/selenium/grid/sessionmap/redis/RedisBackedSessionMap.java b/java/src/org/openqa/selenium/grid/sessionmap/redis/RedisBackedSessionMap.java index e989b91c4df30..b42386bb3692e 100644 --- a/java/src/org/openqa/selenium/grid/sessionmap/redis/RedisBackedSessionMap.java +++ b/java/src/org/openqa/selenium/grid/sessionmap/redis/RedisBackedSessionMap.java @@ -188,7 +188,7 @@ public Session get(SessionId id) throws NoSuchSessionException { : JSON.toType(rawStereotype, Capabilities.class); String rawStart = connection.get(startKey(id)); - Instant start = JSON.toType(rawStart, Instant.class); + Instant start = rawStart == null ? Instant.EPOCH : JSON.toType(rawStart, Instant.class); CAPABILITIES.accept(span, caps); CAPABILITIES_EVENT.accept(attributeMap, caps); diff --git a/java/test/org/openqa/selenium/grid/sessionmap/config/BUILD.bazel b/java/test/org/openqa/selenium/grid/sessionmap/config/BUILD.bazel new file mode 100644 index 0000000000000..e427b7dd4c304 --- /dev/null +++ b/java/test/org/openqa/selenium/grid/sessionmap/config/BUILD.bazel @@ -0,0 +1,15 @@ +load("@rules_jvm_external//:defs.bzl", "artifact") +load("//java:defs.bzl", "JUNIT5_DEPS", "java_test_suite") + +java_test_suite( + name = "SmallTests", + size = "small", + srcs = glob(["*Test.java"]), + deps = [ + "//java/src/org/openqa/selenium/grid/config", + "//java/src/org/openqa/selenium/grid/sessionmap/config", + artifact("com.beust:jcommander"), + artifact("org.junit.jupiter:junit-jupiter-api"), + artifact("org.assertj:assertj-core"), + ] + JUNIT5_DEPS, +) diff --git a/java/test/org/openqa/selenium/grid/sessionmap/config/SessionMapFlagsTest.java b/java/test/org/openqa/selenium/grid/sessionmap/config/SessionMapFlagsTest.java new file mode 100644 index 0000000000000..0719759cad7e6 --- /dev/null +++ b/java/test/org/openqa/selenium/grid/sessionmap/config/SessionMapFlagsTest.java @@ -0,0 +1,69 @@ +// 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.sessionmap.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.beust.jcommander.JCommander; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.grid.config.AnnotatedConfig; +import org.openqa.selenium.grid.config.Config; + +class SessionMapFlagsTest { + + private SessionMapFlags flags; + + @BeforeEach + void setUp() { + flags = new SessionMapFlags(); + } + + @Test + void sessionsSchemeFlagPopulatesConfig() { + JCommander.newBuilder().addObject(flags).build().parse("--sessions-scheme", "redis"); + + Config config = new AnnotatedConfig(flags); + + assertThat(config.get("sessions", "scheme")).contains("redis"); + } + + @Test + void sessionsImplementationFlagPopulatesConfig() { + String impl = "org.openqa.selenium.grid.sessionmap.redis.RedisBackedSessionMap"; + JCommander.newBuilder().addObject(flags).build().parse("--sessions-implementation", impl); + + Config config = new AnnotatedConfig(flags); + + assertThat(config.get("sessions", "implementation")).contains(impl); + } + + @Test + void sessionsSchemeIsAbsentWhenFlagNotSet() { + Config config = new AnnotatedConfig(flags); + + assertThat(config.get("sessions", "scheme")).isEmpty(); + } + + @Test + void sessionsImplementationIsAbsentWhenFlagNotSet() { + Config config = new AnnotatedConfig(flags); + + assertThat(config.get("sessions", "implementation")).isEmpty(); + } +} diff --git a/java/test/org/openqa/selenium/grid/sessionmap/redis/BUILD.bazel b/java/test/org/openqa/selenium/grid/sessionmap/redis/BUILD.bazel index 7351df7de5ea0..4c96ff0bc4a0e 100644 --- a/java/test/org/openqa/selenium/grid/sessionmap/redis/BUILD.bazel +++ b/java/test/org/openqa/selenium/grid/sessionmap/redis/BUILD.bazel @@ -7,6 +7,9 @@ java_test_suite( srcs = glob(["*Test.java"]), tags = ["skip-rbe"], deps = [ + "//java/src/org/openqa/selenium/events/local", + "//java/src/org/openqa/selenium/grid/config", + "//java/src/org/openqa/selenium/grid/sessionmap", "//java/src/org/openqa/selenium/grid/sessionmap/redis", "//java/src/org/openqa/selenium/remote", "//java/test/org/openqa/selenium/remote/tracing:tracing-support", diff --git a/java/test/org/openqa/selenium/grid/sessionmap/redis/RedisBackedSessionMapTest.java b/java/test/org/openqa/selenium/grid/sessionmap/redis/RedisBackedSessionMapTest.java index 4f133619e5b40..f6cc63c4725f5 100644 --- a/java/test/org/openqa/selenium/grid/sessionmap/redis/RedisBackedSessionMapTest.java +++ b/java/test/org/openqa/selenium/grid/sessionmap/redis/RedisBackedSessionMapTest.java @@ -19,6 +19,7 @@ import static java.util.UUID.randomUUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.openqa.selenium.testing.Safely.safelyCall; @@ -36,6 +37,8 @@ import org.openqa.selenium.NoSuchSessionException; import org.openqa.selenium.events.EventBus; import org.openqa.selenium.events.local.GuavaEventBus; +import org.openqa.selenium.grid.config.Config; +import org.openqa.selenium.grid.config.MapConfig; import org.openqa.selenium.grid.data.Availability; import org.openqa.selenium.grid.data.NodeId; import org.openqa.selenium.grid.data.NodeRemovedEvent; @@ -44,6 +47,7 @@ import org.openqa.selenium.grid.data.SessionClosedEvent; import org.openqa.selenium.grid.data.Slot; import org.openqa.selenium.grid.data.SlotId; +import org.openqa.selenium.grid.sessionmap.SessionMap; import org.openqa.selenium.remote.SessionId; import org.openqa.selenium.remote.tracing.DefaultTestTracer; import org.openqa.selenium.remote.tracing.Tracer; @@ -236,6 +240,77 @@ private Session createSession(URI uri) { Instant.now()); } + @Test + void addReturnsTrue() throws URISyntaxException { + Session session = createSession(new URI("http://example.com/foo")); + assertThat(sessions.add(session)).isTrue(); + } + + @Test + void getUriThrowsForMalformedUriStoredInRedis() { + SessionId id = new SessionId(randomUUID()); + sessions.getRedisClient().mset(Map.of("session:" + id + ":uri", "not a valid uri")); + assertThatThrownBy(() -> sessions.getUri(id)) + .isInstanceOf(NoSuchSessionException.class) + .hasMessageContaining(id.toString()); + } + + @Test + void getSucceedsWhenStartKeyIsMissing() throws URISyntaxException { + Session expected = createSession(new URI("http://example.com/foo")); + sessions.add(expected); + sessions.getRedisClient().del("session:" + expected.getId() + ":start"); + + Session seen = sessions.get(expected.getId()); + + assertThat(seen.getId()).isEqualTo(expected.getId()); + assertThat(seen.getUri()).isEqualTo(expected.getUri()); + assertThat(seen.getStartTime()).isEqualTo(Instant.EPOCH); + } + + @Test + void removeIsIdempotent() { + SessionId id = new SessionId(randomUUID()); + assertThatNoException().isThrownBy(() -> sessions.remove(id)); + } + + @Test + void removeByUriIsNoOpWhenRedisIsEmpty() throws URISyntaxException { + assertThatNoException() + .isThrownBy(() -> sessions.removeByUri(new URI("http://example.com/foo"))); + } + + @Test + void nodeRemovedEventIgnoresSlotsWithNullSession() throws URISyntaxException { + URI nodeUri = new URI("http://example.com/node"); + Session activeSession = createSession(nodeUri); + sessions.add(activeSession); + + bus.fire(new NodeRemovedEvent(createNodeStatusWithNullSlot(nodeUri, activeSession))); + + assertThatThrownBy(() -> sessions.get(activeSession.getId())) + .isInstanceOf(NoSuchSessionException.class); + } + + @Test + void createFromConfigBuildsWorkingSessionMap() throws URISyntaxException { + Config config = + new MapConfig( + Map.of( + "sessions", Map.of("host", redisUri.toString()), + "events", + Map.of("implementation", "org.openqa.selenium.events.local.GuavaEventBus"))); + + SessionMap sessionMap = RedisBackedSessionMap.create(config); + try { + Session expected = createSession(new URI("http://example.com/foo")); + sessionMap.add(expected); + assertThat(((RedisBackedSessionMap) sessionMap).get(expected.getId())).isEqualTo(expected); + } finally { + ((RedisBackedSessionMap) sessionMap).getRedisClient().close(); + } + } + private org.openqa.selenium.grid.data.NodeStatus createNodeStatus(URI nodeUri, Session session) { NodeId nodeId = new NodeId(UUID.randomUUID()); return new org.openqa.selenium.grid.data.NodeStatus( @@ -254,4 +329,29 @@ private org.openqa.selenium.grid.data.NodeStatus createNodeStatus(URI nodeUri, S "test", Map.of()); } + + private org.openqa.selenium.grid.data.NodeStatus createNodeStatusWithNullSlot( + URI nodeUri, Session activeSession) { + NodeId nodeId = new NodeId(UUID.randomUUID()); + return new org.openqa.selenium.grid.data.NodeStatus( + nodeId, + nodeUri, + 2, + Set.of( + new Slot( + new SlotId(nodeId, UUID.randomUUID()), + new ImmutableCapabilities("browserName", "cheese"), + Instant.now(), + activeSession), + new Slot( + new SlotId(nodeId, UUID.randomUUID()), + new ImmutableCapabilities("browserName", "cheese"), + Instant.now(), + null)), + Availability.UP, + Duration.ofSeconds(30), + Duration.ofSeconds(30), + "test", + Map.of()); + } }