Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions java/src/org/openqa/selenium/grid/data/SessionClosedEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.openqa.selenium.grid.data;

import java.util.Objects;
import java.util.function.Consumer;
import org.openqa.selenium.events.Event;
import org.openqa.selenium.events.EventListener;
Expand All @@ -26,15 +27,39 @@

public class SessionClosedEvent extends Event {

public static final String DEFAULT_REASON = "session-closed-event";

private static final EventName SESSION_CLOSED = new EventName("session-closed");

public SessionClosedEvent(SessionId id) {
super(SESSION_CLOSED, id);
this(id, DEFAULT_REASON);
}

public SessionClosedEvent(SessionId id, String reason) {
super(SESSION_CLOSED, new Data(id, reason));
}

public static EventListener<SessionId> listener(Consumer<SessionId> handler) {
public static EventListener<Data> listener(Consumer<Data> handler) {
Require.nonNull("Handler", handler);

return new EventListener<>(SESSION_CLOSED, SessionId.class, handler);
return new EventListener<>(SESSION_CLOSED, Data.class, handler);
}

public static class Data {
private final SessionId sessionId;
private final String reason;

private Data(SessionId sessionId, String reason) {
this.sessionId = Require.nonNull("Session id", sessionId);
this.reason = Objects.requireNonNullElse(reason, DEFAULT_REASON);
}

public SessionId getSessionId() {
return sessionId;
}

public String getReason() {
return reason;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public LocalGridModel(EventBus events) {
this.events = Require.nonNull("Event bus", events);

this.events.addListener(NodeDrainStarted.listener(nodeId -> setAvailability(nodeId, DRAINING)));
this.events.addListener(SessionClosedEvent.listener(this::release));
this.events.addListener(
SessionClosedEvent.listener(event -> this.release(event.getSessionId())));
}

public static LocalGridModel create(Config config) {
Expand Down
8 changes: 7 additions & 1 deletion java/src/org/openqa/selenium/grid/node/local/LocalNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
import org.openqa.selenium.grid.node.config.NodeOptions;
import org.openqa.selenium.grid.node.docker.DockerSession;
import org.openqa.selenium.grid.security.Secret;
import org.openqa.selenium.grid.sessionmap.SessionMap;
import org.openqa.selenium.internal.Debug;
import org.openqa.selenium.internal.Either;
import org.openqa.selenium.internal.Require;
Expand Down Expand Up @@ -321,6 +322,11 @@ private void stopTimedOutSession(SessionId id, SessionSlot slot, RemovalCause ca
attributeMap.put("session.id", id.toString());
attributeMap.put("session.timeout_in_seconds", getSessionTimeout().toSeconds());
attributeMap.put("session.remove.cause", cause.name());
String closeReason =
cause == RemovalCause.EXPIRED
? SessionMap.REASON_SESSION_TIMEOUT
: SessionMap.REASON_SESSION_CLOSED_EVENT;
attributeMap.put("session.close.reason", closeReason);
if (cause == RemovalCause.EXPIRED) {
// Session is timing out, stopping it by sending a DELETE
LOG.log(Level.INFO, () -> String.format("Session id %s timed out, stopping...", id));
Expand All @@ -342,7 +348,7 @@ private void stopTimedOutSession(SessionId id, SessionSlot slot, RemovalCause ca
}
}
// Attempt to stop the session
slot.stop();
slot.stop(closeReason);
// Decrement pending sessions if Node is draining
if (this.isDraining()) {
int done = pendingSessions.decrementAndGet();
Expand Down
7 changes: 4 additions & 3 deletions java/src/org/openqa/selenium/grid/node/local/SessionSlot.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,12 @@ public ActiveSession getSession() {
return currentSession;
}

public void stop() {
public void stop(String reason) {
if (isAvailable()) {
return;
}

String closeReason = Require.nonNull("Session close reason", reason);
SessionId id = currentSession.getId();
try {
currentSession.stop();
Expand All @@ -117,8 +118,8 @@ public void stop() {
currentSession = null;
connectionCounter.set(0);
release();
bus.fire(new SessionClosedEvent(id));
LOG.info(String.format("Stopping session %s", id));
bus.fire(new SessionClosedEvent(id, closeReason));
LOG.info(String.format("Stopping session %s (reason: %s)", id, closeReason));
}

@Override
Expand Down
1 change: 1 addition & 0 deletions java/src/org/openqa/selenium/grid/router/Router.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ public Router(
routes =
combine(
get("/status").to(() -> new GridStatusHandler(tracer, distributor)),
get("/se/grid/sessions/history").to(() -> new SessionHistoryHandler(tracer, sessions)),
sessions.with(new SpanDecorator(tracer, req -> "session_map")),
queue.with(new SpanDecorator(tracer, req -> "session_queue")),
distributor.with(new SpanDecorator(tracer, req -> "distributor")),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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.router;

import static org.openqa.selenium.remote.http.Contents.asJson;
import static org.openqa.selenium.remote.tracing.HttpTracing.newSpanAsChildOf;
import static org.openqa.selenium.remote.tracing.Tags.HTTP_REQUEST;

import com.google.common.collect.ImmutableMap;
import java.time.Instant;
import java.util.List;
import org.openqa.selenium.grid.sessionmap.SessionHistoryFilters;
import org.openqa.selenium.grid.sessionmap.SessionMap;
import org.openqa.selenium.grid.sessionmap.SessionMetadata;
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;
import org.openqa.selenium.remote.tracing.Span;
import org.openqa.selenium.remote.tracing.Tracer;

class SessionHistoryHandler implements HttpHandler {

private final Tracer tracer;
private final SessionMap sessions;

SessionHistoryHandler(Tracer tracer, SessionMap sessions) {
this.tracer = Require.nonNull("Tracer", tracer);
this.sessions = Require.nonNull("Session map", sessions);
}

@Override
public HttpResponse execute(HttpRequest req) {
try (Span span = newSpanAsChildOf(tracer, req, "router.get_session_history")) {
HTTP_REQUEST.accept(span, req);

SessionHistoryFilters filters;
try {
filters = SessionHistoryFilters.fromRequest(req);
} catch (IllegalArgumentException e) {
return new HttpResponse().setStatus(400).setContent(errorPayload(e.getMessage()));
}

filters
.getSessionId()
.ifPresent(sessionId -> span.setAttribute("session.history.sessionId", sessionId.toString()));
filters
.getCloseReason()
.ifPresent(reason -> span.setAttribute("session.history.reason", reason));
filters
.getStartedAfter()
.map(Instant::toString)
.ifPresent(value -> span.setAttribute("session.history.startedAfter", value));
filters
.getEndedAfter()
.map(Instant::toString)
.ifPresent(value -> span.setAttribute("session.history.endedAfter", value));

List<SessionMetadata> history = sessions.getSessionHistory(filters);
return new HttpResponse().setContent(asJson(ImmutableMap.of("value", history)));
}
}

private byte[] errorPayload(String message) {
return asJson(
ImmutableMap.of(
"value",
ImmutableMap.of(
"error", "invalid argument", "message", message, "stacktrace", ""),
"status",
13));
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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;

import static org.openqa.selenium.remote.http.Contents.asJson;
import static org.openqa.selenium.remote.tracing.HttpTracing.newSpanAsChildOf;
import static org.openqa.selenium.remote.tracing.Tags.HTTP_REQUEST;

import com.google.common.collect.ImmutableMap;
import java.time.Instant;
import java.util.List;
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;
import org.openqa.selenium.remote.tracing.Span;
import org.openqa.selenium.remote.tracing.Tracer;

class GetSessionHistory implements HttpHandler {

private final Tracer tracer;
private final SessionMap sessions;

GetSessionHistory(Tracer tracer, SessionMap sessions) {
this.tracer = Require.nonNull("Tracer", tracer);
this.sessions = Require.nonNull("Session map", sessions);
}

@Override
public HttpResponse execute(HttpRequest req) {
try (Span span = newSpanAsChildOf(tracer, req, "sessions.get_session_history")) {
HTTP_REQUEST.accept(span, req);

SessionHistoryFilters filters;
try {
filters = SessionHistoryFilters.fromRequest(req);
} catch (IllegalArgumentException e) {
return new HttpResponse().setStatus(400).setContent(asJson(errorPayload(e.getMessage())));
}

span.setAttribute("session.history.hasSessionId", filters.getSessionId().isPresent());
filters.getCloseReason().ifPresent(reason -> span.setAttribute("session.history.reason", reason));
filters
.getStartedAfter()
.map(Instant::toString)
.ifPresent(start -> span.setAttribute("session.history.startedAfter", start));
filters
.getEndedAfter()
.map(Instant::toString)
.ifPresent(end -> span.setAttribute("session.history.endedAfter", end));

List<SessionMetadata> value = sessions.getSessionHistory(filters);
return new HttpResponse().setContent(asJson(ImmutableMap.of("value", value)));
}
}

private ImmutableMap<String, Object> errorPayload(String message) {
return ImmutableMap.of(
"value",
ImmutableMap.of(
"error", "invalid argument", "message", message, "stacktrace", ""),
"status",
13);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.openqa.selenium.grid.sessionmap;

import java.time.Instant;
import org.openqa.selenium.NoSuchSessionException;
import org.openqa.selenium.grid.data.Session;
import org.openqa.selenium.remote.SessionId;
Expand All @@ -39,7 +40,7 @@ public Session get(SessionId id) throws NoSuchSessionException {
}

@Override
public void remove(SessionId id) {
public void remove(SessionId id, String reason, Instant endedAt) {
// no-op
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
import static org.openqa.selenium.remote.RemoteTags.SESSION_ID;
import static org.openqa.selenium.remote.tracing.HttpTracing.newSpanAsChildOf;
import static org.openqa.selenium.remote.tracing.Tags.HTTP_REQUEST;
import static org.openqa.selenium.remote.http.Contents.asJson;

import com.google.common.collect.ImmutableMap;
import java.time.Instant;
import java.time.format.DateTimeParseException;
import java.util.Optional;
import org.openqa.selenium.internal.Require;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.remote.http.HttpHandler;
Expand All @@ -47,8 +52,48 @@ public HttpResponse execute(HttpRequest req) {
HTTP_REQUEST.accept(span, req);
SESSION_ID.accept(span, id);

sessions.remove(id);
return new HttpResponse();
String reason =
Optional.ofNullable(req.getQueryParameter("reason"))
.filter(value -> !value.isBlank())
.orElse(SessionMap.REASON_HTTP_REQUEST);
span.setAttribute("session.close.reason", reason);

Instant endedAt = parseEndTime(req).orElseGet(Instant::now);
span.setAttribute("session.close.endedAt", endedAt.toString());

try {
sessions.remove(id, reason, endedAt);
return new HttpResponse();
} catch (IllegalArgumentException e) {
return new HttpResponse().setStatus(400).setContent(errorPayload(e.getMessage()));
}
}
}

private Optional<Instant> parseEndTime(HttpRequest req) {
String endedAt = req.getQueryParameter("endedAt");
if ((endedAt == null || endedAt.isBlank())) {
endedAt = req.getQueryParameter("timestamp");
}

if (endedAt == null || endedAt.isBlank()) {
return Optional.empty();
}

try {
return Optional.of(Instant.parse(endedAt));
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Unable to parse timestamp: " + endedAt, e);
}
}

private byte[] errorPayload(String message) {
return asJson(
ImmutableMap.of(
"value",
ImmutableMap.of(
"error", "invalid argument", "message", message, "stacktrace", ""),
"status",
13));
}
}
Loading