Skip to content

Commit ea55a8a

Browse files
committed
[grid] Implement custom cache weigher and enhance query handling in GraphqlHandler
1 parent 805f545 commit ea55a8a

File tree

1 file changed

+77
-11
lines changed

1 file changed

+77
-11
lines changed

java/src/org/openqa/selenium/grid/graphql/GraphqlHandler.java

Lines changed: 77 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030

3131
import com.github.benmanes.caffeine.cache.Cache;
3232
import com.github.benmanes.caffeine.cache.Caffeine;
33+
import com.github.benmanes.caffeine.cache.Weigher;
3334
import graphql.ExecutionInput;
3435
import graphql.ExecutionResult;
3536
import graphql.GraphQL;
@@ -43,6 +44,7 @@
4344
import java.io.InputStream;
4445
import java.io.UncheckedIOException;
4546
import java.net.URI;
47+
import java.time.Duration;
4648
import java.util.HashMap;
4749
import java.util.Map;
4850
import java.util.concurrent.CompletableFuture;
@@ -60,17 +62,19 @@
6062
import org.openqa.selenium.remote.tracing.Status;
6163
import org.openqa.selenium.remote.tracing.Tracer;
6264

63-
public class GraphqlHandler implements HttpHandler {
65+
public class GraphqlHandler implements HttpHandler, AutoCloseable {
6466

6567
public static final String GRID_SCHEMA =
6668
"/org/openqa/selenium/grid/graphql/selenium-grid-schema.graphqls";
6769
public static final Json JSON = new Json();
70+
private static final long MAX_QUERY_SIZE_BYTES = 1024 * 1024; // 1MB limit
6871
private final Tracer tracer;
6972
private final Distributor distributor;
7073
private final NewSessionQueue newSessionQueue;
7174
private final URI publicUri;
7275
private final String version;
7376
private final GraphQL graphQl;
77+
private final Cache<String, CompletableFuture<PreparsedDocumentEntry>> cache;
7478

7579
public GraphqlHandler(
7680
Tracer tracer,
@@ -83,25 +87,45 @@ public GraphqlHandler(
8387
this.publicUri = Require.nonNull("Uri", publicUri);
8488
this.version = Require.nonNull("GridVersion", version);
8589
this.tracer = Require.nonNull("Tracer", tracer);
90+
91+
// Container-aware cache sizing: 5% of heap memory
8692
long maxMemory = Runtime.getRuntime().maxMemory();
87-
long cacheSize = maxMemory / 1024 / 1024;
93+
long cacheWeightLimit = (long) (maxMemory * 0.05);
94+
95+
// Custom weigher to prevent single entries from exceeding 10% of cache weight
96+
Weigher<String, CompletableFuture<PreparsedDocumentEntry>> weigher =
97+
new QueryCacheWeigher(cacheWeightLimit);
98+
99+
this.cache =
100+
Caffeine.newBuilder()
101+
.maximumWeight(cacheWeightLimit)
102+
.weigher(weigher)
103+
.expireAfterAccess(Duration.ofMinutes(30)) // Time-based eviction
104+
.build();
88105

89106
GraphQLSchema schema =
90107
new SchemaGenerator()
91108
.makeExecutableSchema(buildTypeDefinitionRegistry(), buildRuntimeWiring());
92109

93-
Cache<String, CompletableFuture<PreparsedDocumentEntry>> cache =
94-
Caffeine.newBuilder().maximumSize(cacheSize).build();
95-
96110
graphQl =
97111
GraphQL.newGraphQL(schema)
98112
.preparsedDocumentProvider(
99-
(executionInput, computeFunction) ->
100-
cache.get(
101-
executionInput.getQuery(),
102-
key ->
103-
CompletableFuture.supplyAsync(
104-
() -> computeFunction.apply(executionInput))))
113+
(executionInput, computeFunction) -> {
114+
String query = executionInput.getQuery();
115+
116+
// Query size validation with bypass for oversized queries
117+
if (query.getBytes().length > MAX_QUERY_SIZE_BYTES) {
118+
// Bypass cache for oversized queries to prevent cache pollution
119+
return CompletableFuture.supplyAsync(
120+
() -> computeFunction.apply(executionInput));
121+
}
122+
123+
return cache.get(
124+
query,
125+
key ->
126+
CompletableFuture.supplyAsync(
127+
() -> computeFunction.apply(executionInput)));
128+
})
105129
.build();
106130
}
107131

@@ -180,6 +204,24 @@ public HttpResponse execute(HttpRequest req) throws UncheckedIOException {
180204
}
181205
}
182206

207+
@Override
208+
public void close() {
209+
// Cancel pending CompletableFutures
210+
cache
211+
.asMap()
212+
.values()
213+
.forEach(
214+
future -> {
215+
if (!future.isDone()) {
216+
future.cancel(true);
217+
}
218+
});
219+
220+
// Invalidate and clean cache
221+
cache.invalidateAll();
222+
cache.cleanUp();
223+
}
224+
183225
private RuntimeWiring buildRuntimeWiring() {
184226
GridData gridData = new GridData(distributor, newSessionQueue, publicUri, version);
185227
return RuntimeWiring.newRuntimeWiring()
@@ -203,4 +245,28 @@ private TypeDefinitionRegistry buildTypeDefinitionRegistry() {
203245
throw new UncheckedIOException(e);
204246
}
205247
}
248+
249+
// Custom weigher class for query cache
250+
private static class QueryCacheWeigher
251+
implements Weigher<String, CompletableFuture<PreparsedDocumentEntry>> {
252+
253+
private final long maxSingleEntryWeight;
254+
255+
public QueryCacheWeigher(long cacheWeightLimit) {
256+
this.maxSingleEntryWeight = (long) (cacheWeightLimit * 0.1); // 10% limit
257+
}
258+
259+
@Override
260+
public int weigh(String key, CompletableFuture<PreparsedDocumentEntry> value) {
261+
// Estimate memory usage including CompletableFuture overhead
262+
long keyWeight = key.length() * 2; // UTF-16 encoding
263+
long futureOverhead = 200; // Estimated CompletableFuture overhead
264+
long documentOverhead = 500; // Estimated PreparsedDocumentEntry overhead
265+
266+
long totalWeight = keyWeight + futureOverhead + documentOverhead;
267+
268+
// Bounds checking to prevent single entries from dominating cache
269+
return (int) Math.min(totalWeight, maxSingleEntryWeight);
270+
}
271+
}
206272
}

0 commit comments

Comments
 (0)