All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.stargate.sgv2.graphql.web.resources.GraphqlCache Maven / Gradle / Ivy

There is a newer version: 2.0.0-ALPHA-17
Show newest version
/*
 * Copyright The Stargate Authors
 *
 * Licensed 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 io.stargate.sgv2.graphql.web.resources;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import graphql.GraphQL;
import graphql.execution.AsyncExecutionStrategy;
import graphql.schema.GraphQLSchema;
import io.stargate.bridge.proto.Schema;
import io.stargate.bridge.proto.Schema.CqlKeyspaceDescribe;
import io.stargate.sgv2.common.futures.Futures;
import io.stargate.sgv2.common.grpc.StargateBridgeClient;
import io.stargate.sgv2.common.grpc.UnauthorizedKeyspaceException;
import io.stargate.sgv2.graphql.persistence.graphqlfirst.SchemaSource;
import io.stargate.sgv2.graphql.persistence.graphqlfirst.SchemaSourceDao;
import io.stargate.sgv2.graphql.schema.CassandraFetcherExceptionHandler;
import io.stargate.sgv2.graphql.schema.cqlfirst.SchemaFactory;
import io.stargate.sgv2.graphql.schema.graphqlfirst.AdminSchemaBuilder;
import io.stargate.sgv2.graphql.schema.graphqlfirst.migration.CassandraMigrator;
import io.stargate.sgv2.graphql.schema.graphqlfirst.processor.ProcessedSchema;
import io.stargate.sgv2.graphql.schema.graphqlfirst.processor.SchemaProcessor;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * Manages the {@link GraphQL} instances used by our REST resources.
 *
 * 

This includes staying up to date with CQL schema changes. */ public class GraphqlCache { private final boolean disableDefaultKeyspace; private final GraphQL ddlGraphql; private final GraphQL schemaFirstAdminGraphql; private volatile CompletionStage> defaultKeyspaceName; private final Cache dmlGraphqlCache = Caffeine.newBuilder().maximumSize(1000).expireAfterAccess(5, TimeUnit.MINUTES).build(); public GraphqlCache(boolean disableDefaultKeyspace) { this.disableDefaultKeyspace = disableDefaultKeyspace; this.ddlGraphql = newGraphql(SchemaFactory.newDdlSchema()); this.schemaFirstAdminGraphql = newGraphql(new AdminSchemaBuilder().build()); } public GraphQL getDdl() { return ddlGraphql; } public GraphQL getSchemaFirstAdminGraphql() { return schemaFirstAdminGraphql; } /** * @throws UnauthorizedKeyspaceException (wrapped in the future) if the client is not authorized * to read the keyspace's contents. */ public CompletionStage> getDmlAsync( StargateBridgeClient bridge, String keyspaceName) { String decoratedKeyspaceName = bridge.decorateKeyspaceName(keyspaceName); GraphqlHolder holder = dmlGraphqlCache.get(decoratedKeyspaceName, __ -> new GraphqlHolder(keyspaceName)); assert holder != null; return holder.getGraphql(bridge); } public Optional getDml(StargateBridgeClient bridge, String keyspaceName) { return Futures.getUninterruptibly(getDmlAsync(bridge, keyspaceName)); } /** * Fill the cache entry when it's been computed externally. This is used after a schema-first * deployment: we already have the GraphQL since it's been generated in order to validate the * deployment, so use it instead of having the cache recompute it. */ public void putDml( CqlKeyspaceDescribe keyspaceDescribe, SchemaSource newSource, GraphQL graphql) { Schema.CqlKeyspace keyspace = keyspaceDescribe.getCqlKeyspace(); GraphqlHolder holder = dmlGraphqlCache.get(keyspace.getGlobalName(), __ -> new GraphqlHolder(keyspace.getName())); assert holder != null; holder.putGraphql(graphql, keyspaceDescribe.getHash().getValue(), newSource); } public CompletionStage> getDefaultKeyspaceNameAsync( StargateBridgeClient bridge) { // Lazy init with double-checked locking: CompletionStage> result = this.defaultKeyspaceName; if (result != null) { return result; } synchronized (this) { if (defaultKeyspaceName == null) { defaultKeyspaceName = disableDefaultKeyspace ? CompletableFuture.completedFuture(Optional.empty()) : new DefaultKeyspaceHelper(bridge).find(); } return defaultKeyspaceName; } } public Optional getDefaultKeyspaceName(StargateBridgeClient bridge) { return Futures.getUninterruptibly(getDefaultKeyspaceNameAsync(bridge)); } private static GraphQL newGraphql(GraphQLSchema schema) { return GraphQL.newGraphQL(schema) .defaultDataFetcherExceptionHandler(CassandraFetcherExceptionHandler.INSTANCE) // Use parallel execution strategy for mutations (serial is default) .mutationExecutionStrategy( new AsyncExecutionStrategy(CassandraFetcherExceptionHandler.INSTANCE)) .build(); } static class GraphqlHolder { private final String keyspaceName; private final AtomicReference stateRef = new AtomicReference<>(null); GraphqlHolder(String keyspaceName) { this.keyspaceName = keyspaceName; } CompletionStage> getGraphql(StargateBridgeClient bridge) { CompletionStage> keyspaceFuture = bridge.getKeyspaceAsync(keyspaceName, true); return keyspaceFuture.thenCompose( maybeKeyspace -> maybeKeyspace .map(keyspace -> handleExisting(keyspace, bridge)) .orElse(handleMissing())); } // Handles a possible state change when we know the keyspace doesn't exist. private CompletionStage> handleMissing() { stateRef.set(null); return CompletableFuture.completedFuture(Optional.empty()); } // Handles a possible state change when we know the keyspace exists. private CompletionStage> handleExisting( CqlKeyspaceDescribe keyspace, StargateBridgeClient bridge) { // Next step is to check if this is a GraphQL schema-first keyspace CompletionStage> sourceFuture = new SchemaSourceDao(bridge).getLatestVersionAsync(keyspaceName); return sourceFuture.thenCompose( maybeSource -> { GraphqlHolderState newState = new GraphqlHolderState(keyspace.getHash().getValue(), maybeSource); GraphqlHolderState oldState = stateRef.get(); if (newState.equals(oldState)) { // The state matches, we already have the latest version return oldState.graphqlFuture; } else if (stateRef.compareAndSet(oldState, newState)) { // We installed our new state, it is our responsibility to recompute compute(keyspace, maybeSource, bridge, newState.graphqlFuture); } else { // Another thread beat us to computing. Assume this is at least our keyspace // hash (or even a more recent one). newState = stateRef.get(); } return newState.graphqlFuture; }); } private void compute( CqlKeyspaceDescribe keyspace, Optional maybeSource, StargateBridgeClient bridge, CompletableFuture> toComplete) { GraphQL graphql = maybeSource .map(source -> computeSchemaFirst(keyspace, bridge, source)) .orElseGet(() -> computeCqlFirst(keyspace)); toComplete.complete(Optional.of(graphql)); } private GraphQL computeSchemaFirst( CqlKeyspaceDescribe keyspace, StargateBridgeClient bridge, SchemaSource source) { ProcessedSchema processedSchema = new SchemaProcessor(bridge, true).process(source.getContents(), keyspace); // Check that the data model still matches CassandraMigrator.forPersisted().compute(processedSchema.getMappingModel(), keyspace); return processedSchema.getGraphql(); } private GraphQL computeCqlFirst(CqlKeyspaceDescribe keyspace) { return newGraphql(SchemaFactory.newDmlSchema(keyspace)); } void putGraphql(GraphQL graphql, int hash, SchemaSource newSource) { GraphqlHolderState newState = new GraphqlHolderState(hash, Optional.of(newSource)); newState.graphqlFuture.complete(Optional.of(graphql)); stateRef.set(newState); } } static class GraphqlHolderState { // The hash of the CqlKeyspaceDescribe that this GraphQL is based on. final int hash; // If this is a schema-first GraphQL, the source that it is based on. final Optional source; // The result of the computation of this GraphQL (possibly still in progress). final CompletableFuture> graphqlFuture; GraphqlHolderState(int hash, Optional source) { this.hash = hash; this.source = source; this.graphqlFuture = new CompletableFuture<>(); } @Override public boolean equals(Object other) { // Note: graphqlFuture deliberately omitted if (other == this) { return true; } else if (other instanceof GraphqlHolderState) { GraphqlHolderState that = (GraphqlHolderState) other; return this.hash == that.hash && this.source.equals(that.source); } else { return false; } } @Override public int hashCode() { return Objects.hash(hash, source); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy