Skip to content

Commit b02f584

Browse files
feat: savepoints (#2278)
* feat: savepoint Adds support for savepoints to the Connection API. Savepoints use the internal retry mechanism for read/write transactions to emulate actual savepoints. Rolling back to a savepoint is guaranteed to always work, but resuming the transaction after rolling back can fail if the underlying data has been modified, or if one or more of the statements before the savepoint that was rolled back to returns non-deterministic data. * fix: add clirr diff * docs: comments + more tests * chore: fix and test identifier verification * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: make savepoint feature configurable * fix: set savepoint support * docs: improve javadoc * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent f40ce23 commit b02f584

27 files changed

+2631
-233
lines changed

README.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-spanner'
5757
If you are using Gradle without BOM, add this to your dependencies:
5858

5959
```Groovy
60-
implementation 'com.google.cloud:google-cloud-spanner:6.38.2'
60+
implementation 'com.google.cloud:google-cloud-spanner:6.39.0'
6161
```
6262

6363
If you are using SBT, add this to your dependencies:
6464

6565
```Scala
66-
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.38.2"
66+
libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.39.0"
6767
```
6868
<!-- {x-version-update-end} -->
6969

@@ -411,7 +411,7 @@ Java is a registered trademark of Oracle and/or its affiliates.
411411
[kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html
412412
[stability-image]: https://img.shields.io/badge/stability-stable-green
413413
[maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg
414-
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.38.2
414+
[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.39.0
415415
[authentication]: https://github.com/googleapis/google-cloud-java#authentication
416416
[auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes
417417
[predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles

google-cloud-spanner/clirr-ignored-differences.xml

+26
Original file line numberDiff line numberDiff line change
@@ -222,4 +222,30 @@
222222
<className>com/google/cloud/spanner/connection/Connection</className>
223223
<method>com.google.cloud.spanner.ResultSet analyzeUpdateStatement(com.google.cloud.spanner.Statement, com.google.cloud.spanner.ReadContext$QueryAnalyzeMode, com.google.cloud.spanner.Options$UpdateOption[])</method>
224224
</difference>
225+
<!-- Savepoints -->
226+
<difference>
227+
<differenceType>7012</differenceType>
228+
<className>com/google/cloud/spanner/connection/Connection</className>
229+
<method>void setSavepointSupport(com.google.cloud.spanner.connection.SavepointSupport)</method>
230+
</difference>
231+
<difference>
232+
<differenceType>7012</differenceType>
233+
<className>com/google/cloud/spanner/connection/Connection</className>
234+
<method>com.google.cloud.spanner.connection.SavepointSupport getSavepointSupport()</method>
235+
</difference>
236+
<difference>
237+
<differenceType>7012</differenceType>
238+
<className>com/google/cloud/spanner/connection/Connection</className>
239+
<method>void savepoint(java.lang.String)</method>
240+
</difference>
241+
<difference>
242+
<differenceType>7012</differenceType>
243+
<className>com/google/cloud/spanner/connection/Connection</className>
244+
<method>void releaseSavepoint(java.lang.String)</method>
245+
</difference>
246+
<difference>
247+
<differenceType>7012</differenceType>
248+
<className>com/google/cloud/spanner/connection/Connection</className>
249+
<method>void rollbackToSavepoint(java.lang.String)</method>
250+
</difference>
225251
</differences>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java

+29
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import com.google.api.gax.grpc.GrpcCallContext;
2222
import com.google.api.gax.longrunning.OperationFuture;
2323
import com.google.api.gax.rpc.ApiCallContext;
24+
import com.google.cloud.spanner.Dialect;
2425
import com.google.cloud.spanner.ErrorCode;
2526
import com.google.cloud.spanner.Options.RpcPriority;
2627
import com.google.cloud.spanner.SpannerException;
@@ -45,6 +46,7 @@
4546
import java.util.concurrent.Future;
4647
import java.util.concurrent.TimeUnit;
4748
import java.util.concurrent.TimeoutException;
49+
import javax.annotation.Nonnull;
4850
import javax.annotation.Nullable;
4951
import javax.annotation.concurrent.GuardedBy;
5052

@@ -128,6 +130,33 @@ B setRpcPriority(@Nullable RpcPriority rpcPriority) {
128130
this.rpcPriority = builder.rpcPriority;
129131
}
130132

133+
/**
134+
* Returns a descriptive name for the type of transaction / unit of work. This is used in error
135+
* messages.
136+
*/
137+
abstract String getUnitOfWorkName();
138+
139+
@Override
140+
public void savepoint(@Nonnull String name, @Nonnull Dialect dialect) {
141+
throw SpannerExceptionFactory.newSpannerException(
142+
ErrorCode.FAILED_PRECONDITION, "Savepoint is not supported for " + getUnitOfWorkName());
143+
}
144+
145+
@Override
146+
public void releaseSavepoint(@Nonnull String name) {
147+
throw SpannerExceptionFactory.newSpannerException(
148+
ErrorCode.FAILED_PRECONDITION,
149+
"Release savepoint is not supported for " + getUnitOfWorkName());
150+
}
151+
152+
@Override
153+
public void rollbackToSavepoint(
154+
@Nonnull String name, @Nonnull SavepointSupport savepointSupport) {
155+
throw SpannerExceptionFactory.newSpannerException(
156+
ErrorCode.FAILED_PRECONDITION,
157+
"Rollback to savepoint is not supported for " + getUnitOfWorkName());
158+
}
159+
131160
StatementExecutor getStatementExecutor() {
132161
return statementExecutor;
133162
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractMultiUseTransaction.java

+98
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,71 @@
1717
package com.google.cloud.spanner.connection;
1818

1919
import com.google.api.core.ApiFuture;
20+
import com.google.cloud.spanner.Dialect;
2021
import com.google.cloud.spanner.ErrorCode;
2122
import com.google.cloud.spanner.Options.QueryOption;
2223
import com.google.cloud.spanner.ReadContext;
2324
import com.google.cloud.spanner.ResultSet;
2425
import com.google.cloud.spanner.SpannerException;
2526
import com.google.cloud.spanner.SpannerExceptionFactory;
2627
import com.google.cloud.spanner.connection.AbstractStatementParser.ParsedStatement;
28+
import com.google.common.annotations.VisibleForTesting;
2729
import com.google.common.base.Preconditions;
30+
import com.google.common.collect.ImmutableList;
2831
import com.google.spanner.v1.SpannerGrpc;
32+
import java.util.LinkedList;
33+
import java.util.Objects;
34+
import javax.annotation.Nonnull;
2935

3036
/**
3137
* Base class for {@link Connection}-based transactions that can be used for multiple read and
3238
* read/write statements.
3339
*/
3440
abstract class AbstractMultiUseTransaction extends AbstractBaseUnitOfWork {
3541

42+
/** In-memory savepoint implementation that is used by the Connection API. */
43+
static class Savepoint {
44+
private final String name;
45+
46+
static Savepoint of(String name) {
47+
return new Savepoint(name);
48+
}
49+
50+
Savepoint(String name) {
51+
this.name = name;
52+
}
53+
54+
/** Returns the index of the first statement that was executed after this savepoint. */
55+
int getStatementPosition() {
56+
return -1;
57+
}
58+
59+
/** Returns the index of the first mutation that was executed after this savepoint. */
60+
int getMutationPosition() {
61+
return -1;
62+
}
63+
64+
@Override
65+
public boolean equals(Object o) {
66+
if (!(o instanceof Savepoint)) {
67+
return false;
68+
}
69+
return Objects.equals(((Savepoint) o).name, name);
70+
}
71+
72+
@Override
73+
public int hashCode() {
74+
return name.hashCode();
75+
}
76+
77+
@Override
78+
public String toString() {
79+
return name;
80+
}
81+
}
82+
83+
private final LinkedList<Savepoint> savepoints = new LinkedList<>();
84+
3685
AbstractMultiUseTransaction(Builder<?, ? extends AbstractMultiUseTransaction> builder) {
3786
super(builder);
3887
}
@@ -94,4 +143,53 @@ public void abortBatch() {
94143
throw SpannerExceptionFactory.newSpannerException(
95144
ErrorCode.FAILED_PRECONDITION, "Run batch is not supported for transactions");
96145
}
146+
147+
abstract Savepoint savepoint(String name);
148+
149+
abstract void rollbackToSavepoint(Savepoint savepoint);
150+
151+
@VisibleForTesting
152+
ImmutableList<Savepoint> getSavepoints() {
153+
return ImmutableList.copyOf(savepoints);
154+
}
155+
156+
@Override
157+
public void savepoint(@Nonnull String name, @Nonnull Dialect dialect) {
158+
if (dialect != Dialect.POSTGRESQL) {
159+
// Check that there is no savepoint with this name. Note that PostgreSQL allows multiple
160+
// savepoints in a transaction with the same name, so we don't execute this check for PG.
161+
if (savepoints.stream().anyMatch(savepoint -> savepoint.name.equals(name))) {
162+
throw SpannerExceptionFactory.newSpannerException(
163+
ErrorCode.INVALID_ARGUMENT, "Savepoint with name " + name + " already exists");
164+
}
165+
}
166+
savepoints.add(savepoint(name));
167+
}
168+
169+
@Override
170+
public void releaseSavepoint(@Nonnull String name) {
171+
// Remove the given savepoint and all later savepoints from the transaction.
172+
savepoints.subList(getSavepointIndex(name), savepoints.size()).clear();
173+
}
174+
175+
@Override
176+
public void rollbackToSavepoint(
177+
@Nonnull String name, @Nonnull SavepointSupport savepointSupport) {
178+
int index = getSavepointIndex(name);
179+
rollbackToSavepoint(savepoints.get(index));
180+
if (index < (savepoints.size() - 1)) {
181+
// Remove all savepoints that come after this savepoint from the transaction.
182+
// Rolling back to a savepoint does not remove that savepoint, only the ones that come after.
183+
savepoints.subList(index + 1, savepoints.size()).clear();
184+
}
185+
}
186+
187+
private int getSavepointIndex(String name) {
188+
int index = savepoints.lastIndexOf(savepoint(name));
189+
if (index == -1) {
190+
throw SpannerExceptionFactory.newSpannerException(
191+
ErrorCode.INVALID_ARGUMENT, "Savepoint with name " + name + " does not exist");
192+
}
193+
return index;
194+
}
97195
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ClientSideStatementValueConverters.java

+19
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,25 @@ public Priority convert(String value) {
457457
}
458458
}
459459

460+
/** Converter for converting strings to {@link SavepointSupport} values. */
461+
static class SavepointSupportConverter
462+
implements ClientSideStatementValueConverter<SavepointSupport> {
463+
private final CaseInsensitiveEnumMap<SavepointSupport> values =
464+
new CaseInsensitiveEnumMap<>(SavepointSupport.class);
465+
466+
public SavepointSupportConverter(String allowedValues) {}
467+
468+
@Override
469+
public Class<SavepointSupport> getParameterClass() {
470+
return SavepointSupport.class;
471+
}
472+
473+
@Override
474+
public SavepointSupport convert(String value) {
475+
return values.get(value);
476+
}
477+
}
478+
460479
static class ExplainCommandConverter implements ClientSideStatementValueConverter<String> {
461480
@Override
462481
public Class<String> getParameterClass() {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/Connection.java

+55
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,61 @@ default RpcPriority getRPCPriority() {
713713
*/
714714
ApiFuture<Void> rollbackAsync();
715715

716+
/** Returns the current savepoint support for this connection. */
717+
SavepointSupport getSavepointSupport();
718+
719+
/** Sets how savepoints should be supported on this connection. */
720+
void setSavepointSupport(SavepointSupport savepointSupport);
721+
722+
/**
723+
* Creates a savepoint with the given name.
724+
*
725+
* <p>The uniqueness constraints on a savepoint name depends on the database dialect that is used:
726+
*
727+
* <ul>
728+
* <li>{@link Dialect#GOOGLE_STANDARD_SQL} requires that savepoint names are unique within a
729+
* transaction. The name of a savepoint that has been released or destroyed because the
730+
* transaction has rolled back to a savepoint that was defined before that savepoint can be
731+
* re-used within the transaction.
732+
* <li>{@link Dialect#POSTGRESQL} follows the rules for savepoint names in PostgreSQL. This
733+
* means that multiple savepoints in one transaction can have the same name, but only the
734+
* last savepoint with a given name is visible. See <a
735+
* href="https://www.postgresql.org/docs/current/sql-savepoint.html">PostgreSQL savepoint
736+
* documentation</a> for more information.
737+
* </ul>
738+
*
739+
* @param name the name of the savepoint to create
740+
* @throws SpannerException if a savepoint with the same name already exists and the dialect that
741+
* is used is {@link Dialect#GOOGLE_STANDARD_SQL}
742+
* @throws SpannerException if there is no transaction on this connection
743+
* @throws SpannerException if internal retries have been disabled for this connection
744+
*/
745+
void savepoint(String name);
746+
747+
/**
748+
* Releases the savepoint with the given name. The savepoint and all later savepoints will be
749+
* removed from the current transaction and can no longer be used.
750+
*
751+
* @param name the name of the savepoint to release
752+
* @throws SpannerException if no savepoint with the given name exists
753+
*/
754+
void releaseSavepoint(String name);
755+
756+
/**
757+
* Rolls back to the given savepoint. Rolling back to a savepoint undoes all changes and releases
758+
* all internal locks that have been taken by the transaction after the savepoint. Rolling back to
759+
* a savepoint does not remove the savepoint from the transaction, and it is possible to roll back
760+
* to the same savepoint multiple times. All savepoints that have been defined after the given
761+
* savepoint are removed from the transaction.
762+
*
763+
* @param name the name of the savepoint to roll back to.
764+
* @throws SpannerException if no savepoint with the given name exists.
765+
* @throws AbortedDueToConcurrentModificationException if rolling back to the savepoint failed
766+
* because another transaction has modified the data that has been read or modified by this
767+
* transaction
768+
*/
769+
void rollbackToSavepoint(String name);
770+
716771
/**
717772
* @return <code>true</code> if this connection has a transaction (that has not necessarily
718773
* started). This method will only return false when the {@link Connection} is in autocommit

google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/ConnectionImpl.java

+43
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.google.cloud.spanner.connection;
1818

1919
import static com.google.cloud.spanner.SpannerApiFutures.get;
20+
import static com.google.cloud.spanner.connection.ConnectionPreconditions.checkValidIdentifier;
2021

2122
import com.google.api.core.ApiFuture;
2223
import com.google.api.core.ApiFutures;
@@ -213,6 +214,7 @@ static UnitOfWorkType of(TransactionMode transactionMode) {
213214
private TimestampBound readOnlyStaleness = TimestampBound.strong();
214215
private QueryOptions queryOptions = QueryOptions.getDefaultInstance();
215216
private RpcPriority rpcPriority = null;
217+
private SavepointSupport savepointSupport = SavepointSupport.FAIL_AFTER_ROLLBACK;
216218

217219
private String transactionTag;
218220
private String statementTag;
@@ -840,6 +842,46 @@ private ApiFuture<Void> endCurrentTransactionAsync(EndTransactionMethod endTrans
840842
return res;
841843
}
842844

845+
@Override
846+
public SavepointSupport getSavepointSupport() {
847+
return this.savepointSupport;
848+
}
849+
850+
@Override
851+
public void setSavepointSupport(SavepointSupport savepointSupport) {
852+
ConnectionPreconditions.checkState(!isClosed(), CLOSED_ERROR_MSG);
853+
ConnectionPreconditions.checkState(
854+
!isBatchActive(), "Cannot set SavepointSupport while in a batch");
855+
ConnectionPreconditions.checkState(
856+
!isTransactionStarted(), "Cannot set SavepointSupport while a transaction is active");
857+
this.savepointSupport = savepointSupport;
858+
}
859+
860+
@Override
861+
public void savepoint(String name) {
862+
ConnectionPreconditions.checkState(isInTransaction(), "This connection has no transaction");
863+
ConnectionPreconditions.checkState(
864+
savepointSupport.isSavepointCreationAllowed(),
865+
"This connection does not allow the creation of savepoints. Current value of SavepointSupport: "
866+
+ savepointSupport);
867+
getCurrentUnitOfWorkOrStartNewUnitOfWork().savepoint(checkValidIdentifier(name), getDialect());
868+
}
869+
870+
@Override
871+
public void releaseSavepoint(String name) {
872+
ConnectionPreconditions.checkState(
873+
isTransactionStarted(), "This connection has no active transaction");
874+
getCurrentUnitOfWorkOrStartNewUnitOfWork().releaseSavepoint(checkValidIdentifier(name));
875+
}
876+
877+
@Override
878+
public void rollbackToSavepoint(String name) {
879+
ConnectionPreconditions.checkState(
880+
isTransactionStarted(), "This connection has no active transaction");
881+
getCurrentUnitOfWorkOrStartNewUnitOfWork()
882+
.rollbackToSavepoint(checkValidIdentifier(name), savepointSupport);
883+
}
884+
843885
@Override
844886
public StatementResult execute(Statement statement) {
845887
Preconditions.checkNotNull(statement);
@@ -1302,6 +1344,7 @@ UnitOfWork createNewUnitOfWork() {
13021344
return ReadWriteTransaction.newBuilder()
13031345
.setDatabaseClient(dbClient)
13041346
.setRetryAbortsInternally(retryAbortsInternally)
1347+
.setSavepointSupport(savepointSupport)
13051348
.setReturnCommitStats(returnCommitStats)
13061349
.setTransactionRetryListeners(transactionRetryListeners)
13071350
.setStatementTimeout(statementTimeout)

0 commit comments

Comments
 (0)