3030
3131import com .github .benmanes .caffeine .cache .Cache ;
3232import com .github .benmanes .caffeine .cache .Caffeine ;
33+ import com .github .benmanes .caffeine .cache .Weigher ;
3334import graphql .ExecutionInput ;
3435import graphql .ExecutionResult ;
3536import graphql .GraphQL ;
4344import java .io .InputStream ;
4445import java .io .UncheckedIOException ;
4546import java .net .URI ;
47+ import java .time .Duration ;
4648import java .util .HashMap ;
4749import java .util .Map ;
4850import java .util .concurrent .CompletableFuture ;
6062import org .openqa .selenium .remote .tracing .Status ;
6163import 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