Skip to content

Conversation

@komamitsu
Copy link
Contributor

@komamitsu komamitsu commented Oct 3, 2024

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

  • Add SnapshotHandler that receives uncommitted Snapshot
  • Add CommitHandler.setSnapshotHandler()
  • Relax the access scopes of some information about ConsensusCommitManager and Snapshot

Checklist

The following is a best-effort checklist. If any items in this checklist are not applicable to this PR or are dependent on other, unmerged PRs, please still mark the checkboxes after you have read and understood each item.

  • I have commented my code, particularly in hard-to-understand areas.
  • I have updated the documentation to reflect the changes.
  • Any remaining open issues linked to this PR are documented and up-to-date (Jira, GitHub, etc.).
  • Tests (unit, integration, etc.) have been added for the changes.
  • My changes generate no new warnings.
  • Any dependent changes in other PRs have been merged and published.

Additional notes (optional)

None

Release notes

N/A

}

ConsensusCommitManager(DatabaseConfig databaseConfig) {
public ConsensusCommitManager(DatabaseConfig databaseConfig) {
Copy link
Contributor Author

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.

Copy link
Collaborator

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?

Suggested change
public ConsensusCommitManager(DatabaseConfig databaseConfig) {
protected ConsensusCommitManager(DatabaseConfig databaseConfig) {

Copy link
Contributor Author

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) {
Copy link
Contributor Author

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.

Copy link
Contributor

@feeblefakie feeblefakie left a 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);
Copy link
Contributor

@feeblefakie feeblefakie Oct 7, 2024

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?

Copy link
Contributor Author

@komamitsu komamitsu Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@feeblefakie

Can we just set Function or BiFunction like it does with setSnapshotHandler and 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.

Copy link
Contributor

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.

Copy link
Contributor Author

@komamitsu komamitsu Oct 9, 2024

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed it in 0379139

@komamitsu komamitsu requested a review from feeblefakie October 8, 2024 01:41
Copy link
Contributor

@feeblefakie feeblefakie left a 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);
Copy link
Contributor

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.

Copy link
Collaborator

@brfrn169 brfrn169 left a 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;
Copy link
Collaborator

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()?

Copy link
Contributor Author

@komamitsu komamitsu Oct 9, 2024

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);
  }

Copy link
Collaborator

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:

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?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed them in 71b40a4

Copy link
Contributor

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-

Copy link
Contributor Author

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;
Copy link
Collaborator

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;
Copy link
Collaborator

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) {
Copy link
Collaborator

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?

Suggested change
public ConsensusCommitManager(DatabaseConfig databaseConfig) {
protected ConsensusCommitManager(DatabaseConfig databaseConfig) {

Optional<PreparedSnapshotHookFuture> snapshotHookFuture;
try {
prepare(snapshot);
snapshotHookFuture = prepare(snapshot);
Copy link
Collaborator

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?

Suggested change
snapshotHookFuture = prepare(snapshot);
snapshotHandleFuture = handleSnapshotBeforePreparation(snapshot);
prepare(snapshot);

What do you think?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed them in 067d69d.

@brfrn169 I also noticed the original PR didn't properly handle errors when invoking the snapshot hook and waiting the snapshot future, resulting in throwing non-CommitException and resource leaks. I enhanced the error handling in the commit. PTAL!

@@ -0,0 +1,10 @@
package com.scalar.db.transaction.consensuscommit;

public interface PreparedSnapshotHook {
Copy link
Collaborator

@brfrn169 brfrn169 Oct 9, 2024

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.

Maybe BeforePreparationSnapshotHook would be a better name?

In the future, we might add something like BeforeValidationSnapshotHook or BeforeCommitSnapshotHook.

What do you think?

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed it in 0fe41c9

Copy link
Contributor

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?

Copy link
Contributor Author

@komamitsu komamitsu Oct 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@feeblefakie

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?

Copy link
Contributor Author

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.

Done

@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?

Copy link
Contributor Author

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 ...

Copy link
Contributor Author

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.

Done

Oops. What I did was not along with this idea. I'll address this...

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 ...

Reverted

@komamitsu komamitsu requested a review from brfrn169 October 9, 2024 11:09
private final TransactionTableMetadataManager tableMetadataManager;
private final ParallelExecutor parallelExecutor;

@LazyInit @Nullable private BeforePreparationSnapshotHook beforePreparationSnapshotHook;
Copy link
Collaborator

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought @LazyInit also 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?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense. Thanks!

Comment on lines 60 to 66
try {
if (beforePreparationSnapshotHook == null) {
return Optional.empty();
}

return Optional.of(beforePreparationSnapshotHook.handle(tableMetadataManager, snapshot));
} catch (Exception e) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit:

Suggested change
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) {

Copy link
Contributor Author

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());
Copy link
Collaborator

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:

Suggested change
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());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Suggested change
throw new CommitException(e.getMessage(), e, snapshot.getId());
throw new CommitException(
CoreError.WAITING_BEFORE_PREPARATION_SNAPSHOT_HOOK_FAILED.buildMessage(e.getMessage()),
e,
snapshot.getId());

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@komamitsu komamitsu requested a review from brfrn169 October 10, 2024 10:22
package com.scalar.db.transaction.consensuscommit;

public interface BeforePreparationSnapshotHook {
BeforePreparationSnapshotHookFuture handle(
Copy link
Collaborator

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?

Suggested change
BeforePreparationSnapshotHookFuture handle(
Future<Void> handle(

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@komamitsu komamitsu requested a review from brfrn169 October 15, 2024 08:26
@komamitsu
Copy link
Contributor Author

komamitsu commented Oct 16, 2024

@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.

  • Renamed PreparedSnapshotHook to BeforePreparationSnapshotHook
    • Reason: Prepared still sounds "after-prepare-phase"
  • Made BeforePreparationSnapshotHook.handle() simply return Future<Void> for now
    • Reason: I initially used PreparedSnapshotHookFuture since I had considered the potential extendability of the return value. But, we would need to modify CommitHandler as well if we add a new API to the return Future-ish value. Using PreparedSnapshotHookFuture isn't so useful at the moment.
  • Passed immutable Snapshot.ReadWriteSet instead of mutable Snapshot itself to the snapshot hooks.
    • Reason: For safety considering future changes.

@brfrn169 I updated this PR. PTAL when you have a chance.

Copy link
Collaborator

@brfrn169 brfrn169 left a 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!

Comment on lines +96 to +97
CoreError.HANDLING_BEFORE_PREPARATION_SNAPSHOT_HOOK_FAILED.buildMessage(e.getMessage()),
e,
Copy link
Collaborator

@brfrn169 brfrn169 Oct 16, 2024

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?

Copy link
Contributor Author

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

Copy link
Contributor

@Torch3333 Torch3333 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you!

@komamitsu komamitsu merged commit 3b260bc into master Oct 18, 2024
46 checks passed
@komamitsu komamitsu deleted the add-snapshot-handler branch October 18, 2024 04:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants