Skip to content

Commit 4ed4240

Browse files
committed
feat(Datastore): Add a failsafe hatch for disabling previous transaction retries using spring.cloud.gcp.datastore.previous-transaction-retry-enabled.
Change-Id: Ib23c2d4199fdccaf88eef83fc12e0085d40fe34a
1 parent d23dd6c commit 4ed4240

5 files changed

Lines changed: 67 additions & 5 deletions

File tree

docs/src/main/asciidoc/datastore.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ The following configuration options are available:
8282
| `spring.cloud.gcp.datastore.emulator.firestore-in-datastore-mode` | Configures whether the emulator runs in "Cloud Firestore in Datastore Mode" | No | `false`
8383
| `spring.cloud.gcp.datastore.skip-null-value` | Whether skip inserting `null` values. | No | The default value is `false`.
8484
If configured to `true`, `null` value will not be inserted into the datastore.
85-
85+
| `spring.cloud.gcp.datastore.previous-transaction-retry-enabled` | Whether to retry a transactions with previous transaction when combined with Retriable. | No | `true`
8686
|===
8787

8888
==== Repository settings

spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/datastore/DatastoreTransactionManagerAutoConfiguration.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2626
import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration;
2727
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
28+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
2829
import org.springframework.context.annotation.Bean;
2930

3031
/**
@@ -44,24 +45,29 @@ private DatastoreTransactionManagerAutoConfiguration() {
4445

4546
/** Configuration class. */
4647
@AutoConfiguration
48+
@EnableConfigurationProperties(GcpDatastoreProperties.class)
4749
static class DatastoreTransactionManagerConfiguration {
4850

4951
private final DatastoreProvider datastore;
5052

5153
private final TransactionManagerCustomizers transactionManagerCustomizers;
5254

55+
private final boolean previousTransactionRetryEnabled;
56+
5357
DatastoreTransactionManagerConfiguration(
58+
GcpDatastoreProperties gcpDatastoreProperties,
5459
DatastoreProvider datastore,
5560
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
5661
this.datastore = datastore;
5762
this.transactionManagerCustomizers = transactionManagerCustomizers.getIfAvailable();
63+
this.previousTransactionRetryEnabled = gcpDatastoreProperties.isPreviousTransactionRetryEnabled();
5864
}
5965

6066
@Bean
6167
@ConditionalOnMissingBean
6268
public DatastoreTransactionManager datastoreTransactionManager() {
6369
DatastoreTransactionManager transactionManager =
64-
new DatastoreTransactionManager(this.datastore);
70+
new DatastoreTransactionManager(this.datastore, this.previousTransactionRetryEnabled);
6571
if (this.transactionManagerCustomizers != null) {
6672
this.transactionManagerCustomizers.customize(transactionManager);
6773
}

spring-cloud-gcp-autoconfigure/src/main/java/com/google/cloud/spring/autoconfigure/datastore/GcpDatastoreProperties.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ public class GcpDatastoreProperties implements CredentialsSupplier {
4949
/** Whether skip the insertion if the value is null */
5050
private boolean skipNullValue;
5151

52+
/** Whether to retry a transactions with previous transaction when combined with Retriable. */
53+
private boolean previousTransactionRetryEnabled = true;
54+
5255
@Override
5356
public Credentials getCredentials() {
5457
return this.credentials;
@@ -97,4 +100,12 @@ public boolean isSkipNullValue() {
97100
public void setSkipNullValue(boolean skipNullValue) {
98101
this.skipNullValue = skipNullValue;
99102
}
103+
104+
public boolean isPreviousTransactionRetryEnabled() {
105+
return previousTransactionRetryEnabled;
106+
}
107+
108+
public void setPreviousTransactionRetryEnabled(boolean previousTransactionRetryEnabled) {
109+
this.previousTransactionRetryEnabled = previousTransactionRetryEnabled;
110+
}
100111
}

spring-cloud-gcp-data-datastore/src/main/java/com/google/cloud/spring/data/datastore/core/DatastoreTransactionManager.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,24 @@
3939
*/
4040
public class DatastoreTransactionManager extends AbstractPlatformTransactionManager {
4141

42-
private static final String PREVIOUS_TRANSACTION_ID_ATTRIBUTE = "datastore-transaction-id";
42+
static final String PREVIOUS_TRANSACTION_ID_ATTRIBUTE = "datastore-transaction-id";
4343

4444
private static final TransactionOptions READ_ONLY_OPTIONS =
4545
TransactionOptions.newBuilder()
4646
.setReadOnly(TransactionOptions.ReadOnly.newBuilder().build())
4747
.build();
4848

4949
private final Supplier<Datastore> datastore;
50+
51+
private final boolean previousTransactionRetryEnabled;
5052

5153
public DatastoreTransactionManager(final Supplier<Datastore> datastore) {
54+
this(datastore, true);
55+
}
56+
57+
public DatastoreTransactionManager(final Supplier<Datastore> datastore, boolean previousTransactionRetryEnabled) {
5258
this.datastore = datastore;
59+
this.previousTransactionRetryEnabled = previousTransactionRetryEnabled;
5360
}
5461

5562
@Override
@@ -82,7 +89,7 @@ protected void doBegin(Object transactionObject, TransactionDefinition transacti
8289
Tx tx = (Tx) transactionObject;
8390
if (transactionDefinition.isReadOnly()) {
8491
tx.transaction = tx.datastore.newTransaction(READ_ONLY_OPTIONS);
85-
} else if (retryContext != null && retryContext.hasAttribute(PREVIOUS_TRANSACTION_ID_ATTRIBUTE)) {
92+
} else if (this.previousTransactionRetryEnabled && retryContext != null && retryContext.hasAttribute(PREVIOUS_TRANSACTION_ID_ATTRIBUTE)) {
8693
tx.transaction = tx.datastore.newTransaction(
8794
TransactionOptions.newBuilder()
8895
.setReadWrite(ReadWrite.newBuilder().setPreviousTransaction(
@@ -110,7 +117,7 @@ protected void doCommit(DefaultTransactionStatus defaultTransactionStatus)
110117
this.logger.debug("Transaction was not committed because it is no longer active.");
111118
}
112119
} catch (DatastoreException ex) {
113-
if (retryContext != null) {
120+
if (this.previousTransactionRetryEnabled && retryContext != null) {
114121
retryContext.setAttribute(PREVIOUS_TRANSACTION_ID_ATTRIBUTE, tx.transaction.getTransactionId());
115122
}
116123
throw new TransactionSystemException("Cloud Datastore transaction failed to commit.", ex);

spring-cloud-gcp-data-datastore/src/test/java/com/google/cloud/spring/data/datastore/core/DatastoreTransactionManagerTests.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static org.assertj.core.api.Assertions.assertThat;
2020
import static org.assertj.core.api.Assertions.assertThatThrownBy;
21+
import static org.mockito.ArgumentMatchers.any;
2122
import static org.mockito.Mockito.doThrow;
2223
import static org.mockito.Mockito.mock;
2324
import static org.mockito.Mockito.never;
@@ -34,6 +35,8 @@
3435
import org.junit.rules.ExpectedException;
3536
import org.mockito.Mock;
3637
import org.mockito.MockitoAnnotations;
38+
import org.springframework.retry.RetryContext;
39+
import org.springframework.retry.support.RetrySynchronizationManager;
3740
import org.springframework.transaction.TransactionDefinition;
3841
import org.springframework.transaction.TransactionSystemException;
3942
import org.springframework.transaction.support.DefaultTransactionDefinition;
@@ -46,6 +49,8 @@ class DatastoreTransactionManagerTests {
4649

4750
@Mock Transaction transaction;
4851

52+
@Mock RetryContext retryContext;
53+
4954
private Tx tx;
5055

5156
private DatastoreTransactionManager manager;
@@ -61,6 +66,7 @@ void initMocks() {
6166

6267
when(this.datastore.newTransaction()).thenReturn(this.transaction);
6368
when(this.status.getTransaction()).thenReturn(this.tx);
69+
RetrySynchronizationManager.register(retryContext);
6470
}
6571

6672
@Test
@@ -148,4 +154,36 @@ void testDoRollbackNotActive() {
148154
this.manager.doRollback(this.status);
149155
verify(this.transaction, never()).rollback();
150156
}
157+
158+
@Test
159+
void testFailCommitSetsPreviousTransactionId() {
160+
DatastoreException exception = new DatastoreException(0, "", "");
161+
when(this.transaction.isActive()).thenReturn(true);
162+
when(this.transaction.commit()).thenThrow(exception);
163+
this.tx.setTransaction(this.transaction);
164+
165+
assertThatThrownBy(() -> this.manager.doCommit(this.status))
166+
.isInstanceOf(TransactionSystemException.class)
167+
.hasMessage("Cloud Datastore transaction failed to commit.")
168+
.hasCause(exception);
169+
verify(retryContext, times(1)).setAttribute(any(), any());
170+
}
171+
172+
@Test
173+
void testFailCommitRespectsIgnoringRetry() {
174+
this.manager = new DatastoreTransactionManager(() -> datastore, false);
175+
this.tx = (Tx) manager.doGetTransaction();
176+
when(this.status.getTransaction()).thenReturn(this.tx);
177+
178+
DatastoreException exception = new DatastoreException(0, "", "");
179+
when(this.transaction.isActive()).thenReturn(true);
180+
when(this.transaction.commit()).thenThrow(exception);
181+
this.tx.setTransaction(this.transaction);
182+
183+
assertThatThrownBy(() -> this.manager.doCommit(this.status))
184+
.isInstanceOf(TransactionSystemException.class)
185+
.hasMessage("Cloud Datastore transaction failed to commit.")
186+
.hasCause(exception);
187+
verify(retryContext, never()).setAttribute(any(), any());
188+
}
151189
}

0 commit comments

Comments
 (0)