Skip to content

Commit f831476

Browse files
authored
Merge pull request #132 from SpineEventEngine/thread-safe-transactions
Thread-safe transactions
2 parents e0bc530 + 803bb21 commit f831476

File tree

13 files changed

+368
-58
lines changed

13 files changed

+368
-58
lines changed

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ gcs-auth-key.json
2929
# They are stored in the encrypted file and are decrypted on the CI server only.
3030

3131
# This file extracted form credentials.tar
32-
spine-dev*.json
33-
/datastore/src/test/resources/spine-dev.json
34-
/stackdriver-trace/src/test/resources/spine-dev.json
32+
**/spine-dev*.json
3533

3634
package-lock.json

config

datastore/src/main/java/io/spine/server/storage/datastore/DatastoreWrapper.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,17 @@ private void deleteEntities(Key[] keys) {
432432
}
433433
}
434434

435+
/**
436+
* Starts a new database transaction.
437+
*
438+
* @return the new transaction
439+
* @see TransactionWrapper
440+
*/
441+
public final TransactionWrapper newTransaction() {
442+
Transaction tx = datastore.newTransaction();
443+
return new TransactionWrapper(tx);
444+
}
445+
435446
/**
436447
* Starts a transaction.
437448
*
@@ -443,7 +454,9 @@ private void deleteEntities(Key[] keys) {
443454
* if a transaction is already started on this instance of
444455
* {@code DatastoreWrapper}
445456
* @see #isTransactionActive()
457+
* @deprecated Use {@link #newTransaction()} instead.
446458
*/
459+
@Deprecated
447460
public void startTransaction() throws IllegalStateException {
448461
checkState(!isTransactionActive(), NOT_ACTIVE_TRANSACTION_CONDITION_MESSAGE);
449462
activeTransaction = datastore.newTransaction();
@@ -461,7 +474,9 @@ public void startTransaction() throws IllegalStateException {
461474
* if no transaction is started on this instance of
462475
* {@code DatastoreWrapper}
463476
* @see #isTransactionActive()
477+
* @deprecated Use {@link #newTransaction()} instead.
464478
*/
479+
@Deprecated
465480
public void commitTransaction() throws IllegalStateException {
466481
checkState(isTransactionActive(), ACTIVE_TRANSACTION_CONDITION_MESSAGE);
467482
activeTransaction.commit();
@@ -481,7 +496,9 @@ public void commitTransaction() throws IllegalStateException {
481496
* if no transaction is active for the current
482497
* instance of {@code DatastoreWrapper}
483498
* @see #isTransactionActive()
499+
* @deprecated Use {@link #newTransaction()} instead.
484500
*/
501+
@Deprecated
485502
public void rollbackTransaction() throws IllegalStateException {
486503
checkState(isTransactionActive(), ACTIVE_TRANSACTION_CONDITION_MESSAGE);
487504
activeTransaction.rollback();
@@ -492,7 +509,9 @@ public void rollbackTransaction() throws IllegalStateException {
492509
* Checks whether there is an active transaction on this instance of {@code DatastoreWrapper}.
493510
*
494511
* @return {@code true} if there is an active transaction, {@code false} otherwise
512+
* @deprecated Use {@link #newTransaction()} instead.
495513
*/
514+
@Deprecated
496515
public boolean isTransactionActive() {
497516
return activeTransaction != null && activeTransaction.isActive();
498517
}

datastore/src/main/java/io/spine/server/storage/datastore/DsMessageStorage.java

Lines changed: 8 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -141,31 +141,20 @@ public final void write(I id, M message) {
141141
* Behaves similarly to {@link #write(Message)}, but performs the operation
142142
* in scope of a Datastore transaction.
143143
*
144-
* <p>If there is no active transaction at the moment, the new transaction is started
145-
* and committed. In case there is already an active transaction, the write operation
146-
* is performed in its scope.
147-
*
148144
* @param message
149145
* the message to write
150146
*/
151-
@SuppressWarnings("OverlyBroadCatchBlock") // handling all possible transaction-related issues.
152147
final void writeTransactionally(M message) {
153148
checkNotNull(message);
154149

155-
boolean txRequired = !datastore.isTransactionActive();
156-
if (txRequired) {
157-
datastore.startTransaction();
158-
}
159-
try {
160-
write(message);
161-
if (txRequired) {
162-
datastore.commitTransaction();
163-
}
164-
} catch (Exception e) {
165-
if (txRequired && datastore.isTransactionActive()) {
166-
datastore.rollbackTransaction();
167-
}
168-
throw newIllegalStateException("Error committing the transaction.", e);
150+
try (TransactionWrapper tx = datastore.newTransaction()) {
151+
Entity entity = toEntity(message);
152+
tx.createOrUpdate(entity);
153+
tx.commit();
154+
} catch (RuntimeException e) {
155+
throw newIllegalStateException(e,
156+
"Error writing a `%s` in transaction.",
157+
message.getClass().getName());
169158
}
170159
}
171160

datastore/src/main/java/io/spine/server/storage/datastore/MessageColumn.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import com.google.cloud.datastore.Entity;
2424
import com.google.cloud.datastore.Value;
25+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2526
import com.google.errorprone.annotations.Immutable;
2627
import com.google.protobuf.Message;
2728

@@ -55,6 +56,7 @@ interface MessageColumn<M extends Message> {
5556
* the message which field value is going to be set
5657
* @return the same instance of {@code builder} with the property filled
5758
*/
59+
@CanIgnoreReturnValue
5860
default Entity.Builder fill(Entity.Builder builder, M message) {
5961
Value<?> value = getter().apply(message);
6062
builder.set(columnName(), value);

datastore/src/main/java/io/spine/server/storage/datastore/RecordStorageBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
package io.spine.server.storage.datastore;
2222

23+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2324
import com.google.protobuf.Descriptors.Descriptor;
2425
import io.spine.server.entity.Entity;
2526
import io.spine.server.entity.model.EntityClass;
@@ -58,6 +59,7 @@ abstract class RecordStorageBuilder<I,
5859
/**
5960
* Sets the type URL of the entity state, which is stored in the resulting storage.
6061
*/
62+
@CanIgnoreReturnValue
6163
public B setStateType(TypeUrl stateTypeUrl) {
6264
checkNotNull(stateTypeUrl);
6365
Descriptor descriptor = stateTypeUrl.toTypeName()
@@ -69,6 +71,7 @@ public B setStateType(TypeUrl stateTypeUrl) {
6971
/**
7072
* Assignts the ID class of the stored entities.
7173
*/
74+
@CanIgnoreReturnValue
7275
public B setIdClass(Class<I> idClass) {
7376
this.idClass = checkNotNull(idClass);
7477
return self();
@@ -77,6 +80,7 @@ public B setIdClass(Class<I> idClass) {
7780
/**
7881
* Assigns the class of the stored entity.
7982
*/
83+
@CanIgnoreReturnValue
8084
public B setEntityClass(Class<? extends Entity<?, ?>> entityClass) {
8185
this.entityClass = checkNotNull(entityClass);
8286
return self();
@@ -90,6 +94,7 @@ public B setEntityClass(Class<? extends Entity<?, ?>> entityClass) {
9094
* {@linkplain #setEntityClass(Class) entity class} separately.
9195
*/
9296
@SuppressWarnings("unchecked") // The ID class is ensured by the parameter type.
97+
@CanIgnoreReturnValue
9398
public B setModelClass(EntityClass<? extends Entity<I, ?>> modelClass) {
9499
TypeUrl stateType = modelClass.stateType();
95100
Class<I> idClass = (Class<I>) modelClass.idClass();
@@ -104,6 +109,7 @@ public B setModelClass(EntityClass<? extends Entity<I, ?>> modelClass) {
104109
/**
105110
* Sets the {@link io.spine.server.storage.datastore.DatastoreWrapper} to use in this storage.
106111
*/
112+
@CanIgnoreReturnValue
107113
public B setDatastore(DatastoreWrapper datastore) {
108114
this.datastore = checkNotNull(datastore);
109115
return self();
@@ -117,6 +123,7 @@ public B setDatastore(DatastoreWrapper datastore) {
117123
* {@link io.spine.server.storage.Storage#isMultitenant multitenant},
118124
* {@code false} otherwise
119125
*/
126+
@CanIgnoreReturnValue
120127
public B setMultitenant(boolean multitenant) {
121128
this.multitenant = multitenant;
122129
return self();
@@ -126,6 +133,7 @@ public B setMultitenant(boolean multitenant) {
126133
* Assigns the type registry of
127134
* the {@linkplain io.spine.server.entity.storage.EntityColumn entity columns}.
128135
*/
136+
@CanIgnoreReturnValue
129137
public B setColumnTypeRegistry(
130138
ColumnTypeRegistry<? extends DatastoreColumnType<?, ?>> columnTypeRegistry) {
131139
this.columnTypeRegistry = checkNotNull(columnTypeRegistry);
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* Copyright 2019, TeamDev. All rights reserved.
3+
*
4+
* Redistribution and use in source and/or binary forms, with or without
5+
* modification, must retain the above copyright notice and the following
6+
* disclaimer.
7+
*
8+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
9+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
10+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
11+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
12+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
13+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
14+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
15+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
16+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
17+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
18+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
19+
*/
20+
21+
package io.spine.server.storage.datastore;
22+
23+
import com.google.cloud.datastore.DatastoreException;
24+
import com.google.cloud.datastore.Entity;
25+
import com.google.cloud.datastore.Key;
26+
import com.google.cloud.datastore.Transaction;
27+
28+
import java.util.Optional;
29+
30+
import static com.google.common.base.Preconditions.checkNotNull;
31+
32+
/**
33+
* A Cloud Datastore transaction wrapper.
34+
*/
35+
public final class TransactionWrapper implements AutoCloseable {
36+
37+
private final Transaction tx;
38+
39+
TransactionWrapper(Transaction tx) {
40+
this.tx = checkNotNull(tx);
41+
}
42+
43+
/**
44+
* Puts the given entity into the Datastore in the transaction.
45+
*/
46+
public void createOrUpdate(Entity entity) {
47+
tx.put(entity);
48+
}
49+
50+
/**
51+
* Reads an entity from the Datastore in the transaction.
52+
*
53+
* @return the entity with the given key or {@code Optional.empty()} if such an entity does not
54+
* exist
55+
*/
56+
public Optional<Entity> read(Key key) {
57+
Entity entity = tx.get(key);
58+
return Optional.ofNullable(entity);
59+
}
60+
61+
/**
62+
* Commits this transaction.
63+
*
64+
* @throws DatastoreException if the transaction is no longer active
65+
*/
66+
public void commit() {
67+
tx.commit();
68+
}
69+
70+
/**
71+
* Rolls back this transaction.
72+
*
73+
* @throws DatastoreException if the transaction is no longer active
74+
*/
75+
public void rollback() {
76+
tx.rollback();
77+
}
78+
79+
/**
80+
* Rolls back this transaction if it's still active.
81+
*/
82+
@Override
83+
public void close() {
84+
if (tx.isActive()) {
85+
rollback();
86+
}
87+
}
88+
}

datastore/src/main/java/io/spine/server/storage/datastore/package-info.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@
1717
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
1818
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1919
*/
20+
2021
/**
2122
* This package contains Google Cloud Datastore implementation of the storages.
2223
*
2324
* @see io.spine.server.storage.AbstractStorage
2425
*/
26+
@CheckReturnValue
2527
@ParametersAreNonnullByDefault
2628
package io.spine.server.storage.datastore;
2729

30+
import com.google.errorprone.annotations.CheckReturnValue;
31+
2832
import javax.annotation.ParametersAreNonnullByDefault;

datastore/src/test/java/io/spine/server/storage/datastore/DatastoreWrapperTest.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ static void tearDown() {
7777
wrapper.dropTable(NAMESPACE_HOLDER_KIND);
7878
}
7979

80+
@SuppressWarnings("deprecation") // To be deleted alongside with the tested API.
8081
@Nested
8182
class SingleTenant {
8283

0 commit comments

Comments
 (0)