-
Notifications
You must be signed in to change notification settings - Fork 38
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
Support condition for transactional operations #899
Conversation
…on_for_transaction # Conflicts: # core/src/main/java/com/scalar/db/transaction/consensuscommit/PrepareMutationComposer.java # core/src/test/java/com/scalar/db/transaction/consensuscommit/PrepareMutationComposerTest.java
…ConflictException instead of CrudException
…on_for_transaction
c0484d3
to
db8b3fe
Compare
…onditionException, respectively for regular transaction and 2pc, when the mutation condition is not satisfied
db8b3fe
to
5ab9a0c
Compare
.../java/com/scalar/db/transaction/consensuscommit/ConsensusCommitMutationOperationChecker.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java
Outdated
Show resolved
Hide resolved
@@ -380,6 +380,7 @@ message TransactionResponse { | |||
TRANSACTION_CONFLICT = 1; | |||
UNKNOWN_TRANSACTION_STATUS = 2; | |||
OTHER = 3; | |||
UNSATISFIED_CONDITION = 4; |
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.
For the transaction gRPC API, I added a new error code to handle a failed mutation condition.
@@ -515,6 +516,7 @@ message TwoPhaseCommitTransactionResponse { | |||
TRANSACTION_CONFLICT = 1; | |||
UNKNOWN_TRANSACTION_STATUS = 2; | |||
OTHER = 3; | |||
UNSATISFIED_CONDITION = 4; |
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.
Likewise for the 2PC transaction gRPC API.
/** | ||
* This class verifies the mutation condition for Put and Delete operation are met by asserting the | ||
* condition on the existing record (if any) targeted by the mutation. It does not validate by the | ||
* condition by using the underlying storage. | ||
*/ | ||
@ThreadSafe | ||
public class ConditionalMutationValidator { |
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 is the core part of the changes. This class verifies the mutation condition is satisfied without leveraging the underlying storage.
if (composer instanceof PrepareMutationComposer) { | ||
conditionalMutationValidator.validateConditionIsSatisfied(entry.getValue(), result); | ||
} | ||
composer.add(entry.getValue(), result); | ||
} | ||
for (Entry<Key, Delete> entry : deleteSet.entrySet()) { | ||
TransactionResult result = | ||
readSet.containsKey(entry.getKey()) ? readSet.get(entry.getKey()).orElse(null) : null; | ||
if (composer instanceof PrepareMutationComposer) { | ||
conditionalMutationValidator.validateConditionIsSatisfied(entry.getValue(), result); | ||
} |
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.
Checking if the mutation condition is met is performed here.
Ideally, I would have preferred to add this call directly to the PrepareMutationComposer
class but this means I would have needed to update the MutationComposer.add()
interface method signature to throw a PreparationUnsatisfiedConditionException
, which seemed more problematic.
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 add a new method to validate conditions in mutations in the write set and the delete set to the Snapshot
class, and we can call it in CommitHandler.prepare()
. 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.
Thank you for the suggestion, I agree.
I did as you say and created the Snapshot.validateConditionalMutations()
method that is executed from CommitHandler.prepare()
see afc2fbe
public static class ConditionCheckerFactory { | ||
public ConditionChecker create(TableMetadata tableMetadata) { | ||
return new ConditionChecker(tableMetadata); | ||
} | ||
|
||
public ConditionCheckerFactory() {} | ||
} |
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 added this to facilitate mocking this class when for the ConsensusCommitMutationOperationChecker
unit test.
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.
Overall LGTM. Left one minor comment. Please take a look when you have time!
Since this change affects the core transaction logic, I'll review it again later. Thanks.
.../java/com/scalar/db/transaction/consensuscommit/ConsensusCommitMutationOperationChecker.java
Outdated
Show resolved
Hide resolved
core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java
Outdated
Show resolved
Hide resolved
case NE: | ||
return Ordering.natural().compare(existingRecordColumn, conditionalExpressionColumn) != 0; | ||
// For 'greater than' and 'less than' types of conditions and when the existing record is | ||
// null, we consider the condition to be unsatisfied. This mimic the behavior as if |
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.
// null, we consider the condition to be unsatisfied. This mimic the behavior as if | |
// null, we consider the condition to be unsatisfied. This mimics the behavior as if |
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, see 562ca5f
core/src/main/java/com/scalar/db/transaction/jdbc/JdbcTransaction.java
Outdated
Show resolved
Hide resolved
@Torch3333 What is the main benefit of having the two separate exceptions? The background of this question is that I'm wondering if it's an option to merge them to just a UnsatisfiedConditionException. |
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! 👍
@komamitsu
|
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 minor comments. Please take a look when you have time!
PrepareMutationComposer composer = | ||
new PrepareMutationComposer(snapshot.getId(), tableMetadataManager); | ||
snapshot.validateConditionalMutations(); |
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 snapshot.validateConditionalMutations()
is no longer a part of prepareRecords()
. So can we move it into the prepare()
method as follows?
public void prepare(Snapshot snapshot) throws PreparationException {
snapshot.validateConditionalMutations();
try {
prepareRecords(snapshot);
} catch (NoMutationException e) {
throw new PreparationConflictException("preparing record exists", e, snapshot.getId());
} catch (RetriableExecutionException e) {
throw new PreparationConflictException(
"conflict happened when preparing records", e, snapshot.getId());
} catch (ExecutionException e) {
throw new PreparationException("preparing records failed", e, snapshot.getId());
}
}
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.
Moreover, I'm also wondering if validating conditions should be Snapshot
's responsibility or not.
@brfrn169 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.
@feeblefakie That's a good point.
Indeed, from the impression of the name, it doesn't seem like validating conditions is the responsibility of Snapshot
. However, if we were to move validateConditionalMutations()
into another class, we would need to expose readSet
, writeSet
, and deleteSet
from Snapshot
(by adding getter methods for them). And if we proceed with this, it feels like we should also move toSerializableWithExtraWrite()
and toSerializableWithExtraRead()
into another class. But such changes amount to substantial refactoring, which shouldn't be done in this PR. Maybe we can retain validateConditionalMutations()
in Snapshot
for now and consider refactoring it in the future.
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.
@brfrn169 Sorry, I should have elaborated more.
I meant, can we check the conditions of mutation operations when calling the operations, not when committing
the transaction? (so that we can do an early abort)
For example, it seems like we can check the conditions of Put
when calling CrudHandler.put
.
I might miss something, 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.
For example, it seems like we can check the conditions of Put when calling CrudHandler.put.
@feeblefakie Ah, indeed, it seems we could do that.
@Torch3333 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.
@brfrn169
After discussing with @feeblefakie this morning, we concluded it is possible to move the condition check to CrudHandler.put
and CrudHandler.delete
.
I will need to add a getter method in the Snapshot
object to read a value from the read set.
Regarding the throw exception when the condition is not satisfied, we can then have a single exception type for regular and 2pc transaction interface that is called UnsatisfiedConditionException
(extending CrudException
) when executing transaction.put()
or transaction.delete()
.
Overall, it will simplify the implementation.
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 revised the PR as described above.
Thank you for the suggestion.
server/src/test/java/com/scalar/db/server/DistributedTransactionServiceTest.java
Outdated
Show resolved
Hide resolved
…on_for_transaction_2 # Conflicts: # docs/api-guide.md
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.
👍
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.
Overall, looking good. Thank you!
I made some comments and suggestions.
PTAL!
Since it's the core part, I will take another look later.
docs/api-guide.md
Outdated
@@ -583,6 +583,47 @@ Put put = | |||
.build(); | |||
``` | |||
|
|||
##### Put with a condition |
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.
@josh-wong Can you review the updates in this guide?
docs/api-guide.md
Outdated
@@ -583,6 +583,47 @@ Put put = | |||
.build(); | |||
``` | |||
|
|||
##### Put with a condition | |||
|
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 helpful to add something like this here.
You can write arbitrary conditions (e.g., a bank account balance must be equal to or more than zero) that you require a transaction to meet before being committed by having some checking logic in the transaction. Alternatively, you can also write simple conditions in a mutation operation, such as Put and Delete.
(If this is added, I think we need to update the next sentence a bit.)
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 for the suggestion,
To avoid redundancy in the explanation, I will merge the Put with a condition
and Delete with a condition
paragraphs.
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.
Fixed in 46441d2
* @param existingRecord the current value of the record targeted by the mutation, if any | ||
* @throws PreparationUnsatisfiedConditionException if the condition is not satisfied | ||
*/ | ||
public void validateConditionIsSatisfied(Put put, @Nullable TransactionResult existingRecord) |
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 bit picky, but I feel the method name is a little redundant since a condition is valid if a condition is satisfied in this case.
How about the following names?
- validateCondition
- checkIfConditionIsSatisfied
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.
I see, thank you.
I used a verbose name for this method to be clear that this method is not about checking the condition is not contextually correct (e.g. the column used in the condition matches the table metadata)
So, I will use your second suggestion checkIfConditionIsSatisfied
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.
If you choose checkIfConditionIsSatisfied
, then we should probably rename validateConditionalMutations
for consistency and clarity.
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 renamed the method to MutationConditionsValidator.checkIfConditionIsSatisfied
in 46441d2
Thank you.
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 some additional comments.
* @param existingRecord the current value of the record targeted by the mutation, if any | ||
* @throws PreparationUnsatisfiedConditionException if the condition is not satisfied | ||
*/ | ||
public void validateConditionIsSatisfied(Put put, @Nullable TransactionResult existingRecord) |
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.
If you choose checkIfConditionIsSatisfied
, then we should probably rename validateConditionalMutations
for consistency and clarity.
} | ||
|
||
/** | ||
* This validates the condition for the Put operation is satisfied. |
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 validates the condition for the Put operation is satisfied. | |
* This checks if the condition of the specified Put operation is satisfied for the specified record. |
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. It's fixed in 46441d2
} | ||
|
||
/** | ||
* This validates the condition for the Delete operation is satisfied. |
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.
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. It's fixed in 46441d2
import javax.annotation.concurrent.ThreadSafe; | ||
|
||
/** | ||
* This class verifies the mutation condition for Put and Delete operation are met by asserting the |
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 something like this?
This class checks if a record satisfies the conditions of Put and Delete operations that mutate the record.
I feel the sentence It does not leverage the underlying storage to validate the condition.
is a bit confusing.
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 sounds good, thank you.
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. It's fixed in 46441d2
* underlying storage to validate the condition. | ||
*/ | ||
@ThreadSafe | ||
public class ConditionalMutationValidator { |
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.
public class ConditionalMutationValidator { | |
public class MutationConditionsValidator { |
I think it validates conditions (not mutations), so should it be like 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.
Indeed, thank you.
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 fixed in 46441d2
PrepareMutationComposer composer = | ||
new PrepareMutationComposer(snapshot.getId(), tableMetadataManager); | ||
snapshot.validateConditionalMutations(); |
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.
Moreover, I'm also wondering if validating conditions should be Snapshot
's responsibility or not.
@brfrn169 What do you think?
09b2ae1
to
46441d2
Compare
@josh-wong I finished revising the documentation, so you can have a look when you get the chance. Thank you. |
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!
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 added a few comments and suggested revisions. PTAL!
…on_for_transaction_2 # Conflicts: # core/src/test/java/com/scalar/db/transaction/consensuscommit/CrudHandlerTest.java
Co-authored-by: Josh Wong <joshua.wong@scalar-labs.com>
@josh-wong Thank you for the suggestions. I applied them all. |
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!🙇🏻♂️
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 left one very minor comment, but LGTM! Thank you!
@@ -519,6 +519,10 @@ public boolean isValidationRequired() { | |||
return isExtraReadEnabled(); | |||
} | |||
|
|||
public Optional<TransactionResult> getFromReadSet(Key key) { |
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.
Super minor, but I think it's better to be placed below containsKeyInReadSet
just to cluster similar methods.
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.
Indeed, thank you. I updated it in c612740
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!🙇🏻♂️
…scalardb into condition_for_transaction_2
Context
Setting conditions for Put or Delete in a transaction was not supported. Setting conditions in the operation builder was possible, but they were ignored.
Changes
Setting condition is now fully supported on Put and Delete operation for consensus commit (regular and 2pc) or Jdbc transactions. For example :
scalardb/docs/api-guide.md
Lines 622 to 640 in 46441d2
When the mutation condition is not met, a
UnsatisfiedConditionException
is thrown when the mutation is executed.