File tree

6 files changed

+200
-3
lines changed

6 files changed

+200
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
import com.google.spanner.admin.database.v1.RestoreDatabaseRequest;
5050
import com.google.spanner.v1.ExecuteSqlRequest.QueryOptions;
5151
import io.grpc.CallCredentials;
52+
import io.grpc.CompressorRegistry;
53+
import io.grpc.ExperimentalApi;
5254
import io.grpc.ManagedChannelBuilder;
5355
import java.io.IOException;
5456
import java.net.MalformedURLException;
@@ -58,6 +60,7 @@
5860
import java.util.Map.Entry;
5961
import java.util.Set;
6062
import javax.annotation.Nonnull;
63+
import javax.annotation.Nullable;
6164
import org.threeten.bp.Duration;
6265

6366
/** Options for the Cloud Spanner service. */
@@ -104,6 +107,7 @@ public class SpannerOptions extends ServiceOptions<Spanner, SpannerOptions> {
104107
private final Map<DatabaseId, QueryOptions> mergedQueryOptions;
105108

106109
private final CallCredentialsProvider callCredentialsProvider;
110+
private final String compressorName;
107111

108112
/**
109113
* Interface that can be used to provide {@link CallCredentials} instead of {@link Credentials} to
@@ -174,6 +178,7 @@ private SpannerOptions(Builder builder) {
174178
this.mergedQueryOptions = ImmutableMap.copyOf(merged);
175179
}
176180
callCredentialsProvider = builder.callCredentialsProvider;
181+
compressorName = builder.compressorName;
177182
}
178183

179184
/**
@@ -238,6 +243,7 @@ public static class Builder
238243
private boolean autoThrottleAdministrativeRequests = false;
239244
private Map<DatabaseId, QueryOptions> defaultQueryOptions = new HashMap<>();
240245
private CallCredentialsProvider callCredentialsProvider;
246+
private String compressorName;
241247
private String emulatorHost = System.getenv("SPANNER_EMULATOR_HOST");
242248

243249
private Builder() {
@@ -309,6 +315,7 @@ private Builder() {
309315
this.autoThrottleAdministrativeRequests = options.autoThrottleAdministrativeRequests;
310316
this.defaultQueryOptions = options.defaultQueryOptions;
311317
this.callCredentialsProvider = options.callCredentialsProvider;
318+
this.compressorName = options.compressorName;
312319
this.channelProvider = options.channelProvider;
313320
this.channelConfigurator = options.channelConfigurator;
314321
this.interceptorProvider = options.interceptorProvider;
@@ -558,6 +565,28 @@ public Builder setCallCredentialsProvider(CallCredentialsProvider callCredential
558565
return this;
559566
}
560567

568+
/**
569+
* Sets the compression to use for all gRPC calls. The compressor must be a valid name known in
570+
* the {@link CompressorRegistry}.
571+
*
572+
* <p>Supported values are:
573+
*
574+
* <ul>
575+
* <li>gzip: Enable gzip compression
576+
* <li>identity: Disable compression
577+
* <li><code>null</code>: Use default compression
578+
* </ul>
579+
*/
580+
@ExperimentalApi("https://.com/grpc/grpc-java/issues/1704")
581+
public Builder setCompressorName(@Nullable String compressorName) {
582+
Preconditions.checkArgument(
583+
compressorName == null
584+
|| CompressorRegistry.getDefaultInstance().lookupCompressor(compressorName) != null,
585+
String.format("%s is not a known compressor", compressorName));
586+
this.compressorName = compressorName;
587+
return this;
588+
}
589+
561590
/**
562591
* Specifying this will allow the client to prefetch up to {@code prefetchChunks} {@code
563592
* PartialResultSet} chunks for each read and query. The data size of each chunk depends on the
@@ -690,6 +719,10 @@ public CallCredentialsProvider getCallCredentialsProvider() {
690719
return callCredentialsProvider;
691720
}
692721

722+
public String getCompressorName() {
723+
return compressorName;
724+
}
725+
693726
/** Returns the default query options to use for the specific database. */
694727
public QueryOptions getDefaultQueryOptions(DatabaseId databaseId) {
695728
// Use the specific query options for the database if any have been specified. These have
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.cloud.spanner.spi.v1;
17+
18+
import io.grpc.CallOptions;
19+
import io.grpc.Channel;
20+
import io.grpc.ClientCall;
21+
import io.grpc.ClientInterceptor;
22+
import io.grpc.ForwardingClientCall;
23+
import io.grpc.Metadata;
24+
import io.grpc.Metadata.Key;
25+
import io.grpc.MethodDescriptor;
26+
27+
class EncodingInterceptor implements ClientInterceptor {
28+
private static final String RESPONSE_ENCODING_KEY_NAME = "x-response-encoding";
29+
private static final Key<String> RESPONSE_ENCODING_KEY =
30+
Metadata.Key.of(RESPONSE_ENCODING_KEY_NAME, Metadata.ASCII_STRING_MARSHALLER);
31+
32+
private final String encoding;
33+
34+
EncodingInterceptor(String encoding) {
35+
this.encoding = encoding;
36+
}
37+
38+
@Override
39+
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
40+
MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
41+
return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
42+
next.newCall(method, callOptions)) {
43+
@Override
44+
public void start(Listener<RespT> responseListener, Metadata headers) {
45+
headers.put(RESPONSE_ENCODING_KEY, encoding);
46+
super.start(responseListener, headers);
47+
}
48+
};
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ private void awaitTermination() throws InterruptedException {
231231
private final String projectName;
232232
private final SpannerMetadataProvider metadataProvider;
233233
private final CallCredentialsProvider callCredentialsProvider;
234+
private final String compressorName;
234235
private final Duration waitTimeout =
235236
systemProperty(PROPERTY_TIMEOUT_SECONDS, DEFAULT_TIMEOUT_SECONDS);
236237
private final Duration idleTimeout =
@@ -284,6 +285,7 @@ public GapicSpannerRpc(final SpannerOptions options) {
284285
mergedHeaderProvider.getHeaders(),
285286
internalHeaderProviderBuilder.getResourceHeaderKey());
286287
this.callCredentialsProvider = options.getCallCredentialsProvider();
288+
this.compressorName = options.getCompressorName();
287289

288290
// Create a managed executor provider.
289291
this.executorProvider =
@@ -312,9 +314,11 @@ public GapicSpannerRpc(final SpannerOptions options) {
312314
// Then check if SpannerOptions provides an InterceptorProvider. Create a default
313315
// SpannerInterceptorProvider if none is provided
314316
.setInterceptorProvider(
315-
MoreObjects.firstNonNull(
316-
options.getInterceptorProvider(),
317-
SpannerInterceptorProvider.createDefault()))
317+
SpannerInterceptorProvider.create(
318+
MoreObjects.firstNonNull(
319+
options.getInterceptorProvider(),
320+
SpannerInterceptorProvider.createDefault()))
321+
.withEncoding(compressorName))
318322
.setHeaderProvider(mergedHeaderProvider)
319323
.build());
320324

Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ public static SpannerInterceptorProvider createDefault() {
4545
return new SpannerInterceptorProvider(defaultInterceptors);
4646
}
4747

48+
static SpannerInterceptorProvider create(GrpcInterceptorProvider provider) {
49+
return new SpannerInterceptorProvider(ImmutableList.copyOf(provider.getInterceptors()));
50+
}
51+
4852
public SpannerInterceptorProvider with(ClientInterceptor clientInterceptor) {
4953
List<ClientInterceptor> interceptors =
5054
ImmutableList.<ClientInterceptor>builder()
@@ -54,6 +58,13 @@ public SpannerInterceptorProvider with(ClientInterceptor clientInterceptor) {
5458
return new SpannerInterceptorProvider(interceptors);
5559
}
5660

61+
SpannerInterceptorProvider withEncoding(String encoding) {
62+
if (encoding != null) {
63+
return with(new EncodingInterceptor(encoding));
64+
}
65+
return this;
66+
}
67+
5768
@Override
5869
public List<ClientInterceptor> getInterceptors() {
5970
return clientInterceptors;
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,35 @@ public String getOptimizerVersion() {
505505
assertThat(options.getDefaultQueryOptions(DatabaseId.of("p", "i", "o")))
506506
.isEqualTo(QueryOptions.newBuilder().setOptimizerVersion("2").build());
507507
}
508+
509+
@Test
510+
public void testCompressorName() {
511+
assertThat(
512+
SpannerOptions.newBuilder()
513+
.setProjectId("p")
514+
.setCompressorName("gzip")
515+
.build()
516+
.getCompressorName())
517+
.isEqualTo("gzip");
518+
assertThat(
519+
SpannerOptions.newBuilder()
520+
.setProjectId("p")
521+
.setCompressorName("identity")
522+
.build()
523+
.getCompressorName())
524+
.isEqualTo("identity");
525+
assertThat(
526+
SpannerOptions.newBuilder()
527+
.setProjectId("p")
528+
.setCompressorName(null)
529+
.build()
530+
.getCompressorName())
531+
.isNull();
532+
try {
533+
SpannerOptions.newBuilder().setCompressorName("foo");
534+
fail("missing expected exception");
535+
} catch (IllegalArgumentException e) {
536+
// ignore, this is the expected exception.
537+
}
538+
}
508539
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2019 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.spanner.it;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
21+
import com.google.cloud.spanner.Database;
22+
import com.google.cloud.spanner.DatabaseClient;
23+
import com.google.cloud.spanner.IntegrationTestEnv;
24+
import com.google.cloud.spanner.ParallelIntegrationTest;
25+
import com.google.cloud.spanner.ResultSet;
26+
import com.google.cloud.spanner.Spanner;
27+
import com.google.cloud.spanner.SpannerOptions;
28+
import com.google.cloud.spanner.Statement;
29+
import org.junit.AfterClass;
30+
import org.junit.BeforeClass;
31+
import org.junit.ClassRule;
32+
import org.junit.Test;
33+
import org.junit.experimental.categories.Category;
34+
import org.junit.runner.RunWith;
35+
import org.junit.runners.JUnit4;
36+
37+
@Category(ParallelIntegrationTest.class)
38+
@RunWith(JUnit4.class)
39+
public class ITSpannerOptionsTest {
40+
@ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv();
41+
private static Database db;
42+
43+
@BeforeClass
44+
public static void setUp() throws Exception {
45+
db = env.getTestHelper().createTestDatabase();
46+
}
47+
48+
@AfterClass
49+
public static void tearDown() throws Exception {
50+
db.drop();
51+
}
52+
53+
@Test
54+
public void testCompression() {
55+
for (String compressorName : new String[] {"gzip", "identity", null}) {
56+
SpannerOptions options =
57+
env.getTestHelper().getOptions().toBuilder().setCompressorName(compressorName).build();
58+
try (Spanner spanner = options.getService()) {
59+
DatabaseClient client = spanner.getDatabaseClient(db.getId());
60+
try (ResultSet rs = client.singleUse().executeQuery(Statement.of("SELECT 1 AS COL1"))) {
61+
assertThat(rs.next()).isTrue();
62+
assertThat(rs.getLong(0)).isEqualTo(1L);
63+
assertThat(rs.next()).isFalse();
64+
}
65+
}
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)