diff --git a/.github/release-please.yml b/.github/release-please.yml index 4eb99a985a0..efc1cdc4b60 100644 --- a/.github/release-please.yml +++ b/.github/release-please.yml @@ -46,3 +46,7 @@ branches: bumpMinorPreMajor: true handleGHRelease: true branch: 6.88.x + - releaseType: java-backport + bumpMinorPreMajor: true + handleGHRelease: true + branch: 6.96.x diff --git a/.github/sync-repo-settings.yaml b/.github/sync-repo-settings.yaml index 4510689141c..3fe63bc890d 100644 --- a/.github/sync-repo-settings.yaml +++ b/.github/sync-repo-settings.yaml @@ -182,6 +182,27 @@ branchProtectionRules: - units-with-multiplexed-session (11) - unmanaged_dependency_check - library_generation + - pattern: 6.96.x + isAdminEnforced: true + requiredApprovingReviewCount: 1 + requiresCodeOwnerReviews: true + requiresStrictStatusChecks: false + requiredStatusCheckContexts: + - dependencies (17) + - lint + - javadoc + - units (8) + - units (11) + - 'Kokoro - Test: Integration' + - 'Kokoro - Test: Integration with Multiplexed Sessions' + - cla/google + - checkstyle + - compile (8) + - compile (11) + - units-with-multiplexed-session (8) + - units-with-multiplexed-session (11) + - unmanaged_dependency_check + - library_generation permissionRules: - team: yoshi-admins permission: admin diff --git a/CHANGELOG.md b/CHANGELOG.md index 849a6a8b339..f803387145b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [6.97.0](https://github.com/googleapis/java-spanner/compare/v6.96.1...v6.97.0) (2025-07-10) + + +### Features + +* Next release from main branch is 6.97.0 ([#3984](https://github.com/googleapis/java-spanner/issues/3984)) ([5651f61](https://github.com/googleapis/java-spanner/commit/5651f6160e1e655f118aa2e7f0203a47cd6914c0)) + + +### Bug Fixes + +* Drop max message size ([#3987](https://github.com/googleapis/java-spanner/issues/3987)) ([3eee899](https://github.com/googleapis/java-spanner/commit/3eee89965547dfa49b4282b470f625d43c92f4fd)) +* Return non-empty metadata for DataBoost queries ([#3936](https://github.com/googleapis/java-spanner/issues/3936)) ([79c0684](https://github.com/googleapis/java-spanner/commit/79c06848c0ac4eff8410dd3bd63db8675c202d94)) + ## [6.96.1](https://github.com/googleapis/java-spanner/compare/v6.96.0...v6.96.1) (2025-06-30) diff --git a/README.md b/README.md index 47fe59a6bdf..971946c8fee 100644 --- a/README.md +++ b/README.md @@ -56,13 +56,13 @@ implementation 'com.google.cloud:google-cloud-spanner' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-spanner:6.96.1' +implementation 'com.google.cloud:google-cloud-spanner:6.97.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.96.1" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.97.0" ``` ## Authentication @@ -731,7 +731,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.96.1 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.97.0 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml index 640cb5b50c3..2d2a1c73f33 100644 --- a/benchmarks/pom.xml +++ b/benchmarks/pom.xml @@ -24,7 +24,7 @@ com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/google-cloud-spanner-bom/pom.xml b/google-cloud-spanner-bom/pom.xml index 850809800ce..ef6c625f894 100644 --- a/google-cloud-spanner-bom/pom.xml +++ b/google-cloud-spanner-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner-bom - 6.96.1 + 6.97.0 pom com.google.cloud @@ -53,43 +53,43 @@ com.google.cloud google-cloud-spanner - 6.96.1 + 6.97.0 com.google.cloud google-cloud-spanner test-jar - 6.96.1 + 6.97.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.96.1 + 6.97.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.96.1 + 6.97.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.96.1 + 6.97.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.96.1 + 6.97.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.96.1 + 6.97.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.96.1 + 6.97.0 diff --git a/google-cloud-spanner-executor/pom.xml b/google-cloud-spanner-executor/pom.xml index 7e6607bbb06..f87e817da49 100644 --- a/google-cloud-spanner-executor/pom.xml +++ b/google-cloud-spanner-executor/pom.xml @@ -5,14 +5,14 @@ 4.0.0 com.google.cloud google-cloud-spanner-executor - 6.96.1 + 6.97.0 jar Google Cloud Spanner Executor com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/google-cloud-spanner/pom.xml b/google-cloud-spanner/pom.xml index a066eeb5758..1d038006cd3 100644 --- a/google-cloud-spanner/pom.xml +++ b/google-cloud-spanner/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.cloud google-cloud-spanner - 6.96.1 + 6.97.0 jar Google Cloud Spanner https://github.com/googleapis/java-spanner @@ -11,7 +11,7 @@ com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 google-cloud-spanner diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporter.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporter.java index 53544672165..b43cf43b250 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporter.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporter.java @@ -141,7 +141,8 @@ private CompletableResultCode exportSpannerClientMetrics(Collection List spannerTimeSeries; try { spannerTimeSeries = - SpannerCloudMonitoringExporterUtils.convertToSpannerTimeSeries(spannerMetricData); + SpannerCloudMonitoringExporterUtils.convertToSpannerTimeSeries( + spannerMetricData, this.spannerProjectId); } catch (Throwable e) { logger.log( Level.WARNING, diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterUtils.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterUtils.java index f67621db963..ba53fa02a6b 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterUtils.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterUtils.java @@ -36,17 +36,24 @@ import com.google.api.MetricDescriptor.MetricKind; import com.google.api.MetricDescriptor.ValueType; import com.google.api.MonitoredResource; +import com.google.monitoring.v3.DroppedLabels; import com.google.monitoring.v3.Point; +import com.google.monitoring.v3.SpanContext; import com.google.monitoring.v3.TimeInterval; import com.google.monitoring.v3.TimeSeries; import com.google.monitoring.v3.TypedValue; +import com.google.protobuf.Any; +import com.google.protobuf.Timestamp; import com.google.protobuf.util.Timestamps; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleExemplarData; import io.opentelemetry.sdk.metrics.data.DoublePointData; +import io.opentelemetry.sdk.metrics.data.ExemplarData; import io.opentelemetry.sdk.metrics.data.HistogramData; import io.opentelemetry.sdk.metrics.data.HistogramPointData; +import io.opentelemetry.sdk.metrics.data.LongExemplarData; import io.opentelemetry.sdk.metrics.data.LongPointData; import io.opentelemetry.sdk.metrics.data.MetricData; import io.opentelemetry.sdk.metrics.data.MetricDataType; @@ -57,6 +64,7 @@ import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; class SpannerCloudMonitoringExporterUtils { @@ -69,7 +77,8 @@ static String getProjectId(Resource resource) { return resource.getAttributes().get(PROJECT_ID_KEY); } - static List convertToSpannerTimeSeries(List collection) { + static List convertToSpannerTimeSeries( + List collection, String projectId) { List allTimeSeries = new ArrayList<>(); for (MetricData metricData : collection) { @@ -94,7 +103,8 @@ static List convertToSpannerTimeSeries(List collection) metricData.getData().getPoints().stream() .map( pointData -> - convertPointToSpannerTimeSeries(metricData, pointData, monitoredResourceBuilder)) + convertPointToSpannerTimeSeries( + metricData, pointData, monitoredResourceBuilder, projectId)) .forEach(allTimeSeries::add); } return allTimeSeries; @@ -103,7 +113,8 @@ static List convertToSpannerTimeSeries(List collection) private static TimeSeries convertPointToSpannerTimeSeries( MetricData metricData, PointData pointData, - MonitoredResource.Builder monitoredResourceBuilder) { + MonitoredResource.Builder monitoredResourceBuilder, + String projectId) { TimeSeries.Builder builder = TimeSeries.newBuilder() .setMetricKind(convertMetricKind(metricData)) @@ -135,7 +146,7 @@ private static TimeSeries convertPointToSpannerTimeSeries( .setEndTime(Timestamps.fromNanos(pointData.getEpochNanos())) .build(); - builder.addPoints(createPoint(metricData.getType(), pointData, timeInterval)); + builder.addPoints(createPoint(metricData.getType(), pointData, timeInterval, projectId)); return builder.build(); } @@ -191,7 +202,7 @@ private static ValueType convertValueType(MetricDataType metricDataType) { } private static Point createPoint( - MetricDataType type, PointData pointData, TimeInterval timeInterval) { + MetricDataType type, PointData pointData, TimeInterval timeInterval, String projectId) { Point.Builder builder = Point.newBuilder().setInterval(timeInterval); switch (type) { case HISTOGRAM: @@ -199,7 +210,8 @@ private static Point createPoint( return builder .setValue( TypedValue.newBuilder() - .setDistributionValue(convertHistogramData((HistogramPointData) pointData)) + .setDistributionValue( + convertHistogramData((HistogramPointData) pointData, projectId)) .build()) .build(); case DOUBLE_GAUGE: @@ -221,7 +233,7 @@ private static Point createPoint( } } - private static Distribution convertHistogramData(HistogramPointData pointData) { + private static Distribution convertHistogramData(HistogramPointData pointData, String projectId) { return Distribution.newBuilder() .setCount(pointData.getCount()) .setMean(pointData.getCount() == 0L ? 0.0D : pointData.getSum() / pointData.getCount()) @@ -229,6 +241,65 @@ private static Distribution convertHistogramData(HistogramPointData pointData) { BucketOptions.newBuilder() .setExplicitBuckets(Explicit.newBuilder().addAllBounds(pointData.getBoundaries()))) .addAllBucketCounts(pointData.getCounts()) + .addAllExemplars( + pointData.getExemplars().stream() + .map(e -> mapExemplar(e, projectId)) + .collect(Collectors.toList())) .build(); } + + private static Distribution.Exemplar mapExemplar(ExemplarData exemplar, String projectId) { + double value = 0; + if (exemplar instanceof DoubleExemplarData) { + value = ((DoubleExemplarData) exemplar).getValue(); + } else if (exemplar instanceof LongExemplarData) { + value = ((LongExemplarData) exemplar).getValue(); + } + + Distribution.Exemplar.Builder exemplarBuilder = + Distribution.Exemplar.newBuilder() + .setValue(value) + .setTimestamp(mapTimestamp(exemplar.getEpochNanos())); + if (exemplar.getSpanContext().isValid()) { + exemplarBuilder.addAttachments( + Any.pack( + SpanContext.newBuilder() + .setSpanName( + makeSpanName( + projectId, + exemplar.getSpanContext().getTraceId(), + exemplar.getSpanContext().getSpanId())) + .build())); + } + if (!exemplar.getFilteredAttributes().isEmpty()) { + exemplarBuilder.addAttachments( + Any.pack(mapFilteredAttributes(exemplar.getFilteredAttributes()))); + } + return exemplarBuilder.build(); + } + + static final long NANO_PER_SECOND = (long) 1e9; + + private static Timestamp mapTimestamp(long epochNanos) { + return Timestamp.newBuilder() + .setSeconds(epochNanos / NANO_PER_SECOND) + .setNanos((int) (epochNanos % NANO_PER_SECOND)) + .build(); + } + + private static String makeSpanName(String projectId, String traceId, String spanId) { + return String.format("projects/%s/traces/%s/spans/%s", projectId, traceId, spanId); + } + + private static DroppedLabels mapFilteredAttributes(Attributes attributes) { + DroppedLabels.Builder labels = DroppedLabels.newBuilder(); + attributes.forEach((k, v) -> labels.putLabel(cleanAttributeKey(k.getKey()), v.toString())); + return labels.build(); + } + + private static String cleanAttributeKey(String key) { + // . is commonly used in OTel but disallowed in GCM label names, + // https://cloud.google.com/monitoring/api/ref_v3/rest/v3/LabelDescriptor#:~:text=Matches%20the%20following%20regular%20expression%3A + return key.replace('.', '_'); + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/MergedResultSet.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/MergedResultSet.java index fcbc49f346d..1cbbf0818c5 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/MergedResultSet.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/MergedResultSet.java @@ -25,6 +25,7 @@ import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; +import com.google.cloud.spanner.Type.Code; import com.google.common.base.Preconditions; import com.google.common.base.Supplier; import com.google.spanner.v1.ResultSetMetadata; @@ -82,9 +83,11 @@ public void run() { break; } } - if (first) { - // Special case: The result set did not return any rows. Push the metadata to the merged - // result set. + if (first + && resultSet.getType().getCode() == Code.STRUCT + && !resultSet.getType().getStructFields().isEmpty()) { + // Special case: The result set did not return any rows, but did return metadata. + // Push the metadata to the merged result set. queue.put( PartitionExecutorResult.typeAndMetadata( resultSet.getType(), resultSet.getMetadata())); @@ -319,13 +322,17 @@ public Struct get() { return currentRow; } - private PartitionExecutorResult getFirstResult() { + private PartitionExecutorResult getFirstResultWithMetadata() { try { metadataAvailableLatch.await(); } catch (InterruptedException interruptedException) { throw SpannerExceptionFactory.propagateInterrupt(interruptedException); } - PartitionExecutorResult result = queue.peek(); + PartitionExecutorResult result = + queue.stream() + .filter(rs -> rs.metadata != null || rs.exception != null) + .findFirst() + .orElse(null); if (result == null) { throw SpannerExceptionFactory.newSpannerException( ErrorCode.FAILED_PRECONDITION, "Thread-unsafe access to ResultSet"); @@ -338,7 +345,7 @@ private PartitionExecutorResult getFirstResult() { public ResultSetMetadata getMetadata() { if (metadata == null) { - return getFirstResult().metadata; + return getFirstResultWithMetadata().metadata; } return metadata; } @@ -355,7 +362,7 @@ public int getParallelism() { public Type getType() { if (type == null) { - return getFirstResult().type; + return getFirstResultWithMetadata().type; } return type; } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java index 5abf3ea98e7..d3017afd618 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpc.java @@ -224,7 +224,7 @@ public class GapicSpannerRpc implements SpannerRpc { PathTemplate.create("projects/{project}"); private static final PathTemplate OPERATION_NAME_TEMPLATE = PathTemplate.create("{database=projects/*/instances/*/databases/*}/operations/{operation}"); - private static final int MAX_MESSAGE_SIZE = 100 * 1024 * 1024; + private static final int MAX_MESSAGE_SIZE = 256 * 1024 * 1024; private static final int MAX_METADATA_SIZE = 32 * 1024; // bytes private static final String PROPERTY_TIMEOUT_SECONDS = "com.google.cloud.spanner.watchdogTimeoutSeconds"; diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITTransactionRetryTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITTransactionRetryTest.java new file mode 100644 index 00000000000..da48f661790 --- /dev/null +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ITTransactionRetryTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 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 static com.google.cloud.spanner.testing.EmulatorSpannerHelper.isUsingEmulator; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeFalse; + +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@Category(ParallelIntegrationTest.class) +@RunWith(JUnit4.class) +public class ITTransactionRetryTest { + + @ClassRule public static IntegrationTestEnv env = new IntegrationTestEnv(); + + @Test + public void TestRetryInfo() { + assumeFalse("emulator does not support parallel transaction", isUsingEmulator()); + + // Creating a database with the table which contains INT64 columns + Database db = + env.getTestHelper() + .createTestDatabase("CREATE TABLE Test(ID INT64, " + "EMPID INT64) PRIMARY KEY (ID)"); + DatabaseClient databaseClient = env.getTestHelper().getClient().getDatabaseClient(db.getId()); + + // Inserting one row + databaseClient + .readWriteTransaction() + .run( + transaction -> { + transaction.buffer( + Mutation.newInsertBuilder("Test").set("ID").to(1).set("EMPID").to(1).build()); + return null; + }); + + int numRetries = 10; + boolean isAbortedWithRetryInfo = false; + while (numRetries-- > 0) { + try (TransactionManager transactionManager1 = databaseClient.transactionManager()) { + try (TransactionManager transactionManager2 = databaseClient.transactionManager()) { + try { + TransactionContext transaction1 = transactionManager1.begin(); + TransactionContext transaction2 = transactionManager2.begin(); + transaction1.executeUpdate( + Statement.of("UPDATE Test SET EMPID = EMPID + 1 WHERE ID = 1")); + transaction2.executeUpdate( + Statement.of("UPDATE Test SET EMPID = EMPID + 1 WHERE ID = 1")); + transactionManager1.commit(); + transactionManager2.commit(); + } catch (AbortedException abortedException) { + assertThat(abortedException.getErrorCode()).isEqualTo(ErrorCode.ABORTED); + if (abortedException.getRetryDelayInMillis() > 0) { + isAbortedWithRetryInfo = true; + break; + } + } + } + } + } + + assertTrue("Transaction is not aborted with the trailers", isAbortedWithRetryInfo); + } +} diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterTest.java index 84a8cf4460c..8b8968bf4e1 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerCloudMonitoringExporterTest.java @@ -42,15 +42,21 @@ import com.google.cloud.monitoring.v3.stub.MetricServiceStub; import com.google.common.collect.ImmutableList; import com.google.monitoring.v3.CreateTimeSeriesRequest; +import com.google.monitoring.v3.DroppedLabels; import com.google.monitoring.v3.TimeSeries; import com.google.protobuf.Empty; import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.api.trace.TraceFlags; +import io.opentelemetry.api.trace.TraceState; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; import io.opentelemetry.sdk.metrics.InstrumentType; import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.DoubleExemplarData; import io.opentelemetry.sdk.metrics.data.HistogramPointData; import io.opentelemetry.sdk.metrics.data.LongPointData; import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.internal.data.ImmutableDoubleExemplarData; import io.opentelemetry.sdk.metrics.internal.data.ImmutableHistogramData; import io.opentelemetry.sdk.metrics.internal.data.ImmutableHistogramPointData; import io.opentelemetry.sdk.metrics.internal.data.ImmutableLongPointData; @@ -347,6 +353,86 @@ public void testExportingSumDataInBatches() { } } + @Test + public void testExportingHistogramDataWithExemplars() { + ArgumentCaptor argumentCaptor = + ArgumentCaptor.forClass(CreateTimeSeriesRequest.class); + + UnaryCallable mockCallable = mock(UnaryCallable.class); + when(mockMetricServiceStub.createServiceTimeSeriesCallable()).thenReturn(mockCallable); + ApiFuture future = ApiFutures.immediateFuture(Empty.getDefaultInstance()); + when(mockCallable.futureCall(argumentCaptor.capture())).thenReturn(future); + + long startEpoch = 10 * 1_000_000_000L; + long endEpoch = 15 * 1_000_000_000L; + long recordTimeEpoch = 12_123_456_789L; + + DoubleExemplarData exemplar = + ImmutableDoubleExemplarData.create( + Attributes.builder().put("request_id", "test").build(), + recordTimeEpoch, + SpanContext.create( + "0123456789abcdef0123456789abcdef", + "0123456789abcdef", + TraceFlags.getSampled(), + TraceState.getDefault()), + 1.5); + + HistogramPointData histogramPointData = + ImmutableHistogramPointData.create( + startEpoch, + endEpoch, + attributes, + 3d, + true, + 1d, + true, + 2d, + Collections.singletonList(1.0), + Arrays.asList(1L, 2L), + Collections.singletonList(exemplar) // ← add exemplar + ); + + MetricData histogramData = + ImmutableMetricData.createDoubleHistogram( + resource, + scope, + "spanner.googleapis.com/internal/client/" + OPERATION_LATENCIES_NAME, + "description", + "ms", + ImmutableHistogramData.create( + AggregationTemporality.CUMULATIVE, ImmutableList.of(histogramPointData))); + + exporter.export(Collections.singletonList(histogramData)); + assertFalse(exporter.lastExportSkippedData()); + + CreateTimeSeriesRequest request = argumentCaptor.getValue(); + TimeSeries timeSeries = request.getTimeSeriesList().get(0); + Distribution distribution = timeSeries.getPoints(0).getValue().getDistributionValue(); + + // Assert exemplar exists and has expected value + assertThat(distribution.getExemplarsCount()).isEqualTo(1); + Distribution.Exemplar exportedExemplar = distribution.getExemplars(0); + assertThat(exportedExemplar.getValue()).isEqualTo(1.5); + + // Assert timestamp mapping + assertThat(exportedExemplar.getTimestamp().getSeconds()) + .isEqualTo(recordTimeEpoch / 1_000_000_000L); + assertThat(exportedExemplar.getTimestamp().getNanos()) + .isEqualTo((int) (recordTimeEpoch % 1_000_000_000L)); + + // Assert attachments: SpanContext + boolean hasSpanAttachment = + exportedExemplar.getAttachmentsList().stream() + .anyMatch(any -> any.is(com.google.monitoring.v3.SpanContext.class)); + assertThat(hasSpanAttachment).isTrue(); + + // Assert attachments: DroppedLabels (filtered attributes) + boolean hasFilteredAttrs = + exportedExemplar.getAttachmentsList().stream().anyMatch(any -> any.is(DroppedLabels.class)); + assertThat(hasFilteredAttrs).isTrue(); + } + @Test public void getAggregationTemporality() throws IOException { SpannerCloudMonitoringExporter actualExporter = diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/MergedResultSetTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/MergedResultSetTest.java index b0465be6106..6d3950efbc3 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/MergedResultSetTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/MergedResultSetTest.java @@ -32,6 +32,8 @@ import com.google.cloud.spanner.SpannerExceptionFactory; import com.google.cloud.spanner.Struct; import com.google.cloud.spanner.Type; +import com.google.spanner.v1.ResultSetMetadata; +import com.google.spanner.v1.StructType; import java.util.ArrayList; import java.util.BitSet; import java.util.Collection; @@ -103,7 +105,7 @@ public static Collection parameters() { return params; } - private MockedResults setupResults(boolean withErrors) { + private MockedResults setupResults(boolean withErrors, boolean withEmptyResults) { Random random = new Random(); Connection connection = mock(Connection.class); List partitions = new ArrayList<>(); @@ -122,10 +124,22 @@ private MockedResults setupResults(boolean withErrors) { when(connection.runPartition(partition)) .thenReturn(new ResultSetWithError(ResultSetsHelper.fromProto(proto), errorIndex)); } else { - when(connection.runPartition(partition)).thenReturn(ResultSetsHelper.fromProto(proto)); - try (ResultSet resultSet = ResultSetsHelper.fromProto(proto)) { - while (resultSet.next()) { - allRows.add(resultSet.getCurrentRowAsStruct()); + if (withEmptyResults && numPartitions > 1 && index == 0) { + when(connection.runPartition(partition)) + .thenReturn( + ResultSetsHelper.fromProto( + com.google.spanner.v1.ResultSet.newBuilder() + .setMetadata( + ResultSetMetadata.newBuilder() + .setRowType(StructType.newBuilder().build()) + .build()) + .build())); + } else { + when(connection.runPartition(partition)).thenReturn(ResultSetsHelper.fromProto(proto)); + try (ResultSet resultSet = ResultSetsHelper.fromProto(proto)) { + while (resultSet.next()) { + allRows.add(resultSet.getCurrentRowAsStruct()); + } } } } @@ -135,7 +149,7 @@ private MockedResults setupResults(boolean withErrors) { @Test public void testAllResultsAreReturned() { - MockedResults results = setupResults(false); + MockedResults results = setupResults(/* withErrors= */ false, /* withEmptyResults= */ false); BitSet rowsFound = new BitSet(results.allRows.size()); try (MergedResultSet resultSet = new MergedResultSet(results.connection, results.partitions, maxParallelism)) { @@ -170,7 +184,7 @@ public void testAllResultsAreReturned() { @Test public void testResultSetStopsAfterFirstError() { - MockedResults results = setupResults(true); + MockedResults results = setupResults(/* withErrors= */ true, /* withEmptyResults= */ false); try (MergedResultSet resultSet = new MergedResultSet(results.connection, results.partitions, maxParallelism)) { if (numPartitions > 0) { @@ -194,6 +208,40 @@ public void testResultSetStopsAfterFirstError() { } } + @Test + public void testResultSetReturnsNonEmptyMetadata() { + MockedResults results = setupResults(/* withErrors= */ false, /* withEmptyResults= */ true); + BitSet rowsFound = new BitSet(results.allRows.size()); + try (MergedResultSet resultSet = + new MergedResultSet(results.connection, results.partitions, maxParallelism)) { + if (numPartitions > 0) { + assertNotNull(resultSet.getMetadata()); + assertEquals(26, resultSet.getMetadata().getRowType().getFieldsCount()); + } + while (resultSet.next()) { + assertRowExists(results.allRows, resultSet.getCurrentRowAsStruct(), rowsFound); + } + if (numPartitions == 0) { + assertEquals(0, resultSet.getColumnCount()); + } else { + assertEquals(26, resultSet.getColumnCount()); + assertEquals(Type.bool(), resultSet.getColumnType(0)); + assertEquals(Type.bool(), resultSet.getColumnType("COL0")); + assertEquals(10, resultSet.getColumnIndex("COL10")); + } + // Check that all rows were found. + assertEquals(results.allRows.size(), rowsFound.nextClearBit(0)); + // Check extended metadata. + assertEquals(numPartitions, resultSet.getNumPartitions()); + if (maxParallelism > 0) { + assertEquals(Math.min(numPartitions, maxParallelism), resultSet.getParallelism()); + } else { + int processors = Runtime.getRuntime().availableProcessors(); + assertEquals(Math.min(numPartitions, processors), resultSet.getParallelism()); + } + } + } + private void assertRowExists(List expectedRows, Struct row, BitSet rowsFound) { for (int i = 0; i < expectedRows.size(); i++) { if (row.equals(expectedRows.get(i))) { diff --git a/grpc-google-cloud-spanner-admin-database-v1/pom.xml b/grpc-google-cloud-spanner-admin-database-v1/pom.xml index 5621431c4ac..c45ca00487c 100644 --- a/grpc-google-cloud-spanner-admin-database-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.96.1 + 6.97.0 grpc-google-cloud-spanner-admin-database-v1 GRPC library for grpc-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml index 82d58977134..c6e6565fb90 100644 --- a/grpc-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/grpc-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.96.1 + 6.97.0 grpc-google-cloud-spanner-admin-instance-v1 GRPC library for grpc-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/grpc-google-cloud-spanner-executor-v1/pom.xml b/grpc-google-cloud-spanner-executor-v1/pom.xml index b09b1065837..df6d17bb928 100644 --- a/grpc-google-cloud-spanner-executor-v1/pom.xml +++ b/grpc-google-cloud-spanner-executor-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-executor-v1 - 6.96.1 + 6.97.0 grpc-google-cloud-spanner-executor-v1 GRPC library for google-cloud-spanner com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/grpc-google-cloud-spanner-v1/pom.xml b/grpc-google-cloud-spanner-v1/pom.xml index a5cf8ba9bbd..177caba6238 100644 --- a/grpc-google-cloud-spanner-v1/pom.xml +++ b/grpc-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.96.1 + 6.97.0 grpc-google-cloud-spanner-v1 GRPC library for grpc-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/pom.xml b/pom.xml index e96d599a8ab..8cba31e627e 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.google.cloud google-cloud-spanner-parent pom - 6.96.1 + 6.97.0 Google Cloud Spanner Parent https://github.com/googleapis/java-spanner @@ -61,47 +61,47 @@ com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.96.1 + 6.97.0 com.google.api.grpc proto-google-cloud-spanner-executor-v1 - 6.96.1 + 6.97.0 com.google.api.grpc grpc-google-cloud-spanner-executor-v1 - 6.96.1 + 6.97.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.96.1 + 6.97.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.96.1 + 6.97.0 com.google.api.grpc grpc-google-cloud-spanner-v1 - 6.96.1 + 6.97.0 com.google.api.grpc grpc-google-cloud-spanner-admin-instance-v1 - 6.96.1 + 6.97.0 com.google.api.grpc grpc-google-cloud-spanner-admin-database-v1 - 6.96.1 + 6.97.0 com.google.cloud google-cloud-spanner - 6.96.1 + 6.97.0 diff --git a/proto-google-cloud-spanner-admin-database-v1/pom.xml b/proto-google-cloud-spanner-admin-database-v1/pom.xml index 6d389a502d5..7bb152dd550 100644 --- a/proto-google-cloud-spanner-admin-database-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-database-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-database-v1 - 6.96.1 + 6.97.0 proto-google-cloud-spanner-admin-database-v1 PROTO library for proto-google-cloud-spanner-admin-database-v1 com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/proto-google-cloud-spanner-admin-instance-v1/pom.xml b/proto-google-cloud-spanner-admin-instance-v1/pom.xml index 5853021dd4b..444694af104 100644 --- a/proto-google-cloud-spanner-admin-instance-v1/pom.xml +++ b/proto-google-cloud-spanner-admin-instance-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-admin-instance-v1 - 6.96.1 + 6.97.0 proto-google-cloud-spanner-admin-instance-v1 PROTO library for proto-google-cloud-spanner-admin-instance-v1 com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/proto-google-cloud-spanner-executor-v1/pom.xml b/proto-google-cloud-spanner-executor-v1/pom.xml index b5b75aced4a..2d1ad601aaa 100644 --- a/proto-google-cloud-spanner-executor-v1/pom.xml +++ b/proto-google-cloud-spanner-executor-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-executor-v1 - 6.96.1 + 6.97.0 proto-google-cloud-spanner-executor-v1 Proto library for google-cloud-spanner com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/proto-google-cloud-spanner-v1/pom.xml b/proto-google-cloud-spanner-v1/pom.xml index 9bb62f5e36d..4aa2a45a658 100644 --- a/proto-google-cloud-spanner-v1/pom.xml +++ b/proto-google-cloud-spanner-v1/pom.xml @@ -4,13 +4,13 @@ 4.0.0 com.google.api.grpc proto-google-cloud-spanner-v1 - 6.96.1 + 6.97.0 proto-google-cloud-spanner-v1 PROTO library for proto-google-cloud-spanner-v1 com.google.cloud google-cloud-spanner-parent - 6.96.1 + 6.97.0 diff --git a/samples/snapshot/pom.xml b/samples/snapshot/pom.xml index fd51e511025..bf20a3f43a2 100644 --- a/samples/snapshot/pom.xml +++ b/samples/snapshot/pom.xml @@ -32,7 +32,7 @@ com.google.cloud google-cloud-spanner - 6.96.1 + 6.97.0 diff --git a/versions.txt b/versions.txt index f8fb39303f1..3823c536f74 100644 --- a/versions.txt +++ b/versions.txt @@ -1,13 +1,13 @@ # Format: # module:released-version:current-version -proto-google-cloud-spanner-admin-instance-v1:6.96.1:6.96.1 -proto-google-cloud-spanner-v1:6.96.1:6.96.1 -proto-google-cloud-spanner-admin-database-v1:6.96.1:6.96.1 -grpc-google-cloud-spanner-v1:6.96.1:6.96.1 -grpc-google-cloud-spanner-admin-instance-v1:6.96.1:6.96.1 -grpc-google-cloud-spanner-admin-database-v1:6.96.1:6.96.1 -google-cloud-spanner:6.96.1:6.96.1 -google-cloud-spanner-executor:6.96.1:6.96.1 -proto-google-cloud-spanner-executor-v1:6.96.1:6.96.1 -grpc-google-cloud-spanner-executor-v1:6.96.1:6.96.1 +proto-google-cloud-spanner-admin-instance-v1:6.97.0:6.97.0 +proto-google-cloud-spanner-v1:6.97.0:6.97.0 +proto-google-cloud-spanner-admin-database-v1:6.97.0:6.97.0 +grpc-google-cloud-spanner-v1:6.97.0:6.97.0 +grpc-google-cloud-spanner-admin-instance-v1:6.97.0:6.97.0 +grpc-google-cloud-spanner-admin-database-v1:6.97.0:6.97.0 +google-cloud-spanner:6.97.0:6.97.0 +google-cloud-spanner-executor:6.97.0:6.97.0 +proto-google-cloud-spanner-executor-v1:6.97.0:6.97.0 +grpc-google-cloud-spanner-executor-v1:6.97.0:6.97.0