From 830b96653dac1e669a5214ecec0b7ad25dafd710 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Mon, 9 Jun 2025 02:19:05 +0700 Subject: [PATCH 1/6] [grid] Add Node session-history endpoint and write to local file for other utility to consume --- .../grid/data/SessionHistoryEntry.java | 66 ++++++++ .../grid/data/SessionStartedEvent.java | 40 +++++ .../selenium/grid/data/SessionStatus.java | 23 +++ .../grid/node/GetNodeSessionHistory.java | 45 ++++++ .../org/openqa/selenium/grid/node/Node.java | 7 + .../selenium/grid/node/config/NodeFlags.java | 21 +++ .../grid/node/config/NodeOptions.java | 8 + .../selenium/grid/node/local/LocalNode.java | 136 +++++++++++++++- .../grid/node/local/LocalNodeFactory.java | 5 +- .../selenium/grid/node/local/SessionSlot.java | 2 + .../selenium/grid/node/remote/RemoteNode.java | 12 ++ .../grid/data/SessionClosedEventTest.java | 73 +++++++++ .../grid/node/GetNodeSessionHistoryTest.java | 149 ++++++++++++++++++ .../grid/node/config/NodeOptionsTest.java | 33 ++++ .../grid/node/local/LocalNodeTest.java | 142 +++++++++++++++++ 15 files changed, 758 insertions(+), 4 deletions(-) create mode 100644 java/src/org/openqa/selenium/grid/data/SessionHistoryEntry.java create mode 100644 java/src/org/openqa/selenium/grid/data/SessionStartedEvent.java create mode 100644 java/src/org/openqa/selenium/grid/data/SessionStatus.java create mode 100644 java/src/org/openqa/selenium/grid/node/GetNodeSessionHistory.java create mode 100644 java/test/org/openqa/selenium/grid/data/SessionClosedEventTest.java create mode 100644 java/test/org/openqa/selenium/grid/node/GetNodeSessionHistoryTest.java diff --git a/java/src/org/openqa/selenium/grid/data/SessionHistoryEntry.java b/java/src/org/openqa/selenium/grid/data/SessionHistoryEntry.java new file mode 100644 index 0000000000000..bc902bdf545be --- /dev/null +++ b/java/src/org/openqa/selenium/grid/data/SessionHistoryEntry.java @@ -0,0 +1,66 @@ +// 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.data; + +import java.time.Instant; +import java.util.Objects; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.SessionId; + +public class SessionHistoryEntry { + private final SessionId sessionId; + private final Instant startTime; + private Instant stopTime; + + public SessionHistoryEntry(SessionId sessionId, Instant startTime, Instant stopTime) { + this.sessionId = Require.nonNull("Session ID", sessionId); + this.startTime = Require.nonNull("Start time", startTime); + this.stopTime = stopTime; // Can be null for ongoing sessions + } + + public SessionId getSessionId() { + return sessionId; + } + + public Instant getStartTime() { + return startTime; + } + + public Instant getStopTime() { + return stopTime; + } + + public void setStopTime(Instant stopTime) { + this.stopTime = stopTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SessionHistoryEntry)) return false; + SessionHistoryEntry that = (SessionHistoryEntry) o; + return Objects.equals(sessionId, that.sessionId) + && Objects.equals(startTime, that.startTime) + && Objects.equals(stopTime, that.stopTime); + } + + @Override + public int hashCode() { + return Objects.hash(sessionId, startTime, stopTime); + } +} diff --git a/java/src/org/openqa/selenium/grid/data/SessionStartedEvent.java b/java/src/org/openqa/selenium/grid/data/SessionStartedEvent.java new file mode 100644 index 0000000000000..c0f3e5c5060f9 --- /dev/null +++ b/java/src/org/openqa/selenium/grid/data/SessionStartedEvent.java @@ -0,0 +1,40 @@ +// 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.data; + +import java.util.function.Consumer; +import org.openqa.selenium.events.Event; +import org.openqa.selenium.events.EventListener; +import org.openqa.selenium.events.EventName; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.SessionId; + +public class SessionStartedEvent extends Event { + + private static final EventName SESSION_STARTED = new EventName("session-started"); + + public SessionStartedEvent(SessionId id) { + super(SESSION_STARTED, id); + } + + public static EventListener listener(Consumer handler) { + Require.nonNull("Handler", handler); + + return new EventListener<>(SESSION_STARTED, SessionId.class, handler); + } +} diff --git a/java/src/org/openqa/selenium/grid/data/SessionStatus.java b/java/src/org/openqa/selenium/grid/data/SessionStatus.java new file mode 100644 index 0000000000000..2f75d02b2f030 --- /dev/null +++ b/java/src/org/openqa/selenium/grid/data/SessionStatus.java @@ -0,0 +1,23 @@ +// 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.data; + +public enum SessionStatus { + SUCCESS, + FAILED +} diff --git a/java/src/org/openqa/selenium/grid/node/GetNodeSessionHistory.java b/java/src/org/openqa/selenium/grid/node/GetNodeSessionHistory.java new file mode 100644 index 0000000000000..041ec94e88294 --- /dev/null +++ b/java/src/org/openqa/selenium/grid/node/GetNodeSessionHistory.java @@ -0,0 +1,45 @@ +// 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.node; + +import static org.openqa.selenium.remote.http.Contents.asJson; + +import com.google.common.collect.ImmutableMap; +import java.io.UncheckedIOException; +import java.util.List; +import org.openqa.selenium.grid.data.SessionHistoryEntry; +import org.openqa.selenium.internal.Require; +import org.openqa.selenium.remote.http.HttpHandler; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; + +class GetNodeSessionHistory implements HttpHandler { + + private final Node node; + + GetNodeSessionHistory(Node node) { + this.node = Require.nonNull("Node", node); + } + + @Override + public HttpResponse execute(HttpRequest req) throws UncheckedIOException { + List sessionHistory = node.getSessionHistory(); + + return new HttpResponse().setContent(asJson(ImmutableMap.of("value", sessionHistory))); + } +} diff --git a/java/src/org/openqa/selenium/grid/node/Node.java b/java/src/org/openqa/selenium/grid/node/Node.java index d2b0d5223802b..fa799f8a6d2ba 100644 --- a/java/src/org/openqa/selenium/grid/node/Node.java +++ b/java/src/org/openqa/selenium/grid/node/Node.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; +import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.Set; @@ -45,6 +46,7 @@ import org.openqa.selenium.grid.data.NodeId; import org.openqa.selenium.grid.data.NodeStatus; import org.openqa.selenium.grid.data.Session; +import org.openqa.selenium.grid.data.SessionHistoryEntry; import org.openqa.selenium.grid.security.RequiresSecretFilter; import org.openqa.selenium.grid.security.Secret; import org.openqa.selenium.internal.Either; @@ -189,6 +191,9 @@ protected Node( delete("/se/grid/node/session/{sessionId}") .to(params -> new StopNodeSession(this, sessionIdFrom(params))) .with(spanDecorator("node.stop_session").andThen(requiresSecret)), + get("/se/grid/node/session-history") + .to(() -> new GetNodeSessionHistory(this)) + .with(spanDecorator("node.get_session_history").andThen(requiresSecret)), get("/se/grid/node/session/{sessionId}") .to(params -> new GetNodeSession(this, sessionIdFrom(params))) .with(spanDecorator("node.get_session").andThen(requiresSecret)), @@ -266,6 +271,8 @@ public TemporaryFilesystem getDownloadsFilesystem(SessionId id) throws IOExcepti public abstract NodeStatus getStatus(); + public abstract List getSessionHistory(); + public abstract HealthCheck getHealthCheck(); public Duration getSessionTimeout() { diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java b/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java index bb65259ee3baf..8d1dc0c9308f5 100644 --- a/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java +++ b/java/src/org/openqa/selenium/grid/node/config/NodeFlags.java @@ -283,6 +283,27 @@ public class NodeFlags implements HasRoles { @ConfigValue(section = NODE_SECTION, name = "enable-managed-downloads", example = "false") public Boolean managedDownloadsEnabled; + @Parameter( + names = {"--status-to-file"}, + description = + "Path to a local file where the Node will write its status information " + + "in JSON format. This file will be updated periodically and can be " + + "consumed by other services running on the same machine.") + @ConfigValue(section = NODE_SECTION, name = "status-to-file", example = "node-status.json") + public String statusFile; + + @Parameter( + names = {"--session-history-to-file"}, + description = + "Path to a local file where the Node will write session history information " + + "in JSON format. This file will contain chronological records of session " + + "start and stop events with sessionId, startTime, and stopTime.") + @ConfigValue( + section = NODE_SECTION, + name = "session-history-to-file", + example = "session-history.json") + public String sessionHistoryFile; + @Override public Set getRoles() { return Collections.singleton(NODE_ROLE); diff --git a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java index 6691650c0ac20..b2486cdb2934f 100644 --- a/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java +++ b/java/src/org/openqa/selenium/grid/node/config/NodeOptions.java @@ -168,6 +168,14 @@ public boolean isManagedDownloadsEnabled() { return config.getBool(NODE_SECTION, "enable-managed-downloads").orElse(Boolean.FALSE); } + public Optional getStatusFile() { + return config.get(NODE_SECTION, "status-to-file"); + } + + public Optional getSessionHistoryFile() { + return config.get(NODE_SECTION, "session-history-to-file"); + } + public String getGridSubPath() { return normalizeSubPath(getPublicGridUri().map(URI::getPath).orElse("")); } 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 8561d77d2e2f5..bea9841e7d071 100644 --- a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java +++ b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java @@ -47,16 +47,24 @@ import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import java.time.Clock; import java.time.Duration; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Queue; import java.util.Set; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -87,6 +95,9 @@ import org.openqa.selenium.grid.data.NodeId; import org.openqa.selenium.grid.data.NodeStatus; import org.openqa.selenium.grid.data.Session; +import org.openqa.selenium.grid.data.SessionClosedEvent; +import org.openqa.selenium.grid.data.SessionHistoryEntry; +import org.openqa.selenium.grid.data.SessionStartedEvent; import org.openqa.selenium.grid.data.Slot; import org.openqa.selenium.grid.data.SlotId; import org.openqa.selenium.grid.jmx.JMXHelper; @@ -146,6 +157,10 @@ public class LocalNode extends Node implements Closeable { private final AtomicInteger sessionCount = new AtomicInteger(); private final Runnable shutdown; private final ReadWriteLock drainLock = new ReentrantReadWriteLock(); + private final Optional statusFilePath; + private final Optional sessionHistoryFilePath; + private final Queue sessionHistory = new ConcurrentLinkedQueue<>(); + private final Map sessionStartTimes = new ConcurrentHashMap<>(); protected LocalNode( Tracer tracer, @@ -163,7 +178,9 @@ protected LocalNode( List factories, Secret registrationSecret, boolean managedDownloadsEnabled, - int connectionLimitPerSession) { + int connectionLimitPerSession, + Optional statusFilePath, + Optional sessionHistoryFilePath) { super( tracer, new NodeId(UUID.randomUUID()), @@ -187,6 +204,8 @@ protected LocalNode( this.bidiEnabled = bidiEnabled; this.managedDownloadsEnabled = managedDownloadsEnabled; this.connectionLimitPerSession = connectionLimitPerSession; + this.statusFilePath = statusFilePath; + this.sessionHistoryFilePath = sessionHistoryFilePath; this.healthCheck = healthCheck == null @@ -277,11 +296,19 @@ protected LocalNode( return thread; }); heartbeatNodeService.scheduleAtFixedRate( - GuardedRunnable.guard(() -> bus.fire(new NodeHeartBeatEvent(getStatus()))), + GuardedRunnable.guard( + () -> { + NodeStatus status = getStatus(); + bus.fire(new NodeHeartBeatEvent(status)); + writeStatusToFile(status); + }), heartbeatPeriod.getSeconds(), heartbeatPeriod.getSeconds(), TimeUnit.SECONDS); + bus.addListener(SessionStartedEvent.listener(this::recordSessionStart)); + bus.addListener(SessionClosedEvent.listener(this::recordSessionStop)); + shutdown = () -> { if (heartbeatNodeService.isShutdown()) return; @@ -1005,6 +1032,11 @@ public NodeStatus getStatus() { getOsInfo()); } + @Override + public List getSessionHistory() { + return new ArrayList<>(sessionHistory); + } + @Override public HealthCheck getHealthCheck() { return healthCheck; @@ -1083,6 +1115,68 @@ private Map toJson() { factories.stream().map(SessionSlot::getStereotype).collect(Collectors.toSet())); } + private void writeStatusToFile(NodeStatus status) { + if (statusFilePath.isEmpty()) { + return; + } + + try { + String statusJson = JSON.toJson(status); + Files.write( + statusFilePath.get(), + statusJson.getBytes(), + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to write status to file: " + statusFilePath.get(), e); + } + } + + private void recordSessionStart(SessionId sessionId) { + if (!isSessionOwner(sessionId)) { + return; + } + Instant startTime = Instant.now(); + sessionStartTimes.put(sessionId, startTime); + sessionHistory.add(new SessionHistoryEntry(sessionId, startTime, null)); + writeSessionHistoryToFile(); + } + + private void recordSessionStop(SessionId sessionId) { + if (!isSessionOwner(sessionId)) { + return; + } + Instant stopTime = Instant.now(); + Instant startTime = sessionStartTimes.remove(sessionId); + if (startTime != null) { + // Find and update the existing history entry + sessionHistory.stream() + .filter(entry -> entry.getSessionId().equals(sessionId)) + .findFirst() + .ifPresent(entry -> entry.setStopTime(stopTime)); + writeSessionHistoryToFile(); + } + } + + private void writeSessionHistoryToFile() { + if (sessionHistoryFilePath.isPresent()) { + try { + List sortedHistory = new ArrayList<>(sessionHistory); + sortedHistory.sort((a, b) -> a.getStartTime().compareTo(b.getStartTime())); + + String historyJson = JSON.toJson(sortedHistory); + Files.write( + sessionHistoryFilePath.get(), + historyJson.getBytes(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + LOG.log(Level.WARNING, "Unable to write session history to file", e); + } + } + } + public static class Builder { private final Tracer tracer; @@ -1177,7 +1271,9 @@ public LocalNode build() { factories.build(), registrationSecret, managedDownloadsEnabled, - connectionLimitPerSession); + connectionLimitPerSession, + Optional.empty(), + Optional.empty()); } public Advanced advanced() { @@ -1202,6 +1298,40 @@ public Advanced healthCheck(HealthCheck healthCheck) { return this; } + public Advanced statusFile(Optional statusFile) { + return sessionHistoryFile(statusFile, Optional.empty()); + } + + public Advanced sessionHistoryFile( + Optional statusFile, Optional sessionHistoryFile) { + Optional statusFilePath = statusFile.map(Paths::get); + Optional sessionHistoryFilePath = sessionHistoryFile.map(Paths::get); + return new Advanced() { + @Override + public Node build() { + return new LocalNode( + tracer, + bus, + uri, + gridUri, + healthCheck, + maxSessions, + drainAfterSessionCount, + cdpEnabled, + bidiEnabled, + ticker, + sessionTimeout, + heartbeatPeriod, + factories.build(), + registrationSecret, + managedDownloadsEnabled, + connectionLimitPerSession, + statusFilePath, + sessionHistoryFilePath); + } + }; + } + public Node build() { return Builder.this.build(); } diff --git a/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java b/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java index 600f516b02992..319cb7531ff37 100644 --- a/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java +++ b/java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java @@ -100,7 +100,10 @@ public static Node create(Config config) { .forEach((caps, factories) -> factories.forEach(factory -> builder.add(caps, factory))); } - return builder.build(); + return builder + .advanced() + .sessionHistoryFile(nodeOptions.getStatusFile(), nodeOptions.getSessionHistoryFile()) + .build(); } private static Collection createSessionFactory( diff --git a/java/src/org/openqa/selenium/grid/node/local/SessionSlot.java b/java/src/org/openqa/selenium/grid/node/local/SessionSlot.java index 3c51b785c13c0..54400d41f1e30 100644 --- a/java/src/org/openqa/selenium/grid/node/local/SessionSlot.java +++ b/java/src/org/openqa/selenium/grid/node/local/SessionSlot.java @@ -37,6 +37,7 @@ import org.openqa.selenium.events.EventBus; import org.openqa.selenium.grid.data.CreateSessionRequest; import org.openqa.selenium.grid.data.SessionClosedEvent; +import org.openqa.selenium.grid.data.SessionStartedEvent; import org.openqa.selenium.grid.node.ActiveSession; import org.openqa.selenium.grid.node.SessionFactory; import org.openqa.selenium.grid.node.relay.RelaySessionFactory; @@ -153,6 +154,7 @@ public Either apply(CreateSessionRequest sess ActiveSession session = possibleSession.right(); currentSession = session; connectionCounter.set(0); + bus.fire(new SessionStartedEvent(session.getId())); return Either.right(session); } else { return Either.left(possibleSession.left()); diff --git a/java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java b/java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java index 947ee18e79978..58f2bd262d114 100644 --- a/java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java +++ b/java/src/org/openqa/selenium/grid/node/remote/RemoteNode.java @@ -38,6 +38,7 @@ import java.net.URI; import java.time.Duration; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -53,6 +54,7 @@ import org.openqa.selenium.grid.data.NodeId; import org.openqa.selenium.grid.data.NodeStatus; import org.openqa.selenium.grid.data.Session; +import org.openqa.selenium.grid.data.SessionHistoryEntry; import org.openqa.selenium.grid.node.HealthCheck; import org.openqa.selenium.grid.node.Node; import org.openqa.selenium.grid.security.AddSecretFilter; @@ -208,6 +210,16 @@ public void releaseConnection(SessionId id) { Values.get(res, Void.class); } + @Override + public List getSessionHistory() { + HttpRequest req = new HttpRequest(GET, "/se/grid/node/session-history"); + HttpTracing.inject(tracer, tracer.getCurrentContext(), req); + + HttpResponse res = client.with(addSecret).execute(req); + + return Values.get(res, List.class); + } + @Override public Session getSession(SessionId id) throws NoSuchSessionException { Require.nonNull("Session ID", id); diff --git a/java/test/org/openqa/selenium/grid/data/SessionClosedEventTest.java b/java/test/org/openqa/selenium/grid/data/SessionClosedEventTest.java new file mode 100644 index 0000000000000..0b5527ad2aec9 --- /dev/null +++ b/java/test/org/openqa/selenium/grid/data/SessionClosedEventTest.java @@ -0,0 +1,73 @@ +// 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.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.remote.SessionId; + +class SessionClosedEventTest { + + @Test + void shouldSerializeAndDeserializeWithSuccessStatus() { + SessionId sessionId = new SessionId("test-session-123"); + SessionClosedEvent originalEvent = new SessionClosedEvent(sessionId, SessionStatus.SUCCESS); + + Json json = new Json(); + String serialized = json.toJson(originalEvent); + SessionClosedEvent deserializedEvent = json.toType(serialized, SessionClosedEvent.class); + + assertThat(deserializedEvent).isNotNull(); + assertThat(deserializedEvent.getData(SessionId.class)).isEqualTo(sessionId); + assertThat(deserializedEvent.getStatus()).isEqualTo(SessionStatus.SUCCESS); + } + + @Test + void shouldSerializeAndDeserializeWithFailedStatus() { + SessionId sessionId = new SessionId("test-session-456"); + SessionClosedEvent originalEvent = new SessionClosedEvent(sessionId, SessionStatus.FAILED); + + Json json = new Json(); + String serialized = json.toJson(originalEvent); + SessionClosedEvent deserializedEvent = json.toType(serialized, SessionClosedEvent.class); + + assertThat(deserializedEvent).isNotNull(); + assertThat(deserializedEvent.getData(SessionId.class)).isEqualTo(sessionId); + assertThat(deserializedEvent.getStatus()).isEqualTo(SessionStatus.FAILED); + } + + @Test + void shouldUseDefaultSuccessStatusWhenNotSpecified() { + SessionId sessionId = new SessionId("test-session-789"); + SessionClosedEvent event = new SessionClosedEvent(sessionId); + + assertThat(event.getStatus()).isEqualTo(SessionStatus.SUCCESS); + } + + @Test + void shouldHandleEventDataSerialization() { + SessionId sessionId = new SessionId("test-session-abc"); + SessionClosedEvent event = new SessionClosedEvent(sessionId, SessionStatus.FAILED); + + String rawData = event.getRawData(); + assertThat(rawData).isNotEmpty(); + assertThat(rawData).contains(sessionId.toString()); + } +} diff --git a/java/test/org/openqa/selenium/grid/node/GetNodeSessionHistoryTest.java b/java/test/org/openqa/selenium/grid/node/GetNodeSessionHistoryTest.java new file mode 100644 index 0000000000000..f7de55f518524 --- /dev/null +++ b/java/test/org/openqa/selenium/grid/node/GetNodeSessionHistoryTest.java @@ -0,0 +1,149 @@ +// 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.node; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.remote.http.HttpMethod.GET; + +import java.time.Instant; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.openqa.selenium.grid.data.SessionHistoryEntry; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.remote.SessionId; +import org.openqa.selenium.remote.http.HttpRequest; +import org.openqa.selenium.remote.http.HttpResponse; + +class GetNodeSessionHistoryTest { + + @Test + void shouldReturnSessionHistoryAsJson() { + SessionId sessionId = new SessionId("test-session"); + Instant startTime = Instant.now(); + Instant stopTime = startTime.plusSeconds(60); + SessionHistoryEntry entry = new SessionHistoryEntry(sessionId, startTime, stopTime); + + TestNode node = new TestNode(Arrays.asList(entry)); + GetNodeSessionHistory handler = new GetNodeSessionHistory(node); + + HttpResponse response = handler.execute(new HttpRequest(GET, "/")); + + assertThat(response.getStatus()).isEqualTo(200); + String content = response.getContentString(); + assertThat(content).contains("test-session"); + assertThat(content).contains("value"); + + Json json = new Json(); + Object responseObj = json.toType(content, Object.class); + assertThat(responseObj).isNotNull(); + } + + @Test + void shouldReturnEmptyListWhenNoHistory() { + TestNode node = new TestNode(Arrays.asList()); + GetNodeSessionHistory handler = new GetNodeSessionHistory(node); + + HttpResponse response = handler.execute(new HttpRequest(GET, "/")); + + assertThat(response.getStatus()).isEqualTo(200); + String content = response.getContentString(); + assertThat(content).contains("value"); + assertThat(content).contains("[]"); + } + + private static class TestNode extends Node { + private final List history; + + TestNode(List history) { + super(null, null, null, null, null); + this.history = history; + } + + @Override + public List getSessionHistory() { + return history; + } + + @Override + public org.openqa.selenium.internal.Either< + org.openqa.selenium.WebDriverException, + org.openqa.selenium.grid.data.CreateSessionResponse> + newSession(org.openqa.selenium.grid.data.CreateSessionRequest sessionRequest) { + return null; + } + + @Override + public org.openqa.selenium.remote.http.HttpResponse executeWebDriverCommand( + org.openqa.selenium.remote.http.HttpRequest req) { + return null; + } + + @Override + public org.openqa.selenium.grid.data.Session getSession( + org.openqa.selenium.remote.SessionId id) { + return null; + } + + @Override + public org.openqa.selenium.remote.http.HttpResponse uploadFile( + org.openqa.selenium.remote.http.HttpRequest req, org.openqa.selenium.remote.SessionId id) { + return null; + } + + @Override + public org.openqa.selenium.remote.http.HttpResponse downloadFile( + org.openqa.selenium.remote.http.HttpRequest req, org.openqa.selenium.remote.SessionId id) { + return null; + } + + @Override + public void stop(org.openqa.selenium.remote.SessionId id) {} + + @Override + public boolean isSessionOwner(org.openqa.selenium.remote.SessionId id) { + return false; + } + + @Override + public boolean tryAcquireConnection(org.openqa.selenium.remote.SessionId id) { + return false; + } + + @Override + public void releaseConnection(org.openqa.selenium.remote.SessionId id) {} + + @Override + public boolean isSupporting(org.openqa.selenium.Capabilities capabilities) { + return false; + } + + @Override + public org.openqa.selenium.grid.data.NodeStatus getStatus() { + return null; + } + + @Override + public org.openqa.selenium.grid.node.HealthCheck getHealthCheck() { + return null; + } + + @Override + public void drain() {} + } +} diff --git a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java index 904c3cd158874..05ba70a903439 100644 --- a/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java +++ b/java/test/org/openqa/selenium/grid/node/config/NodeOptionsTest.java @@ -193,6 +193,39 @@ void cdpCanBeDisabled() { assertThat(nodeOptions.isCdpEnabled()).isFalse(); } + @Test + void statusFileCanBeConfigured() { + Config config = + new MapConfig(singletonMap("node", singletonMap("status-to-file", "node-status.json"))); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getStatusFile()).isPresent(); + assertThat(nodeOptions.getStatusFile().get()).isEqualTo("node-status.json"); + } + + @Test + void statusFileIsOptionalByDefault() { + Config config = new MapConfig(emptyMap()); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getStatusFile()).isEmpty(); + } + + @Test + void sessionHistoryFileCanBeConfigured() { + Config config = + new MapConfig( + singletonMap("node", singletonMap("session-history-to-file", "session-history.json"))); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getSessionHistoryFile()).isPresent(); + assertThat(nodeOptions.getSessionHistoryFile().get()).isEqualTo("session-history.json"); + } + + @Test + void sessionHistoryFileIsOptionalByDefault() { + Config config = new MapConfig(emptyMap()); + NodeOptions nodeOptions = new NodeOptions(config); + assertThat(nodeOptions.getSessionHistoryFile()).isEmpty(); + } + @Test void shouldDetectCorrectDriversOnMac() { assumeTrue(Platform.getCurrent().is(Platform.MAC)); diff --git a/java/test/org/openqa/selenium/grid/node/local/LocalNodeTest.java b/java/test/org/openqa/selenium/grid/node/local/LocalNodeTest.java index ccd2b44f3dae8..4df7ba559f27a 100644 --- a/java/test/org/openqa/selenium/grid/node/local/LocalNodeTest.java +++ b/java/test/org/openqa/selenium/grid/node/local/LocalNodeTest.java @@ -25,8 +25,11 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -434,4 +437,143 @@ void bidiIsDisabledAndResponseCapsShowThat() throws URISyntaxException { assertThat(bidiEnabled).isNotNull(); assertThat(Boolean.parseBoolean(bidiEnabled.toString())).isFalse(); } + + @Test + void statusFileIsWrittenWhenConfigured() throws URISyntaxException, IOException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + Path tempStatusFile = Files.createTempFile("node-status", ".json"); + tempStatusFile.toFile().deleteOnExit(); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .advanced() + .statusFile(Optional.of(tempStatusFile.toString())) + .build(); + + NodeStatus status = localNode.getStatus(); + assertThat(status).isNotNull(); + + assertThat(Files.exists(tempStatusFile)).isTrue(); + String statusContent = Files.readString(tempStatusFile); + assertThat(statusContent).isNotEmpty(); + assertThat(statusContent).contains("\"nodeId\""); + assertThat(statusContent).contains("\"uri\""); + } + + @Test + void statusFileIsNotWrittenWhenNotConfigured() throws URISyntaxException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .build(); + + NodeStatus status = localNode.getStatus(); + assertThat(status).isNotNull(); + } + + @Test + void sessionHistoryIsWrittenWhenConfigured() throws URISyntaxException, IOException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + Path tempHistoryFile = Files.createTempFile("session-history", ".json"); + tempHistoryFile.toFile().deleteOnExit(); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .advanced() + .sessionHistoryFile(Optional.empty(), Optional.of(tempHistoryFile.toString())) + .build(); + + Either response = + localNode.newSession( + new CreateSessionRequest(ImmutableSet.of(W3C), stereotype, ImmutableMap.of())); + assertThat(response.isRight()).isTrue(); + + SessionId sessionId = response.right().getSession().getId(); + localNode.stop(sessionId); + + assertThat(Files.exists(tempHistoryFile)).isTrue(); + String historyContent = Files.readString(tempHistoryFile); + assertThat(historyContent).isNotEmpty(); + assertThat(historyContent).contains(sessionId.toString()); + assertThat(historyContent).contains("startTime"); + assertThat(historyContent).contains("stopTime"); + } + + @Test + void sessionHistoryIsNotWrittenWhenNotConfigured() throws URISyntaxException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .build(); + + Either response = + localNode.newSession( + new CreateSessionRequest(ImmutableSet.of(W3C), stereotype, ImmutableMap.of())); + assertThat(response.isRight()).isTrue(); + + SessionId sessionId = response.right().getSession().getId(); + localNode.stop(sessionId); + } + + @Test + void sessionHistoryEndpointReturnsCorrectData() throws URISyntaxException { + Tracer tracer = DefaultTestTracer.createTracer(); + EventBus bus = new GuavaEventBus(); + URI uri = new URI("http://localhost:7890"); + Capabilities stereotype = new ImmutableCapabilities("browserName", "cheese"); + + LocalNode localNode = + LocalNode.builder(tracer, bus, uri, uri, registrationSecret) + .add( + stereotype, + new TestSessionFactory( + (id, caps) -> new Session(id, uri, stereotype, caps, Instant.now()))) + .build(); + + Either response = + localNode.newSession( + new CreateSessionRequest(ImmutableSet.of(W3C), stereotype, ImmutableMap.of())); + assertThat(response.isRight()).isTrue(); + + SessionId sessionId = response.right().getSession().getId(); + localNode.stop(sessionId); + + List history = localNode.getSessionHistory(); + assertThat(history).hasSize(1); + assertThat(history.get(0).getSessionId()).isEqualTo(sessionId); + assertThat(history.get(0).getStartTime()).isNotNull(); + assertThat(history.get(0).getStopTime()).isNotNull(); + } } From 9436e069688442a1d799684e81f90dd56a33cafb Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Mon, 9 Jun 2025 03:30:49 +0700 Subject: [PATCH 2/6] Fix tests --- .../selenium/grid/data/SessionStatus.java | 23 ------ .../org/openqa/selenium/grid/node/Node.java | 7 +- .../selenium/grid/node/local/LocalNode.java | 56 ++++++++++---- .../grid/data/SessionClosedEventTest.java | 73 ------------------- 4 files changed, 45 insertions(+), 114 deletions(-) delete mode 100644 java/src/org/openqa/selenium/grid/data/SessionStatus.java delete mode 100644 java/test/org/openqa/selenium/grid/data/SessionClosedEventTest.java diff --git a/java/src/org/openqa/selenium/grid/data/SessionStatus.java b/java/src/org/openqa/selenium/grid/data/SessionStatus.java deleted file mode 100644 index 2f75d02b2f030..0000000000000 --- a/java/src/org/openqa/selenium/grid/data/SessionStatus.java +++ /dev/null @@ -1,23 +0,0 @@ -// 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.data; - -public enum SessionStatus { - SUCCESS, - FAILED -} diff --git a/java/src/org/openqa/selenium/grid/node/Node.java b/java/src/org/openqa/selenium/grid/node/Node.java index fa799f8a6d2ba..dd178853792d6 100644 --- a/java/src/org/openqa/selenium/grid/node/Node.java +++ b/java/src/org/openqa/selenium/grid/node/Node.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.ServiceLoader; @@ -271,10 +272,12 @@ public TemporaryFilesystem getDownloadsFilesystem(SessionId id) throws IOExcepti public abstract NodeStatus getStatus(); - public abstract List getSessionHistory(); - public abstract HealthCheck getHealthCheck(); + public List getSessionHistory() { + return new ArrayList<>(); + } + public Duration getSessionTimeout() { return sessionTimeout; } 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 bea9841e7d071..410c1affe02b5 100644 --- a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java +++ b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java @@ -63,7 +63,6 @@ import java.util.Queue; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; @@ -160,7 +159,6 @@ public class LocalNode extends Node implements Closeable { private final Optional statusFilePath; private final Optional sessionHistoryFilePath; private final Queue sessionHistory = new ConcurrentLinkedQueue<>(); - private final Map sessionStartTimes = new ConcurrentHashMap<>(); protected LocalNode( Tracer tracer, @@ -308,6 +306,7 @@ protected LocalNode( bus.addListener(SessionStartedEvent.listener(this::recordSessionStart)); bus.addListener(SessionClosedEvent.listener(this::recordSessionStop)); + bus.addListener(NodeHeartBeatEvent.listener(this::cleanupSessionHistory)); shutdown = () -> { @@ -1138,33 +1137,58 @@ private void recordSessionStart(SessionId sessionId) { return; } Instant startTime = Instant.now(); - sessionStartTimes.put(sessionId, startTime); sessionHistory.add(new SessionHistoryEntry(sessionId, startTime, null)); writeSessionHistoryToFile(); } private void recordSessionStop(SessionId sessionId) { - if (!isSessionOwner(sessionId)) { - return; - } Instant stopTime = Instant.now(); - Instant startTime = sessionStartTimes.remove(sessionId); - if (startTime != null) { - // Find and update the existing history entry - sessionHistory.stream() - .filter(entry -> entry.getSessionId().equals(sessionId)) - .findFirst() - .ifPresent(entry -> entry.setStopTime(stopTime)); - writeSessionHistoryToFile(); + // Find and update the existing history entry + sessionHistory.stream() + .filter(entry -> entry.getSessionId().equals(sessionId)) + .findFirst() + .ifPresent( + entry -> { + entry.setStopTime(stopTime); + writeSessionHistoryToFile(); + }); + } + + private void cleanupSessionHistory(NodeStatus status) { + int maxHistorySize = 100; + if (!status.getNodeId().equals(getId()) || sessionHistory.size() < maxHistorySize) { + return; } + + // Keep only the last 100 completed sessions + List completedSessions = + sessionHistory.stream() + .filter(entry -> entry.getStopTime() != null) + .sorted( + (a, b) -> + b.getStartTime().compareTo(a.getStartTime())) // Sort by start time descending + .limit(100) + .collect(Collectors.toList()); + + // Keep all ongoing sessions + List ongoingSessions = + sessionHistory.stream() + .filter(entry -> entry.getStopTime() == null) + .collect(Collectors.toList()); + + // Clear and rebuild the history queue + sessionHistory.clear(); + sessionHistory.addAll(completedSessions); + sessionHistory.addAll(ongoingSessions); + + // Write the cleaned history to file + writeSessionHistoryToFile(); } private void writeSessionHistoryToFile() { if (sessionHistoryFilePath.isPresent()) { try { List sortedHistory = new ArrayList<>(sessionHistory); - sortedHistory.sort((a, b) -> a.getStartTime().compareTo(b.getStartTime())); - String historyJson = JSON.toJson(sortedHistory); Files.write( sessionHistoryFilePath.get(), diff --git a/java/test/org/openqa/selenium/grid/data/SessionClosedEventTest.java b/java/test/org/openqa/selenium/grid/data/SessionClosedEventTest.java deleted file mode 100644 index 0b5527ad2aec9..0000000000000 --- a/java/test/org/openqa/selenium/grid/data/SessionClosedEventTest.java +++ /dev/null @@ -1,73 +0,0 @@ -// 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.data; - -import static org.assertj.core.api.Assertions.assertThat; - -import org.junit.jupiter.api.Test; -import org.openqa.selenium.json.Json; -import org.openqa.selenium.remote.SessionId; - -class SessionClosedEventTest { - - @Test - void shouldSerializeAndDeserializeWithSuccessStatus() { - SessionId sessionId = new SessionId("test-session-123"); - SessionClosedEvent originalEvent = new SessionClosedEvent(sessionId, SessionStatus.SUCCESS); - - Json json = new Json(); - String serialized = json.toJson(originalEvent); - SessionClosedEvent deserializedEvent = json.toType(serialized, SessionClosedEvent.class); - - assertThat(deserializedEvent).isNotNull(); - assertThat(deserializedEvent.getData(SessionId.class)).isEqualTo(sessionId); - assertThat(deserializedEvent.getStatus()).isEqualTo(SessionStatus.SUCCESS); - } - - @Test - void shouldSerializeAndDeserializeWithFailedStatus() { - SessionId sessionId = new SessionId("test-session-456"); - SessionClosedEvent originalEvent = new SessionClosedEvent(sessionId, SessionStatus.FAILED); - - Json json = new Json(); - String serialized = json.toJson(originalEvent); - SessionClosedEvent deserializedEvent = json.toType(serialized, SessionClosedEvent.class); - - assertThat(deserializedEvent).isNotNull(); - assertThat(deserializedEvent.getData(SessionId.class)).isEqualTo(sessionId); - assertThat(deserializedEvent.getStatus()).isEqualTo(SessionStatus.FAILED); - } - - @Test - void shouldUseDefaultSuccessStatusWhenNotSpecified() { - SessionId sessionId = new SessionId("test-session-789"); - SessionClosedEvent event = new SessionClosedEvent(sessionId); - - assertThat(event.getStatus()).isEqualTo(SessionStatus.SUCCESS); - } - - @Test - void shouldHandleEventDataSerialization() { - SessionId sessionId = new SessionId("test-session-abc"); - SessionClosedEvent event = new SessionClosedEvent(sessionId, SessionStatus.FAILED); - - String rawData = event.getRawData(); - assertThat(rawData).isNotEmpty(); - assertThat(rawData).contains(sessionId.toString()); - } -} From 471f17f821c33518a92aa080b0a9c578478497ef Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Mon, 9 Jun 2025 03:33:29 +0700 Subject: [PATCH 3/6] Suggestion: Add missing write permission Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- java/src/org/openqa/selenium/grid/node/local/LocalNode.java | 1 + 1 file changed, 1 insertion(+) 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 410c1affe02b5..94a65faec6c8b 100644 --- a/java/src/org/openqa/selenium/grid/node/local/LocalNode.java +++ b/java/src/org/openqa/selenium/grid/node/local/LocalNode.java @@ -1194,6 +1194,7 @@ private void writeSessionHistoryToFile() { sessionHistoryFilePath.get(), historyJson.getBytes(), StandardOpenOption.CREATE, + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING); } catch (IOException e) { LOG.log(Level.WARNING, "Unable to write session history to file", e); From b182d49ee323618866f81a91a85f9c3094d7eb95 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Mon, 9 Jun 2025 03:33:41 +0700 Subject: [PATCH 4/6] Suggestion: Use static empty list Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- java/src/org/openqa/selenium/grid/node/Node.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/org/openqa/selenium/grid/node/Node.java b/java/src/org/openqa/selenium/grid/node/Node.java index dd178853792d6..0af813dc02bba 100644 --- a/java/src/org/openqa/selenium/grid/node/Node.java +++ b/java/src/org/openqa/selenium/grid/node/Node.java @@ -275,7 +275,7 @@ public TemporaryFilesystem getDownloadsFilesystem(SessionId id) throws IOExcepti public abstract HealthCheck getHealthCheck(); public List getSessionHistory() { - return new ArrayList<>(); + return Collections.emptyList(); } public Duration getSessionTimeout() { From b9c2a1534287bf59c960cde0b1ee2facf933a929 Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Mon, 9 Jun 2025 03:37:35 +0700 Subject: [PATCH 5/6] Run format --- java/src/org/openqa/selenium/grid/node/Node.java | 1 - 1 file changed, 1 deletion(-) diff --git a/java/src/org/openqa/selenium/grid/node/Node.java b/java/src/org/openqa/selenium/grid/node/Node.java index 0af813dc02bba..49e6b53bf3407 100644 --- a/java/src/org/openqa/selenium/grid/node/Node.java +++ b/java/src/org/openqa/selenium/grid/node/Node.java @@ -29,7 +29,6 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.ServiceLoader; From 0b19339bd98a6afa242efd132a7b5f1cb8dd4b3a Mon Sep 17 00:00:00 2001 From: Viet Nguyen Duc Date: Mon, 9 Jun 2025 03:51:38 +0700 Subject: [PATCH 6/6] Run format --- java/src/org/openqa/selenium/grid/node/Node.java | 1 + 1 file changed, 1 insertion(+) diff --git a/java/src/org/openqa/selenium/grid/node/Node.java b/java/src/org/openqa/selenium/grid/node/Node.java index 49e6b53bf3407..e00e79be675e2 100644 --- a/java/src/org/openqa/selenium/grid/node/Node.java +++ b/java/src/org/openqa/selenium/grid/node/Node.java @@ -29,6 +29,7 @@ import java.io.IOException; import java.net.URI; import java.time.Duration; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.ServiceLoader;