diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java index c7afdd48977..a424a93115a 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractReadContext.java @@ -45,6 +45,7 @@ import com.google.spanner.v1.DirectedReadOptions; import com.google.spanner.v1.ExecuteBatchDmlRequest; import com.google.spanner.v1.ExecuteSqlRequest; +import com.google.spanner.v1.ExecuteSqlRequest.Builder; import com.google.spanner.v1.ExecuteSqlRequest.QueryMode; import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions; import com.google.spanner.v1.PartialResultSet; @@ -457,7 +458,7 @@ void initTransaction() { // A per-transaction sequence number used to identify this ExecuteSqlRequests. Required for DML, // ignored for query by the server. - private AtomicLong seqNo = new AtomicLong(); + private final AtomicLong seqNo = new AtomicLong(); // Allow up to 512MB to be buffered (assuming 1MB chunks). In practice, restart tokens are sent // much more frequently. @@ -488,6 +489,10 @@ long getSeqNo() { return seqNo.incrementAndGet(); } + protected boolean isReadOnly() { + return true; + } + protected boolean isRouteToLeader() { return false; } @@ -622,19 +627,18 @@ private ResultSet executeQueryInternal( @VisibleForTesting QueryOptions buildQueryOptions(QueryOptions requestOptions) { // Shortcut for the most common return value. - if (defaultQueryOptions.equals(QueryOptions.getDefaultInstance()) && requestOptions == null) { - return QueryOptions.getDefaultInstance(); + if (requestOptions == null) { + return defaultQueryOptions; } - // Create a builder based on the default query options. - QueryOptions.Builder builder = defaultQueryOptions.toBuilder(); - // Then overwrite with specific options for this query. - if (requestOptions != null) { - builder.mergeFrom(requestOptions); - } - return builder.build(); + return defaultQueryOptions.toBuilder().mergeFrom(requestOptions).build(); } RequestOptions buildRequestOptions(Options options) { + // Shortcut for the most common return value. + if (!(options.hasPriority() || options.hasTag() || getTransactionTag() != null)) { + return RequestOptions.getDefaultInstance(); + } + RequestOptions.Builder builder = RequestOptions.newBuilder(); if (options.hasPriority()) { builder.setPriority(options.priority()); @@ -655,16 +659,7 @@ ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder( .setSql(statement.getSql()) .setQueryMode(queryMode) .setSession(session.getName()); - Map stmtParameters = statement.getParameters(); - if (!stmtParameters.isEmpty()) { - com.google.protobuf.Struct.Builder paramsBuilder = builder.getParamsBuilder(); - for (Map.Entry param : stmtParameters.entrySet()) { - paramsBuilder.putFields(param.getKey(), Value.toProto(param.getValue())); - if (param.getValue() != null && param.getValue().getType() != null) { - builder.putParamTypes(param.getKey(), param.getValue().getType().toProto()); - } - } - } + addParameters(builder, statement.getParameters()); if (withTransactionSelector) { TransactionSelector selector = getTransactionSelector(); if (selector != null) { @@ -679,12 +674,26 @@ ExecuteSqlRequest.Builder getExecuteSqlRequestBuilder( } else if (defaultDirectedReadOptions != null) { builder.setDirectedReadOptions(defaultDirectedReadOptions); } - builder.setSeqno(getSeqNo()); + if (!isReadOnly()) { + builder.setSeqno(getSeqNo()); + } builder.setQueryOptions(buildQueryOptions(statement.getQueryOptions())); builder.setRequestOptions(buildRequestOptions(options)); return builder; } + static void addParameters(ExecuteSqlRequest.Builder builder, Map stmtParameters) { + if (!stmtParameters.isEmpty()) { + com.google.protobuf.Struct.Builder paramsBuilder = builder.getParamsBuilder(); + for (Map.Entry param : stmtParameters.entrySet()) { + paramsBuilder.putFields(param.getKey(), Value.toProto(param.getValue())); + if (param.getValue() != null && param.getValue().getType() != null) { + builder.putParamTypes(param.getKey(), param.getValue().getType().toProto()); + } + } + } + } + ExecuteBatchDmlRequest.Builder getExecuteBatchDmlRequestBuilder( Iterable statements, Options options) { ExecuteBatchDmlRequest.Builder builder = diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LatencyTest.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LatencyTest.java new file mode 100644 index 00000000000..4f70c32d2b4 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/LatencyTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2021 Google LLC + * + * 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 + * + * https://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 com.google.cloud.spanner; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.cloud.spanner.SpannerOptions.FixedCloseableExecutorProvider; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadLocalRandom; +import org.threeten.bp.Duration; + +public class LatencyTest { + + public static void main(String[] args) throws Exception { + ThreadFactory threadFactory = + ThreadFactoryUtil.tryCreateVirtualThreadFactory("spanner-async-worker"); + if (threadFactory == null) { + return; + } + ScheduledExecutorService service = Executors.newScheduledThreadPool(0, threadFactory); + Spanner spanner = + SpannerOptions.newBuilder() + .setCredentials( + GoogleCredentials.fromStream( + Files.newInputStream( + Paths.get("/Users/loite/Downloads/appdev-soda-spanner-staging.json")))) + .setSessionPoolOption( + SessionPoolOptions.newBuilder() + .setWaitForMinSessions(Duration.ofSeconds(5L)) + // .setUseMultiplexedSession(true) + .build()) + .setUseVirtualThreads(true) + .setAsyncExecutorProvider(FixedCloseableExecutorProvider.create(service)) + .build() + .getService(); + DatabaseClient client = + spanner.getDatabaseClient( + DatabaseId.of("appdev-soda-spanner-staging", "knut-test-ycsb", "latencytest")); + for (int i = 0; i < 1000000; i++) { + try (AsyncResultSet resultSet = + client + .singleUse() + .executeQueryAsync( + Statement.newBuilder("select col_varchar from latency_test where col_bigint=$1") + .bind("p1") + .to(ThreadLocalRandom.current().nextLong(100000L)) + .build())) { + while (resultSet.next()) { + for (int col = 0; col < resultSet.getColumnCount(); col++) { + if (resultSet.getValue(col) == null) { + throw new IllegalStateException(); + } + } + } + } + } + } +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java index 949265ea28a..82b7f06b7d2 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/PartitionedDmlTransaction.java @@ -221,14 +221,6 @@ private ByteString initTransaction(final Options options) { private void setParameters( final ExecuteSqlRequest.Builder requestBuilder, final Map statementParameters) { - if (!statementParameters.isEmpty()) { - com.google.protobuf.Struct.Builder paramsBuilder = requestBuilder.getParamsBuilder(); - for (Map.Entry param : statementParameters.entrySet()) { - paramsBuilder.putFields(param.getKey(), Value.toProto(param.getValue())); - if (param.getValue() != null && param.getValue().getType() != null) { - requestBuilder.putParamTypes(param.getKey(), param.getValue().getType().toProto()); - } - } - } + AbstractReadContext.addParameters(requestBuilder, statementParameters); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java index be306c039bf..d6d72aac33c 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/ResumableStreamIterator.java @@ -60,7 +60,7 @@ abstract class ResumableStreamIterator extends AbstractIterator retryableCodes; private static final Logger logger = Logger.getLogger(ResumableStreamIterator.class.getName()); - private final BackOff backOff; + private BackOff backOff; private final LinkedList buffer = new LinkedList<>(); private final int maxBufferSize; private final ISpan span; @@ -106,7 +106,6 @@ protected ResumableStreamIterator( this.span = tracer.spanBuilderWithExplicitParent(streamName, parent, attributes); this.streamingRetrySettings = Preconditions.checkNotNull(streamingRetrySettings); this.retryableCodes = Preconditions.checkNotNull(retryableCodes); - this.backOff = newBackOff(); } private ExponentialBackOff newBackOff() { @@ -271,7 +270,10 @@ protected PartialResultSet computeNext() { if (delay != -1) { backoffSleep(context, delay); } else { - backoffSleep(context, backOff); + if (this.backOff == null) { + this.backOff = newBackOff(); + } + backoffSleep(context, this.backOff); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java index ab985cebf45..358944e8f36 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionImpl.java @@ -99,6 +99,15 @@ interface SessionTransaction { void close(); } + private static final Map[] CHANNEL_HINT_OPTIONS = + new Map[SpannerOptions.MAX_CHANNELS]; + + static { + for (int i = 0; i < CHANNEL_HINT_OPTIONS.length; i++) { + CHANNEL_HINT_OPTIONS[i] = optionMap(SessionOption.channelHint(i)); + } + } + static final int NO_CHANNEL_HINT = -1; private final SpannerImpl spanner; @@ -125,7 +134,7 @@ interface SessionTransaction { if (channelHint == NO_CHANNEL_HINT) { return sessionReference.getOptions(); } - return optionMap(SessionOption.channelHint(channelHint)); + return CHANNEL_HINT_OPTIONS[channelHint % CHANNEL_HINT_OPTIONS.length]; } @Override diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java index 382bef1b5a2..9e75e5e48c6 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPoolOptions.java @@ -755,7 +755,7 @@ Builder setPoolMaintainerClock(Clock poolMaintainerClock) { * SessionPoolOptions#maxSessions} based on the traffic load. Failing to do so will result in * higher latencies. */ - Builder setUseMultiplexedSession(boolean useMultiplexedSession) { + public Builder setUseMultiplexedSession(boolean useMultiplexedSession) { this.useMultiplexedSession = useMultiplexedSession; return this; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index 447a67c41f5..639943d9970 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -99,7 +99,7 @@ public class SpannerOptions extends ServiceOptions { ImmutableSet.of( "https://www.googleapis.com/auth/spanner.admin", "https://www.googleapis.com/auth/spanner.data"); - private static final int MAX_CHANNELS = 256; + static final int MAX_CHANNELS = 256; @VisibleForTesting static final int DEFAULT_CHANNELS = 4; // Set the default number of channels to GRPC_GCP_ENABLED_DEFAULT_CHANNELS when gRPC-GCP extension // is enabled, to make sure there are sufficient channels available to move the sessions to a diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index e70958968cf..a5401480e06 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -218,6 +218,11 @@ private TransactionContextImpl(Builder builder) { session.getOptions(), ThreadLocalRandom.current().nextLong(Long.MAX_VALUE)); } + @Override + protected boolean isReadOnly() { + return false; + } + @Override protected boolean isRouteToLeader() { return true; diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java index 7de63dc33ba..b32cca08696 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/HeaderInterceptor.java @@ -22,7 +22,11 @@ import static com.google.cloud.spanner.spi.v1.SpannerRpcViews.SPANNER_GFE_HEADER_MISSING_COUNT; import static com.google.cloud.spanner.spi.v1.SpannerRpcViews.SPANNER_GFE_LATENCY; +import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.SpannerRpcMetrics; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.spanner.admin.database.v1.DatabaseName; import io.grpc.CallOptions; import io.grpc.Channel; import io.grpc.ClientCall; @@ -40,6 +44,7 @@ import io.opencensus.tags.Tags; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.common.AttributesBuilder; +import java.util.concurrent.ExecutionException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; @@ -50,15 +55,22 @@ * Missing count metrics. */ class HeaderInterceptor implements ClientInterceptor { - + private static final DatabaseName UNDEFINED_DATABASE_NAME = + DatabaseName.of("undefined-project", "undefined-instance", "undefined-database"); private static final Metadata.Key SERVER_TIMING_HEADER_KEY = Metadata.Key.of("server-timing", Metadata.ASCII_STRING_MARSHALLER); - private static final Pattern SERVER_TIMING_HEADER_PATTERN = Pattern.compile(".*dur=(?\\d+)"); + private static final String SERVER_TIMING_HEADER_PREFIX = "gfet4t7; dur="; private static final Metadata.Key GOOGLE_CLOUD_RESOURCE_PREFIX_KEY = Metadata.Key.of("google-cloud-resource-prefix", Metadata.ASCII_STRING_MARSHALLER); private static final Pattern GOOGLE_CLOUD_RESOURCE_PREFIX_PATTERN = Pattern.compile( ".*projects/(?\\p{ASCII}[^/]*)(/instances/(?\\p{ASCII}[^/]*))?(/databases/(?\\p{ASCII}[^/]*))?"); + private final Cache databaseNameCache = + CacheBuilder.newBuilder().maximumSize(100).build(); + private final Cache tagsCache = + CacheBuilder.newBuilder().maximumSize(1000).build(); + private final Cache attributesCache = + CacheBuilder.newBuilder().maximumSize(1000).build(); // Get the global singleton Tagger object. private static final Tagger TAGGER = Tags.getTagger(); @@ -72,57 +84,49 @@ class HeaderInterceptor implements ClientInterceptor { this.spannerRpcMetrics = spannerRpcMetrics; } - private class SpannerProperties { - String projectId; - String instanceId; - String databaseId; - - SpannerProperties(String projectId, String instanceId, String databaseId) { - this.databaseId = databaseId; - this.instanceId = instanceId; - this.projectId = projectId; - } - } - @Override public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { return new SimpleForwardingClientCall(next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - SpannerProperties spannerProperties = createProjectPropertes(headers); - TagContext tagContext = getTagContext(method.getFullMethodName(), spannerProperties); - Attributes attributes = getMetricAttributes(method.getFullMethodName(), spannerProperties); - super.start( - new SimpleForwardingClientCallListener(responseListener) { - @Override - public void onHeaders(Metadata metadata) { - processHeader(metadata, tagContext, attributes); - super.onHeaders(metadata); - } - }, - headers); + try { + DatabaseName databaseName = extractDatabaseName(headers); + String key = databaseName + method.getFullMethodName(); + TagContext tagContext = getTagContext(key, method.getFullMethodName(), databaseName); + Attributes attributes = + getMetricAttributes(key, method.getFullMethodName(), databaseName); + super.start( + new SimpleForwardingClientCallListener(responseListener) { + @Override + public void onHeaders(Metadata metadata) { + processHeader(metadata, tagContext, attributes); + super.onHeaders(metadata); + } + }, + headers); + } catch (ExecutionException executionException) { + // This should never happen, + throw SpannerExceptionFactory.asSpannerException(executionException.getCause()); + } } }; } private void processHeader(Metadata metadata, TagContext tagContext, Attributes attributes) { MeasureMap measureMap = STATS_RECORDER.newMeasureMap(); - if (metadata.get(SERVER_TIMING_HEADER_KEY) != null) { - String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY); - Matcher matcher = SERVER_TIMING_HEADER_PATTERN.matcher(serverTiming); - if (matcher.find()) { - try { - long latency = Long.parseLong(matcher.group("dur")); - measureMap.put(SPANNER_GFE_LATENCY, latency); - measureMap.put(SPANNER_GFE_HEADER_MISSING_COUNT, 0L); - measureMap.record(tagContext); - - spannerRpcMetrics.recordGfeLatency(latency, attributes); - spannerRpcMetrics.recordGfeHeaderMissingCount(0L, attributes); - } catch (NumberFormatException e) { - LOGGER.log(LEVEL, "Invalid server-timing object in header", matcher.group("dur")); - } + String serverTiming = metadata.get(SERVER_TIMING_HEADER_KEY); + if (serverTiming != null && serverTiming.startsWith(SERVER_TIMING_HEADER_PREFIX)) { + try { + long latency = Long.parseLong(serverTiming.substring(SERVER_TIMING_HEADER_PREFIX.length())); + measureMap.put(SPANNER_GFE_LATENCY, latency); + measureMap.put(SPANNER_GFE_HEADER_MISSING_COUNT, 0L); + measureMap.record(tagContext); + + spannerRpcMetrics.recordGfeLatency(latency, attributes); + spannerRpcMetrics.recordGfeHeaderMissingCount(0L, attributes); + } catch (NumberFormatException e) { + LOGGER.log(LEVEL, "Invalid server-timing object in header: {}", serverTiming); } } else { spannerRpcMetrics.recordGfeHeaderMissingCount(1L, attributes); @@ -130,45 +134,60 @@ private void processHeader(Metadata metadata, TagContext tagContext, Attributes } } - private SpannerProperties createProjectPropertes(Metadata headers) { - String projectId = "undefined-project"; - String instanceId = "undefined-database"; - String databaseId = "undefined-database"; - if (headers.get(GOOGLE_CLOUD_RESOURCE_PREFIX_KEY) != null) { - String googleResourcePrefix = headers.get(GOOGLE_CLOUD_RESOURCE_PREFIX_KEY); - Matcher matcher = GOOGLE_CLOUD_RESOURCE_PREFIX_PATTERN.matcher(googleResourcePrefix); - if (matcher.find()) { - projectId = matcher.group("project"); - if (matcher.group("instance") != null) { - instanceId = matcher.group("instance"); - } - if (matcher.group("database") != null) { - databaseId = matcher.group("database"); - } - } else { - LOGGER.log(LEVEL, "Error parsing google cloud resource header: " + googleResourcePrefix); - } + private DatabaseName extractDatabaseName(Metadata headers) throws ExecutionException { + String googleResourcePrefix = headers.get(GOOGLE_CLOUD_RESOURCE_PREFIX_KEY); + if (googleResourcePrefix != null) { + return databaseNameCache.get( + googleResourcePrefix, + () -> { + String projectId = "undefined-project"; + String instanceId = "undefined-database"; + String databaseId = "undefined-database"; + Matcher matcher = GOOGLE_CLOUD_RESOURCE_PREFIX_PATTERN.matcher(googleResourcePrefix); + if (matcher.find()) { + projectId = matcher.group("project"); + if (matcher.group("instance") != null) { + instanceId = matcher.group("instance"); + } + if (matcher.group("database") != null) { + databaseId = matcher.group("database"); + } + } else { + LOGGER.log( + LEVEL, "Error parsing google cloud resource header: " + googleResourcePrefix); + } + return DatabaseName.of(projectId, instanceId, databaseId); + }); } - return new SpannerProperties(projectId, instanceId, databaseId); + return UNDEFINED_DATABASE_NAME; } - private TagContext getTagContext(String method, SpannerProperties spannerProperties) { - return TAGGER - .currentBuilder() - .putLocal(PROJECT_ID, TagValue.create(spannerProperties.projectId)) - .putLocal(INSTANCE_ID, TagValue.create(spannerProperties.instanceId)) - .putLocal(DATABASE_ID, TagValue.create(spannerProperties.databaseId)) - .putLocal(METHOD, TagValue.create(method)) - .build(); + private TagContext getTagContext(String key, String method, DatabaseName databaseName) + throws ExecutionException { + return tagsCache.get( + key, + () -> + TAGGER + .currentBuilder() + .putLocal(PROJECT_ID, TagValue.create(databaseName.getProject())) + .putLocal(INSTANCE_ID, TagValue.create(databaseName.getInstance())) + .putLocal(DATABASE_ID, TagValue.create(databaseName.getDatabase())) + .putLocal(METHOD, TagValue.create(method)) + .build()); } - private Attributes getMetricAttributes(String method, SpannerProperties spannerProperties) { - AttributesBuilder attributesBuilder = Attributes.builder(); - attributesBuilder.put("database", spannerProperties.databaseId); - attributesBuilder.put("instance_id", spannerProperties.instanceId); - attributesBuilder.put("project_id", spannerProperties.projectId); - attributesBuilder.put("method", method); - - return attributesBuilder.build(); + private Attributes getMetricAttributes(String key, String method, DatabaseName databaseName) + throws ExecutionException { + return attributesCache.get( + key, + () -> { + AttributesBuilder attributesBuilder = Attributes.builder(); + attributesBuilder.put("database", databaseName.getDatabase()); + attributesBuilder.put("instance_id", databaseName.getInstance()); + attributesBuilder.put("project_id", databaseName.getProject()); + attributesBuilder.put("method", method); + + return attributesBuilder.build(); + }); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerMetadataProvider.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerMetadataProvider.java index 77406a5399b..0b8d76d52df 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerMetadataProvider.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/SpannerMetadataProvider.java @@ -15,17 +15,25 @@ */ package com.google.cloud.spanner.spi.v1; +import com.google.cloud.spanner.SpannerExceptionFactory; +import com.google.common.base.MoreObjects; +import com.google.common.base.Strings; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import com.google.common.collect.ImmutableMap; import io.grpc.Metadata; import io.grpc.Metadata.Key; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutionException; import java.util.regex.Matcher; import java.util.regex.Pattern; /** For internal use only. */ class SpannerMetadataProvider { + private final Cache>> extraHeadersCache = + CacheBuilder.newBuilder().maximumSize(100).build(); private final Map, String> headers; private final String resourceHeaderKey; private static final String ROUTE_TO_LEADER_HEADER_KEY = "x-goog-spanner-route-to-leader"; @@ -61,12 +69,20 @@ Metadata newMetadata(String resourceTokenTemplate, String defaultResourceToken) Map> newExtraHeaders( String resourceTokenTemplate, String defaultResourceToken) { - return ImmutableMap.>builder() - .put( - resourceHeaderKey, - Collections.singletonList( - getResourceHeaderValue(resourceTokenTemplate, defaultResourceToken))) - .build(); + try { + return extraHeadersCache.get( + MoreObjects.firstNonNull(resourceTokenTemplate, ""), + () -> + ImmutableMap.>builder() + .put( + resourceHeaderKey, + Collections.singletonList( + getResourceHeaderValue(resourceTokenTemplate, defaultResourceToken))) + .build()); + } catch (ExecutionException executionException) { + // This should never happen. + throw SpannerExceptionFactory.asSpannerException(executionException.getCause()); + } } Map> newRouteToLeaderHeader() { @@ -86,7 +102,7 @@ private Map, String> constructHeadersAsMetadata( private String getResourceHeaderValue(String resourceTokenTemplate, String defaultResourceToken) { String resourceToken = defaultResourceToken; - if (resourceTokenTemplate != null) { + if (!Strings.isNullOrEmpty(resourceTokenTemplate)) { for (Pattern pattern : RESOURCE_TOKEN_PATTERNS) { Matcher m = pattern.matcher(resourceTokenTemplate); if (m.matches()) {