-
Notifications
You must be signed in to change notification settings - Fork 41
Add an extension point to collect uncommitted read-set and write-set #2271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
to make it inheritable
when SnapshotHandler is set in 2PC interface
| } | ||
|
|
||
| ConsensusCommitManager(DatabaseConfig databaseConfig) { | ||
| public ConsensusCommitManager(DatabaseConfig databaseConfig) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To make it easy to inherit ConsensusCommitManager. This change is helpful to utilize the new extension point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, protected is more appropriate?
| public ConsensusCommitManager(DatabaseConfig databaseConfig) { | |
| protected ConsensusCommitManager(DatabaseConfig databaseConfig) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I confirmed it works. Addressed it in d2d1003
| * @param currentTxVersion The current `tx_version`, if it exists, or null otherwise. | ||
| * @return The next `tx_version`. | ||
| */ | ||
| public static int getNextTxVersion(@Nullable Integer currentTxVersion) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic how to determine the next Tx version may be reused by an extension module injected via class loader.
feeblefakie
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
Left some questions. PTAL!
| throw e; | ||
| } | ||
|
|
||
| snapshotHandleFuture.ifPresent(SnapshotHandleFuture::get); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might miss something, but is there any reason for using Future and defining SnapshotHandleFuture?
Can we just set Function or BiFunction like it does with setSnapshotHandler and invoke it here?
Also, I feel SnapshotHandler sounds too general.
Since it is invoked before the commit phase, we should probably name it something like BeforeCommitSnapshotHook.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just set Function or BiFunction like it does with
setSnapshotHandlerand invoke it here?
Yeah, good point. I think using Function should also work.
I named the interface SnapshotHandle**Future** since I wanted to encourage setSnapshotHandler users to implement asynchronous handlers that don't block prepare() and validate(). Also, the reason why I wrapped Future with SnapshotHandleFuture is for maintainability considering future modifications just in case. But, I'm okay to go with simple Function.
Also, I feel SnapshotHandler sounds too general.
Ah, I agree. BeforeCommitSnapshotHook seems good. But it might sound like "validated" snapshots. How about PreparedSnapshotHook? Either is fine with me, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation.
Using Future for a non-blocking hook makes sense in this case since Snapshot won't be changed after the prepare phase, so I'm fine with it.
For the name, how about AfterPrepareSnapshotHook?
I'm also fine with BeforeCommitSnapshotHook and PreparedSnapshotHook.
I think you can decide.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@feeblefakie Thanks for the comment!
For the name, how about AfterPrepareSnapshotHook?
I'm also fine with BeforeCommitSnapshotHook and PreparedSnapshotHook.
Strictly speaking, snapshots are sent asynchronously before prepare() completes. So, I hesitate a bit to use the terms before or after that emphasizes the order. PreparedSnapshotHook might still imply that snapshots are already prepared, but I think it can be also interpreted that snapshots are in the process of being prepared, and relatively better. I'll go with PreparedSnapshotHook.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed it in 0379139
feeblefakie
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! Thank you!
I would like the class name to be changed, but any of the current options is fine with me.
| throw e; | ||
| } | ||
|
|
||
| snapshotHandleFuture.ifPresent(SnapshotHandleFuture::get); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the explanation.
Using Future for a non-blocking hook makes sense in this case since Snapshot won't be changed after the prepare phase, so I'm fine with it.
For the name, how about AfterPrepareSnapshotHook?
I'm also fine with BeforeCommitSnapshotHook and PreparedSnapshotHook.
I think you can decide.
brfrn169
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left several comments. Please take a look when you have time!
| */ | ||
| @SuppressFBWarnings(value = {"EI_EXPOSE_REP"}) | ||
| public Map<Key, Optional<TransactionResult>> getReadSet() { | ||
| return readSet; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about wrapping it with new HashMap() or ImmutableMap.copyOf()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The snapshot hook mechanism introduced in this PR could impact the performance and memory consumption of applications that use ScalarDB. So, I think all the operations in a snapshot hook should be fast and low-memory-footprint as much as possible. As described in the method comment, it would be great if a snapshot hook can read those write set and read set without any coping entries.
I also thought the following iteration method, but it's too usage-specific.
public void iterateWriteSet(BiConsumer<Put, Optional<TransactionResult>> consumer) {
for (Map.Entry<Key, Put> entry : writeSet.entrySet()) {
Optional<TransactionResult> result = Optional.empty();
if (readSet.containsKey(entry.getKey())) {
result = readSet.get(entry.getKey());
}
consumer.accept(entry.getValue(), result);
}
}I think using java.util.Collections#unmodifiableMap() is a reasonable option since it doesn't have its own table but delegates read operations to the original map. The performance impact should be negligible. What do you think?
public Map<Key, Optional<TransactionResult>> getReadSet() {
return Collections.unmodifiableMap(readSet);
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. Sorry, I missed the method comment. That sounds reasonable.
I think using
java.util.Collections#unmodifiableMap()is a reasonable option since it doesn't have its own table but delegates read operations to the original map. The performance impact should be negligible. What do you think?
Actually, I’m not sure about the differences in performance between Collections#unmodifiableMap(), new HashMap(), and ImmutableMap.copyOf(), so let's keep it as is for now.
By the way, we already have getter methods for writeSet and deleteSet, and they copy writeSet and deleteSet:
scalardb/core/src/main/java/com/scalar/db/transaction/consensuscommit/Snapshot.java
Lines 157 to 163 in 0379139
| public List<Put> getPutsInWriteSet() { | |
| return new ArrayList<>(writeSet.values()); | |
| } | |
| public List<Delete> getDeletesInDeleteSet() { | |
| return new ArrayList<>(deleteSet.values()); | |
| } |
For the same reason, we probably don’t want to copy them. So could you please modify these methods as well and add similar method comments, even though it’s not directly related to this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
so let's keep it as is for now.
👍
As for getPutsInWriteSet() and getDeletesInDeleteSet(), they wrap the maps' values with ArrayList constructor, which also internally copies received values. So, I'll change the return type to Collection<Put> and Collection<Delete>, respectively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed them in 71b40a4
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be better to do a simple benchmarking if we care about the performance.
ImmutableMap.copyOf actually tries to avoid copying if it is safe as described in the doc, so it might not cause much overhead.
https://guava.dev/releases/23.0/api/docs/com/google/common/collect/ImmutableMap.html#copyOf-java.util.Map-
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, thanks for the heads-up. I micro-benchmarked yesterday with the following code. Sorry, I should've mentioned this benchmark.
package org.example;
import com.google.common.collect.ImmutableMap;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class IteratorTest {
private static final Map<String, Integer> MAP = new HashMap<>();
static {
MAP.put("one", 1);
MAP.put("two", 2);
MAP.put("three", 3);
MAP.put("four", 4);
MAP.put("five", 5);
MAP.put("six", 6);
MAP.put("seven", 7);
MAP.put("eight", 8);
MAP.put("nine", 9);
MAP.put("ten", 10);
}
private static void testDirect() {
Map<String, Integer> map = MAP;
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
}
private static void testUnmod() {
Map<String, Integer> map = Collections.unmodifiableMap(MAP);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
}
private static void testNewMap() {
Map<String, Integer> map = new HashMap<>(MAP);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
}
private static void testImmutable() {
Map<String, Integer> map = ImmutableMap.copyOf(MAP);
for (Map.Entry<String, Integer> entry : map.entrySet()) {
entry.getKey();
entry.getValue();
}
}
public static void main(String[] args) {
int n = 1000000;
for (int i = 0; i < n; i++) { testDirect(); }
{
long start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
testDirect();
}
System.out.println("Direct: duration=" + (System.currentTimeMillis() - start));
}
for (int i = 0; i < n; i++) { testUnmod(); }
{
long start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
testUnmod();
}
System.out.println("UnmodiMap: duration=" + (System.currentTimeMillis() - start));
}
for (int i = 0; i < n; i++) { testNewMap(); }
{
long start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
testNewMap();
}
System.out.println("NewMap: duration=" + (System.currentTimeMillis() - start));
}
for (int i = 0; i < n; i++) { testImmutable(); }
{
long start = System.currentTimeMillis();
for (int i = 0; i < n; i++) {
testImmutable();
}
System.out.println("Immutable: duration=" + (System.currentTimeMillis() - start));
}
}
}The result was:
> Task :IteratorTest.main()
Direct: duration=20
UnmodiMap: duration=21
NewMap: duration=89
Immutable: duration=130
As for the Javadoc, I also looked into the implementation. It doesn't copy only when the original map is ImmutableMap instance. In the case of Snapshot. the original maps are HashMap and I think they'll be copied.
| */ | ||
| @SuppressFBWarnings(value = {"EI_EXPOSE_REP"}) | ||
| public Map<Key, Put> getWriteSet() { | ||
| return writeSet; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| */ | ||
| @SuppressFBWarnings(value = {"EI_EXPOSE_REP"}) | ||
| public Map<Key, Delete> getDeleteSet() { | ||
| return deleteSet; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| } | ||
|
|
||
| ConsensusCommitManager(DatabaseConfig databaseConfig) { | ||
| public ConsensusCommitManager(DatabaseConfig databaseConfig) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, protected is more appropriate?
| public ConsensusCommitManager(DatabaseConfig databaseConfig) { | |
| protected ConsensusCommitManager(DatabaseConfig databaseConfig) { |
| Optional<PreparedSnapshotHookFuture> snapshotHookFuture; | ||
| try { | ||
| prepare(snapshot); | ||
| snapshotHookFuture = prepare(snapshot); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe we can move the snapshotHandler logic outside the prepare() method as follows?
| snapshotHookFuture = prepare(snapshot); | |
| snapshotHandleFuture = handleSnapshotBeforePreparation(snapshot); | |
| prepare(snapshot); |
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good point! I missed the improvement after we decided to use Snapshot itself... I'll update the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| @@ -0,0 +1,10 @@ | |||
| package com.scalar.db.transaction.consensuscommit; | |||
|
|
|||
| public interface PreparedSnapshotHook { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PreparedSnapshotHook sounds like a hook called after preparation to me. Actually, the state of the snapshot can change after preparation.
scalardb/core/src/main/java/com/scalar/db/transaction/consensuscommit/Snapshot.java
Line 459 in 0379139
| writeSet.put(entry.getKey(), put); |
Maybe BeforePreparationSnapshotHook would be a better name?
In the future, we might add something like BeforeValidationSnapshotHook or BeforeCommitSnapshotHook.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PreparedSnapshotHook sounds like a hook called after preparation to me.
Indeed, I have the same concern slightly. #2271 (comment)
It's possible that the prepared records are asynchronously written before a snapshot is sent. So, I was also wondering if BeforePreparationSnapshotHook-ish name was reasonable or not. But, I think it can mean the snapshot is sent before the prepare phase operation. So, I think it's okay. I'll change it to BeforePreparationSnapshotHook.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed it in 0fe41c9
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As Toshi said, snapshot states might be changed in the case of extra-write. So, we shouldn't trigger the hook asynchronously?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, you're right... I just remembered that @brfrn169 and I discussed this case and planed not to support the snapshot hook feature when extra-write is enabled, since extra-write would be sunset-ed (so, this PR should've checked the configuration...).
Triggering the hook after preparation might increase the total latency. So, I think the invocation of the hook should be asynchronous without waiting for the completion of prepare phase operation, although it might increase the possibility of transactions that abort in prepare phase being handled by the snapshot handler to some extent. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, do you think we should give up passing a snapshot and just passes the immutable read/write-sets like this?
Yes, that’s what I had in mind. Thanks.
@brfrn169 I noticed new ArrayList(map.entrySet()) is twice faster than returning new HashMap(map). So, I added the method List<Entry<Key, Put>> getKeysAndPutsInWriteSet() instead of Map<Key, Put> getWriteSetMap(). I think the new method is very similar to the existing method List<Put> getPutsInWriteSet(). Probably we can replace the usage of List<Put> getPutsInWriteSet() with the new method. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As for extra-write, yeah I'll add a validation for that in this PR later.
Done
On second thought, whether to prohibit EXTRA WRITE depends on the usage of write sets. On the above discussions, I was only thinking of the use case of replication. But, write set captured before prepare might work with EXTRA WRITE in other use cases. So, I think a validation to prohibit using EXTRA WRITE should be handled by a plug-in side not in scalardb:core. Probably I should revert 8f0817d ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, do you think we should give up passing a snapshot and just passes the immutable read/write-sets like this?
Yes, that’s what I had in mind. Thanks.
Oops. What I did was not along with this idea. I'll address this...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the late fix. I updated the PR to pass ReadWriteSet instead of Snapshot itself in d540b4b.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On second thought, whether to prohibit EXTRA WRITE depends on the usage of write sets. On the above discussions, I was only thinking of the use case of replication. But, write set captured before prepare might work with EXTRA WRITE in other use cases. So, I think a validation to prohibit using EXTRA WRITE should be handled by a plug-in side not in scalardb:core. Probably I should revert 8f0817d ...
| private final TransactionTableMetadataManager tableMetadataManager; | ||
| private final ParallelExecutor parallelExecutor; | ||
|
|
||
| @LazyInit @Nullable private BeforePreparationSnapshotHook beforePreparationSnapshotHook; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a minor thing, but I thought @LazyInit also implied that the field was nullable. Should we add @Nullable alongside @LazyInit as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought
@LazyInitalso implied that the field was nullable.
Does it point that the field can be null until it's lazily initialized? If so, I think only @LazyInit may be enough. @Nullable is added since the field can be permanently null if no snapshot hook is set. Does it answer your question?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense. Thanks!
| try { | ||
| if (beforePreparationSnapshotHook == null) { | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| return Optional.of(beforePreparationSnapshotHook.handle(tableMetadataManager, snapshot)); | ||
| } catch (Exception e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small nit:
| try { | |
| if (beforePreparationSnapshotHook == null) { | |
| return Optional.empty(); | |
| } | |
| return Optional.of(beforePreparationSnapshotHook.handle(tableMetadataManager, snapshot)); | |
| } catch (Exception e) { | |
| if (beforePreparationSnapshotHook == null) { | |
| return Optional.empty(); | |
| } | |
| try { | |
| return Optional.of(beforePreparationSnapshotHook.handle(tableMetadataManager, snapshot)); | |
| } catch (Exception e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| // TODO: This method is actually a part of preparation phase. But the callback method name | ||
| // `onPrepareFailure()` should be renamed to more reasonable one. | ||
| onPrepareFailure(snapshot); | ||
| throw new CommitException(e.getMessage(), e, snapshot.getId()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering if we should define a new error message for this as follows:
| throw new CommitException(e.getMessage(), e, snapshot.getId()); | |
| throw new CommitException( | |
| CoreError.HANDLING_BEFORE_PREPARATION_SNAPSHOT_HOOK_FAILED.buildMessage(e.getMessage()), | |
| e, | |
| snapshot.getId()); |
| // TODO: This method is actually a part of validation phase. But the callback method name | ||
| // `onValidateFailure()` should be renamed to more reasonable one. | ||
| onValidateFailure(snapshot); | ||
| throw new CommitException(e.getMessage(), e, snapshot.getId()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto.
| throw new CommitException(e.getMessage(), e, snapshot.getId()); | |
| throw new CommitException( | |
| CoreError.WAITING_BEFORE_PREPARATION_SNAPSHOT_HOOK_FAILED.buildMessage(e.getMessage()), | |
| e, | |
| snapshot.getId()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch! I'll define and use the new error code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| package com.scalar.db.transaction.consensuscommit; | ||
|
|
||
| public interface BeforePreparationSnapshotHook { | ||
| BeforePreparationSnapshotHookFuture handle( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, one more thing. Why not use the Java Future as follows?
| BeforePreparationSnapshotHookFuture handle( | |
| Future<Void> handle( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I first thought using the custom future would be helpful if we add a new API like cancel() to the future. But it also requires some changes in CommitHandler to call new APIs. Since we can postpone implementing the custom future until we actually need such new APIs, I'll go ahead and merge your suggestion to simplify things for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of Snapshot
This reverts commit 5b77134.
|
@feeblefakie JFYI: I added the following changes after you approved this PR, based on some second thoughts and discussion with @brfrn169. Feel free to share your concerns or thoughts.
@brfrn169 I updated this PR. PTAL when you have a chance. |
brfrn169
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Left a minor comment. Other than that, LGTM! Thank you!
| CoreError.HANDLING_BEFORE_PREPARATION_SNAPSHOT_HOOK_FAILED.buildMessage(e.getMessage()), | ||
| e, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When catching ExecutionException, the actual exception is wrapped by ExecutionException, so maybe we should get the cause exception by the getCause() method in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Future (== FutureTask) wraps an exception occurs in get() with new ExecutionException((Throwable)cause). new Throwable(Throwable cause) sets detailedMessage to cause.toString(). So, I think it's okay in this case.
ExecutorService executorService = Executors.newCachedThreadPool();
Future<Object> future = executorService.submit(() -> {
throw new RuntimeException("This is the original exception");
});
executorService.shutdown();
try {
future.get();
}
catch (Exception e) {
System.out.println("Message from the exception: " + e.getMessage());
System.out.println("Message from the cause: " + e.getCause());
}Message from the exception: java.lang.RuntimeException: This is the original exception
Message from the cause: java.lang.RuntimeException: This is the original exception
Torch3333
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thank you!
Description
In the current implementation, it's not possible to develop an extension module injected via class loader that accesses the uncommitted read-set and write-set of a consensus commit transaction. This PR adds an extension point to collect uncommitted read-set and write-set to make it easy to develop extension modules to access those information.
Related issues and/or PRs
None
Changes made
SnapshotHandlerthat receives uncommittedSnapshotCommitHandler.setSnapshotHandler()ConsensusCommitManagerandSnapshotChecklist
Additional notes (optional)
None
Release notes
N/A