Skip to content
This repository has been archived by the owner on Jan 19, 2022. It is now read-only.

Spanner added bounded staleness option and tests #1727

Merged
merged 8 commits into from
Jul 9, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/src/main/asciidoc/spanner.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,9 @@ SpannerQueryOptions spannerQueryOptions = new SpannerQueryOptions().setTimestamp
List<Trade> trades = this.spannerTemplate.query(Trade.class, Statement.of("SELECT * FROM trades"), spannerQueryOptions);
----

You can also read with https://cloud.google.com/spanner/docs/timestamp-bounds[bounded staleness] by setting `.setBoundedTimestamp(true)` on the query and read options objects.
Bounded staleness lets Cloud Spanner choose any point in time later than or equal to the given timestamp, but it cannot be used inside transactions.


===== Read from a secondary index

Expand Down Expand Up @@ -864,7 +867,7 @@ The final returned value and type of the function is determined by the user.
You can use this object just as you would a regular `SpannerOperations` with
a few exceptions:

- Its read functionality cannot perform stale reads, because all reads happen at the single point in time of the transaction.
- Its read functionality cannot perform stale reads (other than the staleness set on the entire transaction), because all reads happen at the single point in time of the transaction.
- It cannot perform sub-transactions via `performReadWriteTransaction` or `performReadOnlyTransaction`
- It cannot perform any write operations.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@
import java.util.function.Function;
import java.util.function.Supplier;

import com.google.cloud.Timestamp;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ReadContext;
import com.google.cloud.spanner.ReadOnlyTransaction;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TimestampBound;

import org.springframework.cloud.gcp.data.spanner.core.admin.SpannerSchemaUtils;
import org.springframework.cloud.gcp.data.spanner.core.convert.SpannerEntityProcessor;
Expand Down Expand Up @@ -73,7 +73,7 @@ protected ReadContext getReadContext() {
}

@Override
protected ReadContext getReadContext(Timestamp timestamp) {
protected ReadContext getReadContext(TimestampBound timestampBound) {
throw new SpannerDataException(
"Getting stale snapshot read contexts is not supported"
+ " in read-only transaction templates.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@
import java.util.function.Function;
import java.util.function.Supplier;

import com.google.cloud.Timestamp;
import com.google.cloud.spanner.DatabaseClient;
import com.google.cloud.spanner.Mutation;
import com.google.cloud.spanner.ReadContext;
import com.google.cloud.spanner.Statement;
import com.google.cloud.spanner.TimestampBound;
import com.google.cloud.spanner.TransactionContext;

import org.springframework.cloud.gcp.data.spanner.core.admin.SpannerSchemaUtils;
Expand Down Expand Up @@ -71,7 +71,7 @@ public long executeDmlStatement(Statement statement) {
}

@Override
protected ReadContext getReadContext(Timestamp timestamp) {
protected ReadContext getReadContext(TimestampBound timestampBound) {
throw new SpannerDataException(
"Getting stale snapshot read contexts is not supported"
+ " in read-write transaction templates.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package org.springframework.cloud.gcp.data.spanner.core;

import com.google.cloud.Timestamp;

import org.springframework.data.domain.Sort;
import org.springframework.util.Assert;

Expand Down Expand Up @@ -68,4 +70,16 @@ public SpannerPageableQueryOptions setAllowPartialRead(boolean allowPartialRead)
super.setAllowPartialRead(allowPartialRead);
return this;
}

@Override
public SpannerPageableQueryOptions setTimestamp(Timestamp timestamp) {
ChengyuanZhao marked this conversation as resolved.
Show resolved Hide resolved
super.setTimestamp(timestamp);
return this;
}

@Override
public SpannerPageableQueryOptions setBoundedTimestamp(boolean boundedTimestamp) {
ChengyuanZhao marked this conversation as resolved.
Show resolved Hide resolved
super.setBoundedTimestamp(boundedTimestamp);
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public class SpannerQueryOptions implements Serializable {

private Timestamp timestamp;

private boolean boundedTimestamp = false;

private Set<String> includeProperties;

private boolean allowPartialRead;
Expand All @@ -67,6 +69,26 @@ public SpannerQueryOptions setIncludeProperties(Set<String> includeProperties) {
return this;
}

/**
* Return whether this options object holds a bounded staleness timestamp.
* @return {@code true} if the timestamp set in this options object is a bounded
* staleness. {@code false} if it is an exact staleness. Default is {@code false}.
*/
public boolean isBoundedTimestamp() {
return this.boundedTimestamp;
}

/**
* Set if this query should be executed with bounded staleness.
* @param boundedTimestamp {@code true} if the timestamp set in this options object is a
* bounded staleness. {@code false} if it is an exact staleness.
* @return this options object.
*/
public SpannerQueryOptions setBoundedTimestamp(boolean boundedTimestamp) {
this.boundedTimestamp = boundedTimestamp;
return this;
}

public Timestamp getTimestamp() {
ChengyuanZhao marked this conversation as resolved.
Show resolved Hide resolved
return this.timestamp;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public class SpannerReadOptions implements Serializable {

private Timestamp timestamp;

private boolean boundedTimestamp = false;

private String index;

private Set<String> includeProperties;
Expand Down Expand Up @@ -77,6 +79,26 @@ public SpannerReadOptions setTimestamp(Timestamp timestamp) {
return this;
}

/**
* Return whether this options object holds a bounded staleness timestamp.
* @return {@code true} if the timestamp set in this options object is a bounded
* staleness. {@code false} if it is an exact staleness. Default is {@code false}.
*/
public boolean isBoundedTimestamp() {
return this.boundedTimestamp;
}

/**
* Set if this query should be executed with bounded staleness.
* @param boundedTimestamp {@code true} if the timestamp set in this options object is a
* bounded staleness. {@code false} if it is an exact staleness.
* @return this options object.
*/
public SpannerReadOptions setBoundedTimestamp(boolean boundedTimestamp) {
this.boundedTimestamp = boundedTimestamp;
return this;
}

public String getIndex() {
return this.index;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,9 @@ protected ReadContext getReadContext() {
return doWithOrWithoutTransactionContext((x) -> x, this.databaseClientProvider.get()::singleUse);
}

protected ReadContext getReadContext(Timestamp timestamp) {
protected ReadContext getReadContext(TimestampBound timestampBound) {
return doWithOrWithoutTransactionContext((x) -> x,
() -> this.databaseClientProvider.get()
.singleUse(TimestampBound.ofReadTimestamp(timestamp)));
() -> this.databaseClientProvider.get().singleUse(timestampBound));
}

public SpannerMappingContext getMappingContext() {
Expand Down Expand Up @@ -380,7 +379,7 @@ public <T> T performReadOnlyTransaction(Function<SpannerTemplate, T> operations,
SpannerReadOptions options = (readOptions != null) ? readOptions : new SpannerReadOptions();
try (ReadOnlyTransaction readOnlyTransaction = (options.getTimestamp() != null)
? this.databaseClientProvider.get().readOnlyTransaction(
TimestampBound.ofReadTimestamp(options.getTimestamp()))
getStaleness(options.getTimestamp(), options.isBoundedTimestamp()))
: this.databaseClientProvider.get().readOnlyTransaction()) {
return operations.apply(new ReadOnlyTransactionSpannerTemplate(
SpannerTemplate.this.databaseClientProvider,
Expand Down Expand Up @@ -413,9 +412,11 @@ public ResultSet executeQuery(Statement statement, SpannerQueryOptions options)

private String getQueryLogMessageWithOptions(Statement statement, SpannerQueryOptions options) {
String message;
StringBuilder logSb = new StringBuilder("Executing query").append(
(options.getTimestamp() != null) ? " at timestamp" + options.getTimestamp()
: "");
StringBuilder logSb = new StringBuilder("Executing query");
if (options.getTimestamp() != null) {
logSb.append(" at timestamp" + options.getTimestamp() + " with "
+ (options.isBoundedTimestamp() ? "bounded" : "exact") + " staleness");
}
for (QueryOption queryOption : options.getQueryOptions()) {
logSb.append(" with option: " + queryOption);
}
Expand All @@ -430,7 +431,8 @@ private ResultSet performQuery(Statement statement, SpannerQueryOptions options)
resultSet = getReadContext().executeQuery(statement);
}
else {
resultSet = ((options.getTimestamp() != null) ? getReadContext(options.getTimestamp())
resultSet = ((options.getTimestamp() != null)
? getReadContext(getStaleness(options.getTimestamp(), options.isBoundedTimestamp()))
: getReadContext()).executeQuery(statement,
options.getQueryOptions());
}
Expand All @@ -445,7 +447,7 @@ private ResultSet executeRead(String tableName, KeySet keys, Iterable<String> co
ResultSet resultSet;

ReadContext readContext = (options != null && options.getTimestamp() != null)
? getReadContext(options.getTimestamp())
? getReadContext(getStaleness(options.getTimestamp(), options.isBoundedTimestamp()))
: getReadContext();

if (options == null) {
Expand Down Expand Up @@ -475,7 +477,8 @@ private void logReadOptions(SpannerReadOptions options, StringBuilder logs) {
return;
}
if (options.getTimestamp() != null) {
logs.append(" at timestamp " + options.getTimestamp());
logs.append(" at timestamp " + options.getTimestamp() + " with "
+ (options.isBoundedTimestamp() ? "bounded" : "exact") + " staleness");
}
for (ReadOption readOption : options.getReadOptions()) {
logs.append(" with option: " + readOption);
Expand Down Expand Up @@ -581,4 +584,9 @@ private void maybeEmitEvent(ApplicationEvent event) {
this.eventPublisher.publishEvent(event);
}
}

private TimestampBound getStaleness(Timestamp timestamp, boolean isBoundedStaleness) {
return isBoundedStaleness ? TimestampBound.ofMinReadTimestamp(timestamp)
: TimestampBound.ofReadTimestamp(timestamp);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ public void readOnlyTransactionTest() {

ReadOnlyTransaction readOnlyTransaction = mock(ReadOnlyTransaction.class);
when(this.databaseClient.readOnlyTransaction(
eq(TimestampBound.ofReadTimestamp(Timestamp.ofTimeMicroseconds(333)))))
// setBoundedTimestamp(true) was used, so this bounded MinReadTimestamp staleness is
// expected.
eq(TimestampBound.ofMinReadTimestamp(Timestamp.ofTimeMicroseconds(333)))))
.thenReturn(readOnlyTransaction);

String finalResult = this.spannerTemplate
Expand All @@ -228,7 +230,7 @@ public void readOnlyTransactionTest() {
Key.of("key"));
return "all done";
}, new SpannerReadOptions()
.setTimestamp(Timestamp.ofTimeMicroseconds(333)));
.setTimestamp(Timestamp.ofTimeMicroseconds(333)).setBoundedTimestamp(true));

assertThat(finalResult).isEqualTo("all done");
verify(readOnlyTransaction, times(2)).read(eq("custom_test_table"), any(), any());
Expand All @@ -241,6 +243,7 @@ public void readOnlyTransactionDmlTest() {

ReadOnlyTransaction readOnlyTransaction = mock(ReadOnlyTransaction.class);
when(this.databaseClient.readOnlyTransaction(
// no setBoundedTimestamp was called, so exact staleness is expected.
eq(TimestampBound.ofReadTimestamp(Timestamp.ofTimeMicroseconds(333)))))
.thenReturn(readOnlyTransaction);

Expand Down Expand Up @@ -352,9 +355,12 @@ public void findKeySetTest() {
public void findMultipleKeysTest() {
ResultSet results = mock(ResultSet.class);
ReadOption readOption = mock(ReadOption.class);
SpannerReadOptions options = new SpannerReadOptions().addReadOption(readOption);
SpannerReadOptions options = new SpannerReadOptions().addReadOption(readOption)
.setTimestamp(Timestamp.ofTimeMicroseconds(333L)).setBoundedTimestamp(true);
KeySet keySet = KeySet.singleKey(Key.of("key"));
when(this.readContext.read(any(), any(), any(), any())).thenReturn(results);
when(this.databaseClient.singleUse(eq(TimestampBound.ofMinReadTimestamp(Timestamp.ofTimeMicroseconds(333L)))))
.thenReturn(this.readContext);

verifyAfterEvents(new AfterReadEvent(Collections.emptyList(), keySet, options),
() -> this.spannerTemplate.read(TestEntity.class, keySet, options), x -> {
Expand All @@ -363,7 +369,8 @@ public void findMultipleKeysTest() {
verify(this.readContext, times(1)).read(eq("custom_test_table"), same(keySet),
any(), same(readOption));
});
verify(this.databaseClient, times(1)).singleUse();
verify(this.databaseClient, times(1))
.singleUse(TimestampBound.ofMinReadTimestamp(Timestamp.ofTimeMicroseconds(333L)));
}

@Test
Expand Down Expand Up @@ -626,11 +633,16 @@ public void findAllPageableTest() {
Sort sort = mock(Sort.class);
Pageable pageable = mock(Pageable.class);

when(this.databaseClient.singleUse(eq(TimestampBound.ofMinReadTimestamp(Timestamp.ofTimeMicroseconds(333L)))))
.thenReturn(this.readContext);

long offset = 5L;
int limit = 3;
SpannerPageableQueryOptions queryOption = new SpannerPageableQueryOptions()
.setOffset(offset)
.setLimit(limit);
.setLimit(limit)
.setTimestamp(Timestamp.ofTimeMicroseconds(333L))
.setBoundedTimestamp(true);

when(pageable.getOffset()).thenReturn(offset);
when(pageable.getPageSize()).thenReturn(limit);
Expand Down