diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index 199186af3a..0ccd50bd01 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -38,6 +38,30 @@ + + org.apache.maven.plugins + maven-surefire-plugin + + sponge_log + + + + default-test + + com.google.cloud.spanner.TracerTest,com.google.cloud.spanner.IntegrationTest + + + + tracer + + test + + + com.google.cloud.spanner.TracerTest + + + + @@ -45,10 +69,6 @@ org.apache.maven.plugins maven-surefire-plugin 3.0.0-M4 - - com.google.cloud.spanner.IntegrationTest - sponge_log - org.apache.maven.plugins @@ -60,7 +80,7 @@ projects/gcloud-devel/instances/spanner-testing com.google.cloud.spanner.IntegrationTest - com.google.cloud.spanner.FlakyTest + com.google.cloud.spanner.FlakyTest,com.google.cloud.spanner.TracerTest 2400 @@ -68,7 +88,7 @@ org.apache.maven.plugins maven-dependency-plugin - io.grpc:grpc-protobuf-lite,org.hamcrest:hamcrest,org.hamcrest:hamcrest-core + io.grpc:grpc-protobuf-lite,org.hamcrest:hamcrest,org.hamcrest:hamcrest-core,com.google.errorprone:error_prone_annotations diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java index 42fe9b7cb7..7b248bfb9d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractResultSet.java @@ -922,6 +922,7 @@ public void close(@Nullable String message) { if (stream != null) { stream.close(message); span.end(TraceUtil.END_SPAN_OPTIONS); + stream = null; } } @@ -997,11 +998,11 @@ protected PartialResultSet computeNext() { continue; } span.addAnnotation("Stream broken. Not safe to retry"); - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; } catch (RuntimeException e) { span.addAnnotation("Stream broken. Not safe to retry"); - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 99270a67c6..39112f33c5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -66,7 +66,7 @@ public Timestamp apply(Session session) { } }); } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; } finally { span.end(TraceUtil.END_SPAN_OPTIONS); @@ -86,7 +86,7 @@ public Timestamp apply(Session session) { } }); } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; } finally { span.end(TraceUtil.END_SPAN_OPTIONS); @@ -165,8 +165,10 @@ public TransactionRunner readWriteTransaction() { try (Scope s = tracer.withSpan(span)) { return getReadWriteSession().readWriteTransaction(); } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; + } finally { + span.end(TraceUtil.END_SPAN_OPTIONS); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java index fc42d29386..7cacaef497 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionClient.java @@ -135,7 +135,7 @@ public void run() { try { sessions = internalBatchCreateSessions(remainingSessionsToCreate, channelHint); } catch (Throwable t) { - TraceUtil.endSpanWithFailure(SpannerImpl.tracer.getCurrentSpan(), t); + TraceUtil.setWithFailure(SpannerImpl.tracer.getCurrentSpan(), t); consumer.onSessionCreateFailure(t, remainingSessionsToCreate); break; } @@ -207,11 +207,12 @@ SessionImpl createSession() { spanner .getRpc() .createSession(db.getName(), spanner.getOptions().getSessionLabels(), options); - span.end(TraceUtil.END_SPAN_OPTIONS); return new SessionImpl(spanner, session.getName(), options); } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; + } finally { + span.end(TraceUtil.END_SPAN_OPTIONS); } } 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 ed11aaf548..50da01f31f 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 @@ -140,14 +140,15 @@ public Timestamp writeAtLeastOnce(Iterable mutations) throws SpannerEx try (Scope s = tracer.withSpan(span)) { CommitResponse response = spanner.getRpc().commit(request, options); Timestamp t = Timestamp.fromProto(response.getCommitTimestamp()); - span.end(TraceUtil.END_SPAN_OPTIONS); return t; } catch (IllegalArgumentException e) { - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw newSpannerException(ErrorCode.INTERNAL, "Could not parse commit timestamp", e); } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; + } finally { + span.end(TraceUtil.END_SPAN_OPTIONS); } } @@ -208,10 +209,11 @@ public void close() { Span span = tracer.spanBuilder(SpannerImpl.DELETE_SESSION).startSpan(); try (Scope s = tracer.withSpan(span)) { spanner.getRpc().deleteSession(name, options); - span.end(TraceUtil.END_SPAN_OPTIONS); } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; + } finally { + span.end(TraceUtil.END_SPAN_OPTIONS); } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java index 1d5b2cddb0..bbcab8cfe1 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java @@ -895,7 +895,7 @@ private PooledSession take() throws SpannerException { return s.session; } } catch (Exception e) { - TraceUtil.endSpanWithFailure(tracer.getCurrentSpan(), e); + TraceUtil.setWithFailure(span, e); throw e; } finally { span.end(TraceUtil.END_SPAN_OPTIONS); diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java index 371cfc4f83..c5488ac55d 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceUtil.java @@ -54,6 +54,16 @@ static ImmutableMap getExceptionAnnotations(SpannerExcep "Status", AttributeValue.stringAttributeValue(e.getErrorCode().toString())); } + static void setWithFailure(Span span, Throwable e) { + if (e instanceof SpannerException) { + span.setStatus( + StatusConverter.fromGrpcStatus(((SpannerException) e).getErrorCode().getGrpcStatus()) + .withDescription(e.getMessage())); + } else { + span.setStatus(Status.INTERNAL.withDescription(e.getMessage())); + } + } + static void endSpanWithFailure(Span span, Throwable e) { if (e instanceof SpannerException) { endSpanWithFailure(span, (SpannerException) e); 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 aaf4267df4..9c2772da4f 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 @@ -294,7 +294,7 @@ public T run(TransactionCallable callable) { } return runInternal(callable); } catch (RuntimeException e) { - TraceUtil.endSpanWithFailure(span, e); + TraceUtil.setWithFailure(span, e); throw e; } finally { // Remove threadLocal rather than set to FALSE to avoid a possible memory leak. diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java new file mode 100644 index 0000000000..4877c60837 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/FailOnOverkillTraceComponentImpl.java @@ -0,0 +1,236 @@ +/* + * Copyright 2020 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 + * + * 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 com.google.cloud.spanner; + +import com.google.errorprone.annotations.concurrent.GuardedBy; +import io.opencensus.common.Clock; +import io.opencensus.internal.ZeroTimeClock; +import io.opencensus.trace.Annotation; +import io.opencensus.trace.AttributeValue; +import io.opencensus.trace.EndSpanOptions; +import io.opencensus.trace.Link; +import io.opencensus.trace.Sampler; +import io.opencensus.trace.Span; +import io.opencensus.trace.Span.Options; +import io.opencensus.trace.SpanBuilder; +import io.opencensus.trace.SpanContext; +import io.opencensus.trace.SpanId; +import io.opencensus.trace.TraceComponent; +import io.opencensus.trace.TraceId; +import io.opencensus.trace.TraceOptions; +import io.opencensus.trace.Tracer; +import io.opencensus.trace.Tracestate; +import io.opencensus.trace.config.TraceConfig; +import io.opencensus.trace.config.TraceParams; +import io.opencensus.trace.export.ExportComponent; +import io.opencensus.trace.export.RunningSpanStore; +import io.opencensus.trace.export.SampledSpanStore; +import io.opencensus.trace.export.SpanExporter; +import io.opencensus.trace.propagation.BinaryFormat; +import io.opencensus.trace.propagation.PropagationComponent; +import io.opencensus.trace.propagation.TextFormat; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +/** + * Simple {@link TraceComponent} implementation that will throw an exception if a {@link Span} is + * ended more than once. + */ +public class FailOnOverkillTraceComponentImpl extends TraceComponent { + private static final Random RANDOM = new Random(); + private final Tracer tracer = new TestTracer(); + private final PropagationComponent propagationComponent = new TestPropagationComponent(); + private final Clock clock = ZeroTimeClock.getInstance(); + private final ExportComponent exportComponent = new TestExportComponent(); + private final TraceConfig traceConfig = new TestTraceConfig(); + private static final Map spans = new LinkedHashMap<>(); + + public static class TestSpan extends Span { + @GuardedBy("this") + private volatile boolean ended = false; + + private String spanName; + + private TestSpan(String spanName, SpanContext context, EnumSet options) { + super(context, options); + this.spanName = spanName; + spans.put(this.spanName, false); + } + + @Override + public void addAnnotation(String description, Map attributes) {} + + @Override + public void addAnnotation(Annotation annotation) {} + + @Override + public void addLink(Link link) {} + + @Override + public void end(EndSpanOptions options) { + synchronized (this) { + if (ended) { + throw new IllegalStateException(this.spanName + " already ended"); + } + spans.put(this.spanName, true); + ended = true; + } + } + } + + public static class TestSpanBuilder extends SpanBuilder { + private String spanName; + + TestSpanBuilder(String spanName) { + this.spanName = spanName; + } + + @Override + public SpanBuilder setSampler(Sampler sampler) { + return this; + } + + @Override + public SpanBuilder setParentLinks(List parentLinks) { + return this; + } + + @Override + public SpanBuilder setRecordEvents(boolean recordEvents) { + return this; + } + + @Override + public Span startSpan() { + return new TestSpan( + this.spanName, + SpanContext.create( + TraceId.generateRandomId(RANDOM), + SpanId.generateRandomId(RANDOM), + TraceOptions.builder().setIsSampled(true).build(), + Tracestate.builder().build()), + EnumSet.of(Options.RECORD_EVENTS)); + } + } + + public static class TestTracer extends Tracer { + @Override + public SpanBuilder spanBuilderWithExplicitParent(String spanName, Span parent) { + return new TestSpanBuilder(spanName); + } + + @Override + public SpanBuilder spanBuilderWithRemoteParent( + String spanName, SpanContext remoteParentSpanContext) { + return new TestSpanBuilder(spanName); + } + } + + public static class TestPropagationComponent extends PropagationComponent { + @Override + public BinaryFormat getBinaryFormat() { + return null; + } + + @Override + public TextFormat getB3Format() { + return null; + } + + @Override + public TextFormat getTraceContextFormat() { + return null; + } + } + + public static class TestSpanExporter extends SpanExporter { + @Override + public void registerHandler(String name, Handler handler) {} + + @Override + public void unregisterHandler(String name) {} + } + + public static class TestExportComponent extends ExportComponent { + private final SpanExporter spanExporter = new TestSpanExporter(); + + @Override + public SpanExporter getSpanExporter() { + return spanExporter; + } + + @Override + public RunningSpanStore getRunningSpanStore() { + return null; + } + + @Override + public SampledSpanStore getSampledSpanStore() { + return null; + } + } + + public static class TestTraceConfig extends TraceConfig { + private volatile TraceParams activeTraceParams = TraceParams.DEFAULT; + + @Override + public TraceParams getActiveTraceParams() { + return activeTraceParams; + } + + @Override + public void updateActiveTraceParams(TraceParams traceParams) { + this.activeTraceParams = traceParams; + } + } + + @Override + public Tracer getTracer() { + return tracer; + } + + Map getSpans() { + return spans; + } + + void clearSpans() { + spans.clear(); + } + + @Override + public PropagationComponent getPropagationComponent() { + return propagationComponent; + } + + @Override + public Clock getClock() { + return clock; + } + + @Override + public ExportComponent getExportComponent() { + return exportComponent; + } + + @Override + public TraceConfig getTraceConfig() { + return traceConfig; + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java new file mode 100644 index 0000000000..cee08ca1d0 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpanTest.java @@ -0,0 +1,331 @@ +/* + * Copyright 2020 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 + * + * 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 com.google.cloud.spanner; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import com.google.api.core.ApiFunction; +import com.google.api.gax.grpc.testing.LocalChannelProvider; +import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.UnaryCallSettings.Builder; +import com.google.cloud.NoCredentials; +import com.google.cloud.spanner.MockSpannerServiceImpl.SimulatedExecutionTime; +import com.google.cloud.spanner.MockSpannerServiceImpl.StatementResult; +import com.google.cloud.spanner.TransactionRunner.TransactionCallable; +import com.google.protobuf.ListValue; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.StructType; +import com.google.spanner.v1.StructType.Field; +import com.google.spanner.v1.TypeCode; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.inprocess.InProcessServerBuilder; +import io.opencensus.trace.Tracing; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.threeten.bp.Duration; + +@RunWith(JUnit4.class) +@Category(TracerTest.class) +public class SpanTest { + private static final String TEST_PROJECT = "my-project"; + private static final String TEST_INSTANCE = "my-instance"; + private static final String TEST_DATABASE = "my-database"; + private static MockSpannerServiceImpl mockSpanner; + private static Server server; + private static LocalChannelProvider channelProvider; + private static final Statement UPDATE_STATEMENT = + Statement.of("UPDATE FOO SET BAR=1 WHERE BAZ=2"); + private static final Statement INVALID_UPDATE_STATEMENT = + Statement.of("UPDATE NON_EXISTENT_TABLE SET BAR=1 WHERE BAZ=2"); + private static final long UPDATE_COUNT = 1L; + private static final Statement SELECT1 = Statement.of("SELECT 1 AS COL1"); + private static final ResultSetMetadata SELECT1_METADATA = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("COL1") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.INT64) + .build()) + .build()) + .build()) + .build(); + private static final com.google.spanner.v1.ResultSet SELECT1_RESULTSET = + com.google.spanner.v1.ResultSet.newBuilder() + .addRows( + ListValue.newBuilder() + .addValues(com.google.protobuf.Value.newBuilder().setStringValue("1").build()) + .build()) + .setMetadata(SELECT1_METADATA) + .build(); + private Spanner spanner; + private DatabaseClient client; + private Spanner spannerWithTimeout; + private DatabaseClient clientWithTimeout; + private static FailOnOverkillTraceComponentImpl failOnOverkillTraceComponent = + new FailOnOverkillTraceComponentImpl(); + + private static final SimulatedExecutionTime ONE_SECOND = + SimulatedExecutionTime.ofMinimumAndRandomTime(1000, 0); + private static final Statement SELECT1AND2 = + Statement.of("SELECT 1 AS COL1 UNION ALL SELECT 2 AS COL1"); + private static final ResultSetMetadata SELECT1AND2_METADATA = + ResultSetMetadata.newBuilder() + .setRowType( + StructType.newBuilder() + .addFields( + Field.newBuilder() + .setName("COL1") + .setType( + com.google.spanner.v1.Type.newBuilder() + .setCode(TypeCode.INT64) + .build()) + .build()) + .build()) + .build(); + private static final StatusRuntimeException FAILED_PRECONDITION = + io.grpc.Status.FAILED_PRECONDITION + .withDescription("Non-retryable test exception.") + .asRuntimeException(); + + @Rule public ExpectedException expectedException = ExpectedException.none(); + + @BeforeClass + public static void startStaticServer() throws Exception { + mockSpanner = new MockSpannerServiceImpl(); + mockSpanner.setAbortProbability(0.0D); // We don't want any unpredictable aborted transactions. + mockSpanner.putStatementResult(StatementResult.update(UPDATE_STATEMENT, UPDATE_COUNT)); + mockSpanner.putStatementResult(StatementResult.query(SELECT1, SELECT1_RESULTSET)); + mockSpanner.putStatementResult( + StatementResult.exception( + INVALID_UPDATE_STATEMENT, + Status.INVALID_ARGUMENT.withDescription("invalid statement").asRuntimeException())); + + String uniqueName = InProcessServerBuilder.generateName(); + server = + InProcessServerBuilder.forName(uniqueName) + // We need to use a real executor for timeouts to occur. + .scheduledExecutorService(new ScheduledThreadPoolExecutor(1)) + .addService(mockSpanner) + .build() + .start(); + channelProvider = LocalChannelProvider.create(uniqueName); + + // Use a little bit reflection to set the test tracer. + java.lang.reflect.Field field = Tracing.class.getDeclaredField("traceComponent"); + field.setAccessible(true); + java.lang.reflect.Field modifiersField = + java.lang.reflect.Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + // Remove the final modifier from the 'traceComponent' field. + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + field.set(null, failOnOverkillTraceComponent); + } + + @AfterClass + public static void stopServer() throws InterruptedException { + server.shutdown(); + server.awaitTermination(); + } + + @Before + public void setUp() throws Exception { + SpannerOptions.Builder builder = + SpannerOptions.newBuilder() + .setProjectId(TEST_PROJECT) + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption(SessionPoolOptions.newBuilder().setMinSessions(0).build()); + + spanner = builder.build().getService(); + + client = spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + final RetrySettings retrySettings = + RetrySettings.newBuilder() + .setInitialRetryDelay(Duration.ofMillis(1L)) + .setMaxRetryDelay(Duration.ofMillis(1L)) + .setInitialRpcTimeout(Duration.ofMillis(75L)) + .setMaxRpcTimeout(Duration.ofMillis(75L)) + .setMaxAttempts(3) + .setTotalTimeout(Duration.ofMillis(200L)) + .build(); + RetrySettings commitRetrySettings = + RetrySettings.newBuilder() + .setInitialRetryDelay(Duration.ofMillis(1L)) + .setMaxRetryDelay(Duration.ofMillis(1L)) + .setInitialRpcTimeout(Duration.ofMillis(5000L)) + .setMaxRpcTimeout(Duration.ofMillis(10000L)) + .setMaxAttempts(1) + .setTotalTimeout(Duration.ofMillis(20000L)) + .build(); + builder + .getSpannerStubSettingsBuilder() + .applyToAllUnaryMethods( + new ApiFunction, Void>() { + @Override + public Void apply(Builder input) { + input.setRetrySettings(retrySettings); + return null; + } + }); + builder + .getSpannerStubSettingsBuilder() + .executeStreamingSqlSettings() + .setRetrySettings(retrySettings); + builder.getSpannerStubSettingsBuilder().commitSettings().setRetrySettings(commitRetrySettings); + builder + .getSpannerStubSettingsBuilder() + .executeStreamingSqlSettings() + .setRetrySettings(retrySettings); + builder.getSpannerStubSettingsBuilder().streamingReadSettings().setRetrySettings(retrySettings); + spannerWithTimeout = builder.build().getService(); + clientWithTimeout = + spannerWithTimeout.getDatabaseClient( + DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + failOnOverkillTraceComponent.clearSpans(); + } + + @After + public void tearDown() { + spanner.close(); + mockSpanner.reset(); + mockSpanner.removeAllExecutionTimes(); + } + + @Test + public void singleUseNonRetryableErrorOnNext() { + expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.FAILED_PRECONDITION)); + try (ResultSet rs = client.singleUse().executeQuery(SELECT1AND2)) { + mockSpanner.addException(FAILED_PRECONDITION); + while (rs.next()) { + // Just consume the result set. + } + } + } + + @Test + public void singleUseExecuteStreamingSqlTimeout() { + expectedException.expect(SpannerMatchers.isSpannerException(ErrorCode.DEADLINE_EXCEEDED)); + try (ResultSet rs = clientWithTimeout.singleUse().executeQuery(SELECT1AND2)) { + mockSpanner.setExecuteStreamingSqlExecutionTime(ONE_SECOND); + while (rs.next()) { + // Just consume the result set. + } + } + } + + @Test + public void singleUse() { + try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) { + while (rs.next()) { + // Just consume the result set. + } + } + Map spans = failOnOverkillTraceComponent.getSpans(); + assertThat(spans.size()).isEqualTo(5); + assertThat(spans).containsEntry("CloudSpanner.ReadOnlyTransaction", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); + assertThat(spans).containsEntry("SessionPool.WaitForSession", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); + assertThat(spans).containsEntry("CloudSpannerOperation.ExecuteStreamingQuery", true); + } + + @Test + public void multiUse() { + try (ReadOnlyTransaction tx = client.readOnlyTransaction()) { + try (ResultSet rs = tx.executeQuery(SELECT1)) { + while (rs.next()) { + // Just consume the result set. + } + } + } + + Map spans = failOnOverkillTraceComponent.getSpans(); + assertThat(spans.size()).isEqualTo(5); + assertThat(spans).containsEntry("CloudSpanner.ReadOnlyTransaction", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); + assertThat(spans).containsEntry("SessionPool.WaitForSession", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); + assertThat(spans).containsEntry("CloudSpannerOperation.ExecuteStreamingQuery", true); + } + + @Test + public void transactionRunner() { + TransactionRunner runner = client.readWriteTransaction(); + runner.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.executeUpdate(UPDATE_STATEMENT); + return null; + } + }); + Map spans = failOnOverkillTraceComponent.getSpans(); + assertThat(spans.size()).isEqualTo(6); + assertThat(spans).containsEntry("CloudSpanner.ReadWriteTransaction", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); + assertThat(spans).containsEntry("SessionPool.WaitForSession", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BeginTransaction", true); + assertThat(spans).containsEntry("CloudSpannerOperation.Commit", true); + } + + @Test + public void transactionRunnerWithError() { + TransactionRunner runner = client.readWriteTransaction(); + try { + runner.run( + new TransactionCallable() { + @Override + public Void run(TransactionContext transaction) throws Exception { + transaction.executeUpdate(INVALID_UPDATE_STATEMENT); + return null; + } + }); + fail("missing expected exception"); + } catch (SpannerException e) { + assertThat(e.getErrorCode()).isEqualTo(ErrorCode.INVALID_ARGUMENT); + } + + Map spans = failOnOverkillTraceComponent.getSpans(); + assertThat(spans.size()).isEqualTo(5); + assertThat(spans).containsEntry("CloudSpanner.ReadWriteTransaction", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessions", true); + assertThat(spans).containsEntry("SessionPool.WaitForSession", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BatchCreateSessionsRequest", true); + assertThat(spans).containsEntry("CloudSpannerOperation.BeginTransaction", true); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TracerTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TracerTest.java new file mode 100644 index 0000000000..cad3dd19fd --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TracerTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2020 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 + * + * 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 com.google.cloud.spanner; + +import org.junit.experimental.categories.Category; + +/** + * Tests marked with this {@link Category} will be executed in a separate execution with the + * maven-surefire plugin. The tests will be excluded from execution with the maven-failsafe plugin. + * + *

Separate execution prevents the injection of any custom tracing configuration from interfering + * with other tests, as most tracing configuration is stored in static final variables. + */ +public interface TracerTest {}