Skip to content

Commit 87180d7

Browse files
Backport to branch(3) : Optimize Coordinator#getStateForGroupCommit() by first looking up the coordinator table with parent transaction ID (#2338)
Co-authored-by: Mitsunori Komatsu <[email protected]>
1 parent a185244 commit 87180d7

File tree

2 files changed

+78
-62
lines changed

2 files changed

+78
-62
lines changed

core/src/main/java/com/scalar/db/transaction/consensuscommit/Coordinator.java

Lines changed: 19 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -90,41 +90,30 @@ public Optional<Coordinator.State> getState(String id) throws CoordinatorExcepti
9090

9191
@VisibleForTesting
9292
Optional<Coordinator.State> getStateForGroupCommit(String fullId) throws CoordinatorException {
93-
// Reading a coordinator state is likely to occur during lazy recovery, as follows:
94-
// 1. Transaction T1 starts and creates PREPARED state records but hasn't committed or aborted
95-
// yet.
96-
// 2. Transaction T2 starts and reads the PREPARED state records created by T1.
97-
// 3. T2 reads the coordinator table record for T1 to decide whether to roll back or roll
98-
// forward.
99-
//
100-
// The likelihood of step 2 would increase if T1 is delayed.
101-
//
102-
// With the group commit feature enabled, delayed transactions are isolated from a normal group
103-
// that is looked up by a parent ID into a delayed group that is looked up by a full ID.
104-
// Therefore, looking up with the full transaction ID should be tried first to minimize read
105-
// operations as much as possible.
106-
107-
// Scan with the full ID for a delayed group that contains only a single transaction.
108-
// The normal lookup logic can be used as is.
109-
Optional<State> stateOfDelayedTxn = get(createGetWith(fullId));
110-
if (stateOfDelayedTxn.isPresent()) {
111-
return stateOfDelayedTxn;
112-
}
113-
11493
// Scan with the parent ID for a normal group that contains multiple transactions.
11594
Keys<String, String, String> idForGroupCommit = keyManipulator.keysFromFullKey(fullId);
11695

11796
String parentId = idForGroupCommit.parentKey;
11897
String childId = idForGroupCommit.childKey;
11998
Get get = createGetWith(parentId);
12099
Optional<State> state = get(get);
121-
return state.flatMap(
122-
s -> {
123-
if (s.getChildIds().contains(childId)) {
124-
return state;
125-
}
126-
return Optional.empty();
127-
});
100+
// The current implementation is optimized for cases where most transactions are
101+
// group-committed. It first looks up a transaction state using the parent ID with a single read
102+
// operation. If no matching transaction state is found (i.e., the transaction was delayed and
103+
// committed individually), it issues an additional read operation using the full ID.
104+
Optional<State> stateContainingTargetTxId =
105+
state.flatMap(
106+
s -> {
107+
if (s.getChildIds().contains(childId)) {
108+
return state;
109+
}
110+
return Optional.empty();
111+
});
112+
if (stateContainingTargetTxId.isPresent()) {
113+
return stateContainingTargetTxId;
114+
}
115+
116+
return get(createGetWith(fullId));
128117
}
129118

130119
public void putState(Coordinator.State state) throws CoordinatorException {
@@ -253,7 +242,8 @@ private void putStateForLazyRecoveryRollbackForGroupCommit(String id)
253242
putState(new Coordinator.State(id, TransactionState.ABORTED));
254243
}
255244

256-
private Get createGetWith(String id) {
245+
@VisibleForTesting
246+
Get createGetWith(String id) {
257247
return new Get(new Key(Attribute.toIdValue(id)))
258248
.withConsistency(Consistency.LINEARIZABLE)
259249
.forNamespace(coordinatorNamespace)

core/src/test/java/com/scalar/db/transaction/consensuscommit/CoordinatorTest.java

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -264,19 +264,9 @@ public void getState_TransactionIdForGroupCommitGivenAndParentIdAndChildIdMatch_
264264
// The IDs used to find the state are:
265265
// - parentId:childId1
266266
// - parentId:childId2
267-
doReturn(
268-
// For the first call,
269-
// - The first get with the full ID shouldn't find a state.
270-
Optional.empty(),
271-
// - The second get with the parent ID should return the state.
272-
Optional.of(resultForGroupCommitState),
273-
// For the second call,
274-
// - The first get with the full ID shouldn't find a state.
275-
Optional.empty(),
276-
// - The second get with the parent ID should return the state.
277-
Optional.of(resultForGroupCommitState))
267+
doReturn(Optional.of(resultForGroupCommitState))
278268
.when(storage)
279-
.get(any(Get.class));
269+
.get(coordinator.createGetWith(parentId));
280270

281271
// Act
282272
Optional<Coordinator.State> state1 = spiedCoordinator.getState(fullId1);
@@ -291,9 +281,9 @@ public void getState_TransactionIdForGroupCommitGivenAndParentIdAndChildIdMatch_
291281
assertThat(state1.get().getCreatedAt()).isEqualTo(ANY_TIME_1);
292282
verify(spiedCoordinator).getStateForGroupCommit(fullId1);
293283
verify(spiedCoordinator).getStateForGroupCommit(fullId2);
294-
verify(storage, times(4)).get(getArgumentCaptor.capture());
284+
verify(storage, times(2)).get(getArgumentCaptor.capture());
295285
assertGetArgumentCaptorForGetState(
296-
getArgumentCaptor.getAllValues(), Arrays.asList(fullId1, parentId, fullId2, parentId));
286+
getArgumentCaptor.getAllValues(), Arrays.asList(parentId, parentId));
297287
}
298288

299289
@ParameterizedTest
@@ -310,6 +300,20 @@ public void getState_TransactionIdForSingleCommitGivenAndFullIdMatches_ShouldRet
310300
String childId = UUID.randomUUID().toString();
311301
String fullId = keyManipulator.fullKey(parentId, childId);
312302
List<String> childIds = Collections.emptyList();
303+
String dummyChildId1 = UUID.randomUUID().toString();
304+
String dummyChildId2 = UUID.randomUUID().toString();
305+
List<String> dummyChildIds = Arrays.asList(dummyChildId1, dummyChildId2);
306+
307+
Result resultForGroupCommitState = mock(Result.class);
308+
when(resultForGroupCommitState.getValue(Attribute.ID))
309+
.thenReturn(Optional.of(new TextValue(Attribute.ID, parentId)));
310+
when(resultForGroupCommitState.getValue(Attribute.CHILD_IDS))
311+
.thenReturn(
312+
Optional.of(new TextValue(Attribute.CHILD_IDS, Joiner.on(',').join(dummyChildIds))));
313+
when(resultForGroupCommitState.getValue(Attribute.STATE))
314+
.thenReturn(Optional.of(new IntValue(Attribute.STATE, transactionState.get())));
315+
when(resultForGroupCommitState.getValue(Attribute.CREATED_AT))
316+
.thenReturn(Optional.of(new BigIntValue(Attribute.CREATED_AT, ANY_TIME_1)));
313317

314318
Result resultForSingleCommitState = mock(Result.class);
315319
when(resultForSingleCommitState.getValue(Attribute.ID))
@@ -323,13 +327,18 @@ public void getState_TransactionIdForSingleCommitGivenAndFullIdMatches_ShouldRet
323327

324328
// Assuming these states exist:
325329
//
326-
// id | child_ids | state
327-
// ------------------+-----------+----------
328-
// parentId:childId | [] | COMMITTED
330+
// id | child_ids | state
331+
// ------------------+----------------------+----------
332+
// parentId:childId | [childId1, childId2] | COMMITTED
329333
//
330334
// The IDs used to find the state are:
331335
// - parentId:childId
332-
doReturn(Optional.of(resultForSingleCommitState)).when(storage).get(any(Get.class));
336+
doReturn(Optional.of(resultForGroupCommitState))
337+
.when(storage)
338+
.get(coordinator.createGetWith(parentId));
339+
doReturn(Optional.of(resultForSingleCommitState))
340+
.when(storage)
341+
.get(coordinator.createGetWith(fullId));
333342

334343
// Act
335344
Optional<Coordinator.State> state = spiedCoordinator.getState(fullId);
@@ -341,9 +350,9 @@ public void getState_TransactionIdForSingleCommitGivenAndFullIdMatches_ShouldRet
341350
Assertions.assertThat(state.get().getState()).isEqualTo(transactionState);
342351
assertThat(state.get().getCreatedAt()).isEqualTo(ANY_TIME_1);
343352
verify(spiedCoordinator).getStateForGroupCommit(fullId);
344-
verify(storage).get(getArgumentCaptor.capture());
353+
verify(storage, times(2)).get(getArgumentCaptor.capture());
345354
assertGetArgumentCaptorForGetState(
346-
getArgumentCaptor.getAllValues(), Collections.singletonList(fullId));
355+
getArgumentCaptor.getAllValues(), Arrays.asList(parentId, fullId));
347356
}
348357

349358
@ParameterizedTest
@@ -381,14 +390,11 @@ public void getState_TransactionIdForGroupCommitGivenAndOnlyParentIdMatches_Shou
381390
//
382391
// The IDs used to find the state are:
383392
// - parentId:childIdX
384-
doReturn(
385-
// The first get with the full ID should return empty.
386-
Optional.empty(),
387-
// The second get with the parent ID should return a state, but it doesn't contain the
388-
// child ID.
389-
Optional.of(resultForGroupCommitState))
393+
doReturn(Optional.of(resultForGroupCommitState))
390394
.when(storage)
391-
.get(any(Get.class));
395+
.get(coordinator.createGetWith(parentId));
396+
397+
doReturn(Optional.empty()).when(storage).get(coordinator.createGetWith(targetFullId));
392398

393399
// Act
394400
Optional<Coordinator.State> state = spiedCoordinator.getState(targetFullId);
@@ -398,7 +404,7 @@ public void getState_TransactionIdForGroupCommitGivenAndOnlyParentIdMatches_Shou
398404
verify(spiedCoordinator).getStateForGroupCommit(targetFullId);
399405
verify(storage, times(2)).get(getArgumentCaptor.capture());
400406
assertGetArgumentCaptorForGetState(
401-
getArgumentCaptor.getAllValues(), Arrays.asList(targetFullId, parentId));
407+
getArgumentCaptor.getAllValues(), Arrays.asList(parentId, targetFullId));
402408
}
403409

404410
@ParameterizedTest
@@ -413,11 +419,23 @@ public void getState_TransactionIdForGroupCommitGivenAndOnlyParentIdMatches_Shou
413419
CoordinatorGroupCommitKeyManipulator keyManipulator =
414420
new CoordinatorGroupCommitKeyManipulator();
415421
String parentId = keyManipulator.generateParentKey();
422+
List<String> childIds =
423+
Arrays.asList(UUID.randomUUID().toString(), UUID.randomUUID().toString());
416424

417425
// Look up with the same parent ID and a wrong child ID.
418426
// But the full ID matches the single committed state.
419427
String targetFullId = keyManipulator.fullKey(parentId, UUID.randomUUID().toString());
420428

429+
Result resultForGroupCommitState = mock(Result.class);
430+
when(resultForGroupCommitState.getValue(Attribute.ID))
431+
.thenReturn(Optional.of(new TextValue(Attribute.ID, parentId)));
432+
when(resultForGroupCommitState.getValue(Attribute.CHILD_IDS))
433+
.thenReturn(Optional.of(new TextValue(Attribute.CHILD_IDS, Joiner.on(',').join(childIds))));
434+
when(resultForGroupCommitState.getValue(Attribute.STATE))
435+
.thenReturn(Optional.of(new IntValue(Attribute.STATE, transactionState.get())));
436+
when(resultForGroupCommitState.getValue(Attribute.CREATED_AT))
437+
.thenReturn(Optional.of(new BigIntValue(Attribute.CREATED_AT, ANY_TIME_1)));
438+
421439
Result resultForSingleCommitState = mock(Result.class);
422440
when(resultForSingleCommitState.getValue(Attribute.ID))
423441
.thenReturn(Optional.of(new TextValue(Attribute.ID, targetFullId)));
@@ -437,7 +455,12 @@ public void getState_TransactionIdForGroupCommitGivenAndOnlyParentIdMatches_Shou
437455
//
438456
// The IDs used to find the state are:
439457
// - parentId:childIdX
440-
doReturn(Optional.of(resultForSingleCommitState)).when(storage).get(any(Get.class));
458+
doReturn(Optional.of(resultForGroupCommitState))
459+
.when(storage)
460+
.get(coordinator.createGetWith(parentId));
461+
doReturn(Optional.of(resultForSingleCommitState))
462+
.when(storage)
463+
.get(coordinator.createGetWith(targetFullId));
441464

442465
// Act
443466
Optional<Coordinator.State> state = spiedCoordinator.getState(targetFullId);
@@ -449,9 +472,9 @@ public void getState_TransactionIdForGroupCommitGivenAndOnlyParentIdMatches_Shou
449472
Assertions.assertThat(state.get().getState()).isEqualTo(transactionState);
450473
assertThat(state.get().getCreatedAt()).isEqualTo(ANY_TIME_1);
451474
verify(spiedCoordinator).getStateForGroupCommit(targetFullId);
452-
verify(storage).get(getArgumentCaptor.capture());
475+
verify(storage, times(2)).get(getArgumentCaptor.capture());
453476
assertGetArgumentCaptorForGetState(
454-
getArgumentCaptor.getAllValues(), Collections.singletonList(targetFullId));
477+
getArgumentCaptor.getAllValues(), Arrays.asList(parentId, targetFullId));
455478
}
456479

457480
@ParameterizedTest
@@ -491,7 +514,10 @@ public void getState_TransactionIdGivenButNoIdMatches_ShouldReturnEmpty(
491514
//
492515
// The IDs used to find the state are:
493516
// - parentId:childIdY
494-
when(storage.get(any(Get.class))).thenReturn(Optional.empty());
517+
doReturn(Optional.of(resultForGroupCommitState))
518+
.when(storage)
519+
.get(coordinator.createGetWith(parentId));
520+
doReturn(Optional.empty()).when(storage).get(coordinator.createGetWith(targetFullId));
495521

496522
// Act
497523
Optional<Coordinator.State> state = spiedCoordinator.getState(targetFullId);
@@ -501,7 +527,7 @@ public void getState_TransactionIdGivenButNoIdMatches_ShouldReturnEmpty(
501527
verify(spiedCoordinator).getStateForGroupCommit(targetFullId);
502528
verify(storage, times(2)).get(getArgumentCaptor.capture());
503529
assertGetArgumentCaptorForGetState(
504-
getArgumentCaptor.getAllValues(), Arrays.asList(targetFullId, parentId));
530+
getArgumentCaptor.getAllValues(), Arrays.asList(parentId, targetFullId));
505531
}
506532

507533
@Test

0 commit comments

Comments
 (0)