Sitelet https://github.com/objectify/objectify/commit/d616968476cfdf2ff4272ed28e84ded92b61fafd
Skip to content

Commit d616968

Browse files
committed
Add TxnOptions, refactor the way transaction methods are implemented
1 parent dc2a4db commit d616968

10 files changed

Lines changed: 249 additions & 159 deletions

File tree

‎src/main/java/com/googlecode/objectify/Objectify.java‎

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,39 @@ public interface Objectify
186186
<R> R transactionless(Work<R> work);
187187

188188
/**
189-
* <p>Exactly the same behavior as the Work version, but doesn't return anything. Convenient for Java8
190-
* so you don't have to return something from the lambda.</p>
189+
* Convenience method that doesn't force you to return something from your lambda.
190+
*/
191+
default void transactionless(Runnable work) {
192+
transactionless((Work<Void>)() -> {
193+
work.run();
194+
return null;
195+
});
196+
}
197+
198+
/**
199+
* <p>Executes work in a transaction with the specified options. If there is already a transaction context,
200+
* that context will be inherited. If there is not already a transaction context, a transaction will be started.</p>
201+
*
202+
* <p>Transaction attributes are set when the transaction is initially started. Even though you can run
203+
* inner transact() blocks, these inner blocks will inherit the outer transaction as-is. You cannot
204+
* change the behavior of a running transaction (though you can suspend it and start a new one).</p>
205+
*
206+
* <p>Within {@code Work.run()}, obtain the correct transactional {@code Objectify} instance by calling
207+
* {@code ObjectifyService.ofy()}</p>
208+
*
209+
* <p>The transaction might retry if there is a concurrency failure; work must be idempotent.</p>
210+
*/
211+
<R> R transact(TxnOptions options, Work<R> work);
212+
213+
/**
214+
* Convenience method that doesn't force you to return something from your lambda.
191215
*/
192-
void transactionless(Runnable work);
216+
default void transact(final TxnOptions options, final Runnable work) {
217+
transact(options, (Work<Void>)() -> {
218+
work.run();
219+
return null;
220+
});
221+
}
193222

194223
/**
195224
* <p>Executes work in a transaction. If there is already a transaction context, that context will be inherited.
@@ -210,7 +239,9 @@ public interface Objectify
210239
* until the full transaction completes normally.
211240
* @return the result of the work
212241
*/
213-
<R> R transact(Work<R> work);
242+
default <R> R transact(final Work<R> work) {
243+
return transact(TxnOptions.deflt(), work);
244+
}
214245

215246
/**
216247
* Convenience method that doesn't force you to return something from your lambda.
@@ -223,17 +254,24 @@ default void transact(final Runnable work) {
223254
}
224255

225256
/**
226-
* <p>Executes work in a new read-write transaction. Note that this is equivalent to {@code transactNew(Integer.MAX_VALUE, work);}</p>
257+
* <p>Executes work in a NEW transaction with the specified options. If there is already an existing transaction
258+
* running, it is suspended while the new transaction completes.</p>
227259
*
228-
* <p>ConcurrentModificationExceptions will cause the transaction to repeat as many times as necessary to
229-
* finish the job. Work <b>MUST</b> idempotent.</p>
260+
* <p>Within {@code Work.run()}, obtain the correct transactional {@code Objectify} instance by calling
261+
* {@code ObjectifyService.ofy()}</p>
230262
*
231-
* <p>Within {@code Work.run()}, obtain the new transactional {@code Objectify} instance by calling {@code ObjectifyService.ofy()}</p>
263+
* <p>The transaction might retry if there is a concurrency failure; work must be idempotent.</p>
264+
*/
265+
<R> R transactNew(TxnOptions options, Work<R> work);
266+
267+
/**
268+
* <p>Executes work in a NEW transaction with the default options.</p>
232269
*
233-
* @param work defines the work to be done in a transaction. After the method exits, the transaction will commit.
234-
* @return the result of the work
270+
* @see Objectify#transactNew(TxnOptions, Work)
235271
*/
236-
<R> R transactNew(Work<R> work);
272+
default <R> R transactNew(final Work<R> work) {
273+
return transactNew(TxnOptions.deflt(), work);
274+
}
237275

238276
/**
239277
* Convenience method that doesn't force you to return something from your lambda.
@@ -246,16 +284,18 @@ default void transactNew(final Runnable work) {
246284
}
247285

248286
/**
249-
* <p>Executes the work in a new read-write transaction, repeating up to limitTries times when a ConcurrentModificationException
250-
* is thrown. This requires your Work to be idempotent; otherwise limit tries to 1.
287+
* <p>Executes the work in a new read-write transaction, trying up to limitTries times when a concurrency
288+
* failure happens.</p>
251289
*
252290
* <p>Within {@code Work.run()}, obtain the new transactional {@code Objectify} instance by calling {@code ObjectifyService.ofy()}</p>
253291
*
254292
* @param limitTries is the max # of tries. Must be > 0. A value of 1 means "try only once".
255293
* @param work defines the work to be done in a transaction. After the method exits, the transaction will commit.
256294
* @return the result of the work
257295
*/
258-
<R> R transactNew(int limitTries, Work<R> work);
296+
default <R> R transactNew(final int limitTries, final Work<R> work) {
297+
return transactNew(TxnOptions.deflt().limitTries(limitTries), work);
298+
}
259299

260300
/**
261301
* Convenience method that doesn't force you to return something from your lambda.
@@ -279,9 +319,11 @@ default void transactNew(int limitTries, Runnable work) {
279319
* <p>Within {@code Work.run()}, obtain the correct transactional {@code Objectify} instance by calling
280320
* {@code ObjectifyService.ofy()}</p>
281321
*
282-
* <p>Readonly transactions do not retry.</p>
322+
* <p>Readonly transactions do not have concurrency failures and do not retry.</p>
283323
*/
284-
<R> R transactReadOnly(Work<R> work);
324+
default <R> R transactReadOnly(final Work<R> work) {
325+
return transact(TxnOptions.deflt().readOnly(true), work);
326+
}
285327

286328
/**
287329
* Convenience method that doesn't force you to return something from your lambda.
@@ -313,10 +355,14 @@ default void transactReadOnly(final Runnable work) {
313355
<R> R execute(TxnType txnType, Work<R> work);
314356

315357
/**
316-
* <p>Exactly the same behavior as the Work version, but doesn't return anything. Convenient for Java8
317-
* so you don't have to return something from the lambda.</p>
358+
* Convenience method that doesn't force you to return something from your lambda.
318359
*/
319-
void execute(TxnType txnType, Runnable work);
360+
default void execute(final TxnType txnType, final Runnable work) {
361+
execute(txnType, (Work<Void>)() -> {
362+
work.run();
363+
return null;
364+
});
365+
}
320366

321367
/**
322368
* Synchronously flushes any deferred operations to the datastore. Objectify does this for you at the end
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.googlecode.objectify;
2+
3+
import com.google.common.base.Preconditions;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.Value;
6+
import lombok.experimental.Accessors;
7+
8+
import java.time.Instant;
9+
import java.util.Optional;
10+
11+
/**
12+
* <p>Options for a transaction. This deliberately has a record-like API, and someday when we baseline
13+
* a more modern java version will be converted to a record.</p>
14+
*
15+
* <p>This roughly approximates the protobuf TransactionOptions.</p>
16+
*/
17+
@Value
18+
@RequiredArgsConstructor
19+
@Accessors(fluent = true)
20+
public class TxnOptions {
21+
private static final TxnOptions DEFAULT = new TxnOptions();
22+
public static TxnOptions deflt() {
23+
return DEFAULT;
24+
}
25+
26+
/**
27+
* Sets the transaction as readOnly. Allows some optimization by the datastore, but attempts
28+
* to write data in the transaction will fail with a DatastoreException.
29+
*/
30+
boolean readOnly;
31+
32+
/**
33+
* <p>For viewing data in past history. Google's documentation says:</p>
34+
*
35+
* <p>This must be a microsecond precision timestamp within the past one hour, or
36+
* if Point-in-Time Recovery is enabled, can additionally be a whole minute timestamp
37+
* within the past 7 days.</p>
38+
*
39+
* <p>If set, readOnly must be true.</p>
40+
*/
41+
Optional<Instant> readTime;
42+
43+
/**
44+
* Number of tries that we may attempt on concurrency failure. Note that readOnly transactions
45+
* aren't retried (they should never have concurrency failures).
46+
*/
47+
int limitTries;
48+
49+
/**
50+
* Construct with default options.
51+
*/
52+
public TxnOptions() {
53+
this(false, Optional.empty(), 200);
54+
}
55+
56+
/** @return options that have the readOnly flag set to the specified value */
57+
public TxnOptions readOnly(final boolean readOnly) {
58+
return new TxnOptions(readOnly, readTime, limitTries);
59+
}
60+
61+
/**
62+
* Note that readOnly must be true for if this is enabled.
63+
*
64+
* @return options that have the readTime flag set to the specified value
65+
*/
66+
public TxnOptions readTime(final Optional<Instant> readTime) {
67+
return new TxnOptions(readOnly, readTime, limitTries);
68+
}
69+
70+
/**
71+
* Convenience method.
72+
*/
73+
public TxnOptions readTime(final Instant readTime) {
74+
return readTime(Optional.ofNullable(readTime));
75+
}
76+
77+
/**
78+
* Change the number of tries. Must be at least 1.
79+
*/
80+
public TxnOptions limitTries(final int limitTries) {
81+
Preconditions.checkArgument(limitTries >= 1, "limitTries must be at least 1");
82+
return new TxnOptions(readOnly, readTime, limitTries);
83+
}
84+
}

‎src/main/java/com/googlecode/objectify/cache/CachingAsyncDatastore.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.google.cloud.datastore.Key;
55
import com.google.cloud.datastore.ReadOption;
66
import com.google.protobuf.ByteString;
7+
import com.googlecode.objectify.TxnOptions;
78
import com.googlecode.objectify.cache.EntityMemcache.Bucket;
89
import com.googlecode.objectify.impl.AsyncDatastore;
910
import com.googlecode.objectify.impl.AsyncTransaction;
@@ -60,8 +61,8 @@ protected void empty(final Iterable<Key> keys) {
6061
}
6162

6263
@Override
63-
public AsyncTransaction newTransaction(final boolean readOnly, final Runnable afterCommit, Optional<ByteString> prevTxnHandle) {
64-
return new CachingAsyncTransaction(raw.newTransaction(readOnly, afterCommit, prevTxnHandle), memcache);
64+
public AsyncTransaction newTransaction(final TxnOptions options, final Runnable afterCommit, Optional<ByteString> prevTxnHandle) {
65+
return new CachingAsyncTransaction(raw.newTransaction(options, afterCommit, prevTxnHandle), memcache);
6566
}
6667

6768
@Override

‎src/main/java/com/googlecode/objectify/impl/AsyncDatastore.java‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.googlecode.objectify.impl;
22

33
import com.google.protobuf.ByteString;
4+
import com.googlecode.objectify.TxnOptions;
45

56
import java.util.Optional;
67

@@ -13,13 +14,13 @@ public interface AsyncDatastore extends AsyncDatastoreReaderWriter {
1314

1415
@Deprecated
1516
default AsyncTransaction newTransaction(Runnable afterCommit) {
16-
return newTransaction(false, afterCommit, Optional.empty());
17+
return newTransaction(TxnOptions.deflt(), afterCommit, Optional.empty());
1718
}
1819

1920
@Deprecated
2021
default AsyncTransaction newTransaction(Runnable afterCommit, ByteString prevTxnHandle) {
21-
return newTransaction(false, afterCommit, Optional.ofNullable(prevTxnHandle));
22+
return newTransaction(TxnOptions.deflt(), afterCommit, Optional.ofNullable(prevTxnHandle));
2223
}
2324

24-
AsyncTransaction newTransaction(boolean readOnly, Runnable afterCommit, Optional<ByteString> prevTxnHandle);
25+
AsyncTransaction newTransaction(TxnOptions options, Runnable afterCommit, Optional<ByteString> prevTxnHandle);
2526
}

‎src/main/java/com/googlecode/objectify/impl/AsyncDatastoreImpl.java‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.googlecode.objectify.impl;
22

33
import com.google.cloud.datastore.Datastore;
4+
import com.google.common.base.Preconditions;
45
import com.google.datastore.v1.TransactionOptions;
56
import com.google.datastore.v1.TransactionOptions.ReadOnly;
67
import com.google.protobuf.ByteString;
8+
import com.google.protobuf.Timestamp;
9+
import com.googlecode.objectify.TxnOptions;
710

811
import java.util.Optional;
912

@@ -19,23 +22,23 @@ public AsyncDatastoreImpl(final Datastore raw) {
1922
}
2023

2124
@Override
22-
public AsyncTransaction newTransaction(final boolean readOnly, final Runnable afterCommit, final Optional<ByteString> prevTxnHandle) {
25+
public AsyncTransaction newTransaction(final TxnOptions options, final Runnable afterCommit, final Optional<ByteString> prevTxnHandle) {
2326
final TransactionOptions.Builder txnOptions = TransactionOptions.newBuilder();
2427

25-
if (readOnly) {
26-
txnOptions.setReadOnly(ReadOnly.newBuilder().build());
27-
}
28+
if (options.readOnly()) {
29+
final ReadOnly.Builder builder = ReadOnly.newBuilder();
30+
options.readTime().ifPresent(time -> {
31+
builder.setReadTime(Timestamp.newBuilder().setSeconds(time.getEpochSecond()).setNanos(time.getNano()));
32+
});
33+
txnOptions.setReadOnly(builder.build());
34+
35+
} else {
36+
Preconditions.checkState(options.readTime().isEmpty(), "readOnly is required if readTime is set");
2837

29-
prevTxnHandle.ifPresent(handle -> {
30-
if (readOnly) {
31-
// setPreviousTransaction() doesn't exist on the readonly version, presumably because
32-
// readonly transactions don't retry.
33-
//txnOptions.getReadOnlyBuilder().setPreviousTransaction(handle);
34-
throw new IllegalStateException("This should be impossible; readonly transactions don't retry");
35-
} else {
38+
prevTxnHandle.ifPresent(handle -> {
3639
txnOptions.getReadWriteBuilder().setPreviousTransaction(handle);
37-
}
38-
});
40+
});
41+
}
3942

4043
return new AsyncTransactionImpl(datastore.newTransaction(txnOptions.build()), afterCommit);
4144
}

‎src/main/java/com/googlecode/objectify/impl/ObjectifyImpl.java‎

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.googlecode.objectify.Key;
88
import com.googlecode.objectify.Objectify;
99
import com.googlecode.objectify.ObjectifyFactory;
10+
import com.googlecode.objectify.TxnOptions;
1011
import com.googlecode.objectify.TxnType;
1112
import com.googlecode.objectify.Work;
1213
import com.googlecode.objectify.annotation.Entity;
@@ -127,53 +128,19 @@ public <R> R execute(final TxnType txnType, final Work<R> work) {
127128
return transactor.execute(this, txnType, work);
128129
}
129130

130-
@Override
131-
public void execute(final TxnType txnType, final Runnable work) {
132-
execute(txnType, (Work<Void>)() -> {
133-
work.run();
134-
return null;
135-
});
136-
}
137-
138131
@Override
139132
public <R> R transactionless(final Work<R> work) {
140133
return transactor.transactionless(this, work);
141134
}
142135

143136
@Override
144-
public void transactionless(final Runnable work) {
145-
transactionless((Work<Void>)() -> {
146-
work.run();
147-
return null;
148-
});
149-
}
150-
151-
@Override
152-
public <R> R transact(Work<R> work) {
153-
return transactor.transact(this, work);
154-
}
155-
156-
@Override
157-
public <R> R transactReadOnly(Work<R> work) {
158-
return transactor.transactReadOnly(this, work);
159-
}
160-
161-
@Override
162-
public void transactReadOnly(final Runnable work) {
163-
transactReadOnly((Work<Void>)() -> {
164-
work.run();
165-
return null;
166-
});
167-
}
168-
169-
@Override
170-
public <R> R transactNew(Work<R> work) {
171-
return this.transactNew(Transactor.DEFAULT_TRY_LIMIT, work);
137+
public <R> R transact(final TxnOptions options, final Work<R> work) {
138+
return transactor.transact(this, options, work);
172139
}
173140

174141
@Override
175-
public <R> R transactNew(int limitTries, Work<R> work) {
176-
return transactor.transactNew(this, limitTries, work);
142+
public <R> R transactNew(final TxnOptions options, Work<R> work) {
143+
return transactor.transactNew(this, options, work);
177144
}
178145

179146
@Override

0 commit comments

Comments
 (0)