Skip to content

Commit 7d6816f

Browse files
authored
feat: add bufferAsync methods (#1145)
* feat: add bufferAsync methods Adds bufferAsync methods to TransactionContext. The existing buffer methods were already non-blocking, but the async versions also return an ApiFuture, which make them easier to use when chaining multiple async calls together. Also changes some calls in the AsyncTransactionManagerTest to use lambdas instead of the test helper methods. Fixes #1126 * fix: do not take lock on async method * build: remove custom skip tests variable * test: add test for committing twice * fix: synchronize buffering and committing
1 parent e70b009 commit 7d6816f

File tree

9 files changed

+574
-326
lines changed

9 files changed

+574
-326
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,4 +605,17 @@
605605
<className>com/google/cloud/spanner/StructReader</className>
606606
<method>com.google.cloud.spanner.Value getValue(java.lang.String)</method>
607607
</difference>
608+
609+
<!-- Adds bufferAsync to DatabaseClient -->
610+
<!-- These are not breaking changes, since we provide default interface implementation -->
611+
<difference>
612+
<differenceType>7012</differenceType>
613+
<className>com/google/cloud/spanner/TransactionContext</className>
614+
<method>com.google.api.core.ApiFuture bufferAsync(com.google.cloud.spanner.Mutation)</method>
615+
</difference>
616+
<difference>
617+
<differenceType>7012</differenceType>
618+
<className>com/google/cloud/spanner/TransactionContext</className>
619+
<method>com.google.api.core.ApiFuture bufferAsync(java.lang.Iterable)</method>
620+
</difference>
608621
</differences>

google-cloud-spanner/src/main/java/com/google/cloud/spanner/AsyncTransactionManager.java

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

1919
import com.google.api.core.ApiFuture;
2020
import com.google.cloud.Timestamp;
21+
import com.google.cloud.spanner.AsyncTransactionManager.TransactionContextFuture;
2122
import com.google.cloud.spanner.TransactionManager.TransactionState;
2223
import com.google.common.util.concurrent.ListenableFuture;
2324
import com.google.common.util.concurrent.MoreExecutors;
@@ -98,31 +99,21 @@ Timestamp get(long timeout, TimeUnit unit)
9899
* <p>Example usage:
99100
*
100101
* <pre>{@code
101-
* TransactionContextFuture txnFuture = manager.beginAsync();
102102
* final String column = "FirstName";
103-
* txnFuture.then(
104-
* new AsyncTransactionFunction<Void, Struct>() {
105-
* @Override
106-
* public ApiFuture<Struct> apply(TransactionContext txn, Void input)
107-
* throws Exception {
108-
* return txn.readRowAsync(
109-
* "Singers", Key.of(singerId), Collections.singleton(column));
110-
* }
111-
* })
112-
* .then(
113-
* new AsyncTransactionFunction<Struct, Void>() {
114-
* @Override
115-
* public ApiFuture<Void> apply(TransactionContext txn, Struct input)
116-
* throws Exception {
117-
* String name = input.getString(column);
118-
* txn.buffer(
119-
* Mutation.newUpdateBuilder("Singers")
120-
* .set(column)
121-
* .to(name.toUpperCase())
122-
* .build());
123-
* return ApiFutures.immediateFuture(null);
124-
* }
125-
* })
103+
* final long singerId = 1L;
104+
* AsyncTransactionManager manager = client.transactionManagerAsync();
105+
* TransactionContextFuture txnFuture = manager.beginAsync();
106+
* txnFuture
107+
* .then((transaction, ignored) ->
108+
* transaction.readRowAsync("Singers", Key.of(singerId), Collections.singleton(column)),
109+
* executor)
110+
* .then((transaction, row) ->
111+
* transaction.bufferAsync(
112+
* Mutation.newUpdateBuilder("Singers")
113+
* .set(column).to(row.getString(column).toUpperCase())
114+
* .build()),
115+
* executor)
116+
* .commitAsync();
126117
* }</pre>
127118
*/
128119
interface AsyncTransactionStep<I, O> extends ApiFuture<O> {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClient.java

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -431,8 +431,7 @@ CommitResponse writeAtLeastOnceWithOptions(
431431
* lifecycle. This API is meant for advanced users. Most users should instead use the {@link
432432
* #runAsync()} API instead.
433433
*
434-
* <p>Example of using {@link AsyncTransactionManager} with lambda expressions (Java 8 and
435-
* higher).
434+
* <p>Example of using {@link AsyncTransactionManager}.
436435
*
437436
* <pre>{@code
438437
* long singerId = 1L;
@@ -449,56 +448,11 @@ CommitResponse writeAtLeastOnceWithOptions(
449448
* .then(
450449
* (transaction, row) -> {
451450
* String name = row.getString(column);
452-
* transaction.buffer(
451+
* return transaction.bufferAsync(
453452
* Mutation.newUpdateBuilder("Singers")
454453
* .set(column)
455454
* .to(name.toUpperCase())
456455
* .build());
457-
* return ApiFutures.immediateFuture(null);
458-
* })
459-
* .commitAsync();
460-
* try {
461-
* commitTimestamp.get();
462-
* break;
463-
* } catch (AbortedException e) {
464-
* Thread.sleep(e.getRetryDelayInMillis());
465-
* transactionFuture = manager.resetForRetryAsync();
466-
* }
467-
* }
468-
* }
469-
* }</pre>
470-
*
471-
* <p>Example of using {@link AsyncTransactionManager} (Java 7).
472-
*
473-
* <pre>{@code
474-
* final long singerId = 1L;
475-
* try (AsyncTransactionManager manager = client().transactionManagerAsync()) {
476-
* TransactionContextFuture transactionFuture = manager.beginAsync();
477-
* while (true) {
478-
* final String column = "FirstName";
479-
* CommitTimestampFuture commitTimestamp =
480-
* transactionFuture.then(
481-
* new AsyncTransactionFunction<Void, Struct>() {
482-
* @Override
483-
* public ApiFuture<Struct> apply(TransactionContext transaction, Void input)
484-
* throws Exception {
485-
* return transaction.readRowAsync(
486-
* "Singers", Key.of(singerId), Collections.singleton(column));
487-
* }
488-
* })
489-
* .then(
490-
* new AsyncTransactionFunction<Struct, Void>() {
491-
* @Override
492-
* public ApiFuture<Void> apply(TransactionContext transaction, Struct input)
493-
* throws Exception {
494-
* String name = input.getString(column);
495-
* transaction.buffer(
496-
* Mutation.newUpdateBuilder("Singers")
497-
* .set(column)
498-
* .to(name.toUpperCase())
499-
* .build());
500-
* return ApiFutures.immediateFuture(null);
501-
* }
502456
* })
503457
* .commitAsync();
504458
* try {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionPool.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,11 @@ public void buffer(Mutation mutation) {
675675
delegate.buffer(mutation);
676676
}
677677

678+
@Override
679+
public ApiFuture<Void> bufferAsync(Mutation mutation) {
680+
return delegate.bufferAsync(mutation);
681+
}
682+
678683
@Override
679684
public Struct readRowUsingIndex(String table, String index, Key key, Iterable<String> columns) {
680685
try {
@@ -703,6 +708,11 @@ public void buffer(Iterable<Mutation> mutations) {
703708
delegate.buffer(mutations);
704709
}
705710

711+
@Override
712+
public ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
713+
return delegate.bufferAsync(mutations);
714+
}
715+
706716
@Override
707717
public long executeUpdate(Statement statement, UpdateOption... options) {
708718
try {

google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionContext.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,23 @@ public interface TransactionContext extends ReadContext {
9191
*/
9292
void buffer(Mutation mutation);
9393

94+
/** Same as {@link #buffer(Mutation)}, but is guaranteed to be non-blocking. */
95+
default ApiFuture<Void> bufferAsync(Mutation mutation) {
96+
throw new UnsupportedOperationException("method should be overwritten");
97+
}
98+
9499
/**
95100
* Buffers mutations to be applied if the transaction commits successfully. The effects of the
96101
* mutations will not be visible to subsequent operations in the transaction. All buffered
97102
* mutations will be applied atomically.
98103
*/
99104
void buffer(Iterable<Mutation> mutations);
100105

106+
/** Same as {@link #buffer(Iterable)}, but is guaranteed to be non-blocking. */
107+
default ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
108+
throw new UnsupportedOperationException("method should be overwritten");
109+
}
110+
101111
/**
102112
* Executes the DML statement(s) and returns the number of rows modified. For non-DML statements,
103113
* it will result in an {@code IllegalArgumentException}. The effects of the DML statement will be

google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@
5454
import io.opencensus.trace.Tracing;
5555
import java.util.ArrayList;
5656
import java.util.List;
57+
import java.util.Queue;
5758
import java.util.concurrent.Callable;
59+
import java.util.concurrent.ConcurrentLinkedQueue;
5860
import java.util.concurrent.ExecutionException;
5961
import java.util.concurrent.Executor;
6062
import java.util.concurrent.TimeUnit;
@@ -75,6 +77,9 @@ class TransactionRunnerImpl implements SessionTransaction, TransactionRunner {
7577
*/
7678
private static final String TRANSACTION_CANCELLED_MESSAGE = "invalidated by a later transaction";
7779

80+
private static final String TRANSACTION_ALREADY_COMMITTED_MESSAGE =
81+
"Transaction has already committed";
82+
7883
@VisibleForTesting
7984
static class TransactionContextImpl extends AbstractReadContext implements TransactionContext {
8085
static class Builder extends AbstractReadContext.Builder<Builder, TransactionContextImpl> {
@@ -146,7 +151,9 @@ public void removeListener(Runnable listener) {
146151
}
147152
}
148153

149-
@GuardedBy("lock")
154+
private final Object committingLock = new Object();
155+
156+
@GuardedBy("committingLock")
150157
private volatile boolean committing;
151158

152159
@GuardedBy("lock")
@@ -155,8 +162,7 @@ public void removeListener(Runnable listener) {
155162
@GuardedBy("lock")
156163
private volatile int runningAsyncOperations;
157164

158-
@GuardedBy("lock")
159-
private List<Mutation> mutations = new ArrayList<>();
165+
private final Queue<Mutation> mutations = new ConcurrentLinkedQueue<>();
160166

161167
@GuardedBy("lock")
162168
private boolean aborted;
@@ -280,6 +286,16 @@ void commit() {
280286
volatile ApiFuture<CommitResponse> commitFuture;
281287

282288
ApiFuture<CommitResponse> commitAsync() {
289+
List<com.google.spanner.v1.Mutation> mutationsProto = new ArrayList<>();
290+
synchronized (committingLock) {
291+
if (committing) {
292+
throw new IllegalStateException(TRANSACTION_ALREADY_COMMITTED_MESSAGE);
293+
}
294+
committing = true;
295+
if (!mutations.isEmpty()) {
296+
Mutation.toProto(mutations, mutationsProto);
297+
}
298+
}
283299
final SettableApiFuture<CommitResponse> res = SettableApiFuture.create();
284300
final SettableApiFuture<Void> finishOps;
285301
CommitRequest.Builder builder =
@@ -303,14 +319,8 @@ ApiFuture<CommitResponse> commitAsync() {
303319
} else {
304320
finishOps = finishedAsyncOperations;
305321
}
306-
if (!mutations.isEmpty()) {
307-
List<com.google.spanner.v1.Mutation> mutationsProto = new ArrayList<>();
308-
Mutation.toProto(mutations, mutationsProto);
309-
builder.addAllMutations(mutationsProto);
310-
}
311-
// Ensure that no call to buffer mutations that would be lost can succeed.
312-
mutations = null;
313322
}
323+
builder.addAllMutations(mutationsProto);
314324
finishOps.addListener(
315325
new CommitRunnable(res, finishOps, builder), MoreExecutors.directExecutor());
316326
return res;
@@ -603,22 +613,44 @@ public void onDone(boolean withBeginTransaction) {
603613

604614
@Override
605615
public void buffer(Mutation mutation) {
606-
synchronized (lock) {
607-
checkNotNull(mutations, "Context is closed");
616+
synchronized (committingLock) {
617+
if (committing) {
618+
throw new IllegalStateException(TRANSACTION_ALREADY_COMMITTED_MESSAGE);
619+
}
608620
mutations.add(checkNotNull(mutation));
609621
}
610622
}
611623

624+
@Override
625+
public ApiFuture<Void> bufferAsync(Mutation mutation) {
626+
// Normally, we would call the async method from the sync method, but this is also safe as
627+
// both are non-blocking anyways, and this prevents the creation of an ApiFuture that is not
628+
// really used when the sync method is called.
629+
buffer(mutation);
630+
return ApiFutures.immediateFuture(null);
631+
}
632+
612633
@Override
613634
public void buffer(Iterable<Mutation> mutations) {
614-
synchronized (lock) {
615-
checkNotNull(this.mutations, "Context is closed");
635+
synchronized (committingLock) {
636+
if (committing) {
637+
throw new IllegalStateException(TRANSACTION_ALREADY_COMMITTED_MESSAGE);
638+
}
616639
for (Mutation mutation : mutations) {
617640
this.mutations.add(checkNotNull(mutation));
618641
}
619642
}
620643
}
621644

645+
@Override
646+
public ApiFuture<Void> bufferAsync(Iterable<Mutation> mutations) {
647+
// Normally, we would call the async method from the sync method, but this is also safe as
648+
// both are non-blocking anyways, and this prevents the creation of an ApiFuture that is not
649+
// really used when the sync method is called.
650+
buffer(mutations);
651+
return ApiFutures.immediateFuture(null);
652+
}
653+
622654
@Override
623655
public long executeUpdate(Statement statement, UpdateOption... options) {
624656
beforeReadOrQuery();

0 commit comments

Comments
 (0)