22
33import static com .google .common .base .Preconditions .checkNotNull ;
44
5+ import com .google .common .annotations .VisibleForTesting ;
56import com .google .errorprone .annotations .concurrent .LazyInit ;
7+ import com .scalar .db .api .Delete ;
68import com .scalar .db .api .DistributedStorage ;
79import com .scalar .db .api .Mutation ;
810import com .scalar .db .api .TransactionState ;
2426import java .util .List ;
2527import java .util .Optional ;
2628import java .util .concurrent .Future ;
29+ import java .util .stream .Collectors ;
30+ import java .util .stream .Stream ;
2731import javax .annotation .Nullable ;
2832import javax .annotation .concurrent .ThreadSafe ;
2933import org .slf4j .Logger ;
@@ -38,6 +42,7 @@ public class CommitHandler {
3842 private final ParallelExecutor parallelExecutor ;
3943 private final MutationsGrouper mutationsGrouper ;
4044 protected final boolean coordinatorWriteOmissionOnReadOnlyEnabled ;
45+ private final boolean onePhaseCommitEnabled ;
4146
4247 @ LazyInit @ Nullable private BeforePreparationSnapshotHook beforePreparationSnapshotHook ;
4348
@@ -48,13 +53,15 @@ public CommitHandler(
4853 TransactionTableMetadataManager tableMetadataManager ,
4954 ParallelExecutor parallelExecutor ,
5055 MutationsGrouper mutationsGrouper ,
51- boolean coordinatorWriteOmissionOnReadOnlyEnabled ) {
56+ boolean coordinatorWriteOmissionOnReadOnlyEnabled ,
57+ boolean onePhaseCommitEnabled ) {
5258 this .storage = checkNotNull (storage );
5359 this .coordinator = checkNotNull (coordinator );
5460 this .tableMetadataManager = checkNotNull (tableMetadataManager );
5561 this .parallelExecutor = checkNotNull (parallelExecutor );
5662 this .mutationsGrouper = checkNotNull (mutationsGrouper );
5763 this .coordinatorWriteOmissionOnReadOnlyEnabled = coordinatorWriteOmissionOnReadOnlyEnabled ;
64+ this .onePhaseCommitEnabled = onePhaseCommitEnabled ;
5865 }
5966
6067 /**
@@ -118,6 +125,16 @@ public void commit(Snapshot snapshot, boolean readOnly)
118125
119126 Optional <Future <Void >> snapshotHookFuture = invokeBeforePreparationSnapshotHook (snapshot );
120127
128+ if (canOnePhaseCommit (snapshot )) {
129+ try {
130+ onePhaseCommitRecords (snapshot );
131+ return ;
132+ } catch (Exception e ) {
133+ safelyCallOnFailureBeforeCommit (snapshot );
134+ throw e ;
135+ }
136+ }
137+
121138 if (hasWritesOrDeletesInSnapshot ) {
122139 try {
123140 prepareRecords (snapshot );
@@ -170,6 +187,52 @@ public void commit(Snapshot snapshot, boolean readOnly)
170187 }
171188 }
172189
190+ @ VisibleForTesting
191+ boolean canOnePhaseCommit (Snapshot snapshot ) throws CommitException {
192+ if (!onePhaseCommitEnabled ) {
193+ return false ;
194+ }
195+
196+ // If validation is required (in SERIALIZABLE isolation), we cannot one-phase commit the
197+ // transaction
198+ if (snapshot .isValidationRequired ()) {
199+ return false ;
200+ }
201+
202+ // If the snapshot has no write and deletes, we do not one-phase commit the transaction
203+ if (!snapshot .hasWritesOrDeletes ()) {
204+ return false ;
205+ }
206+
207+ List <Delete > deletesInDeleteSet = snapshot .getDeletesInDeleteSet ();
208+
209+ // If a record corresponding to a delete in the delete set does not exist in the storage, we
210+ // cannot one-phase commit the transaction. This is because the storage does not support
211+ // delete-if-not-exists semantics, so we cannot detect conflicts with other transactions.
212+ for (Delete delete : deletesInDeleteSet ) {
213+ Optional <TransactionResult > result = snapshot .getFromReadSet (new Snapshot .Key (delete ));
214+
215+ // For deletes, we always perform implicit pre-reads if the result does not exit in the read
216+ // set. So the result should always exist in the read set.
217+ assert result != null ;
218+
219+ if (!result .isPresent ()) {
220+ return false ;
221+ }
222+ }
223+
224+ try {
225+ // If the mutations can be grouped altogether, the mutations can be done in a single mutate
226+ // API call, so we can one-phase commit the transaction
227+ return mutationsGrouper .canBeGroupedAltogether (
228+ Stream .concat (snapshot .getPutsInWriteSet ().stream (), deletesInDeleteSet .stream ())
229+ .collect (Collectors .toList ()));
230+ } catch (ExecutionException e ) {
231+ throw new CommitException (
232+ CoreError .CONSENSUS_COMMIT_COMMITTING_RECORDS_FAILED .buildMessage (), e , snapshot .getId ());
233+ }
234+ }
235+
173236 protected void handleCommitConflict (Snapshot snapshot , Exception cause )
174237 throws CommitConflictException , UnknownTransactionStatusException {
175238 try {
@@ -197,6 +260,30 @@ protected void handleCommitConflict(Snapshot snapshot, Exception cause)
197260 }
198261 }
199262
263+ @ VisibleForTesting
264+ void onePhaseCommitRecords (Snapshot snapshot ) throws CommitException {
265+ try {
266+ OnePhaseCommitMutationComposer composer =
267+ new OnePhaseCommitMutationComposer (snapshot .getId (), tableMetadataManager );
268+ snapshot .to (composer );
269+
270+ // One-phase commit does not require grouping mutations and using the parallel executor since
271+ // it is always executed in a single mutate API call.
272+ storage .mutate (composer .get ());
273+ } catch (NoMutationException e ) {
274+ throw new CommitConflictException (
275+ CoreError .CONSENSUS_COMMIT_PREPARING_RECORD_EXISTS .buildMessage (), e , snapshot .getId ());
276+ } catch (RetriableExecutionException e ) {
277+ throw new CommitConflictException (
278+ CoreError .CONSENSUS_COMMIT_CONFLICT_OCCURRED_WHEN_COMMITTING_RECORDS .buildMessage (),
279+ e ,
280+ snapshot .getId ());
281+ } catch (ExecutionException e ) {
282+ throw new CommitException (
283+ CoreError .CONSENSUS_COMMIT_COMMITTING_RECORDS_FAILED .buildMessage (), e , snapshot .getId ());
284+ }
285+ }
286+
200287 public void prepareRecords (Snapshot snapshot ) throws PreparationException {
201288 try {
202289 PrepareMutationComposer composer =
0 commit comments