Skip to content
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

Merged
merged 17 commits into from
Jun 23, 2023

Conversation

Torch3333
Copy link
Contributor

@Torch3333 Torch3333 commented Jun 7, 2023

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

// Build a condition
MutationCondition condition =
ConditionBuilder.putIf(ConditionBuilder.column("c4").isEqualToFloat(0.0F))
.and(ConditionBuilder.column("c5").isEqualToDouble(0.0))
.build();
Put put =
Put.newBuilder()
.namespace("ns")
.table("tbl")
.partitionKey(partitionKey)
.clusteringKey(clusteringKey)
.floatValue("c4", 1.23F)
.doubleValue("c5", 4.56)
.condition(condition) // condition
.build();
// Execute the Put operation
transaction.put(put);

When the mutation condition is not met, a UnsatisfiedConditionException is thrown when the mutation is executed.

Vincent Guilpain added 5 commits April 28, 2023 11:25
…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
@Torch3333 Torch3333 force-pushed the condition_for_transaction_2 branch from c0484d3 to db8b3fe Compare June 8, 2023 05:08
…onditionException, respectively for regular transaction and 2pc, when the mutation condition is not satisfied
@Torch3333 Torch3333 force-pushed the condition_for_transaction_2 branch from db8b3fe to 5ab9a0c Compare June 8, 2023 07:15
@Torch3333 Torch3333 changed the title Support conditions for transactional operations Support condition for transactional operations Jun 8, 2023
@Torch3333 Torch3333 marked this pull request as ready for review June 8, 2023 08:09
@Torch3333 Torch3333 self-assigned this Jun 8, 2023
@Torch3333 Torch3333 added the enhancement New feature or request label Jun 8, 2023
@@ -380,6 +380,7 @@ message TransactionResponse {
TRANSACTION_CONFLICT = 1;
UNKNOWN_TRANSACTION_STATUS = 2;
OTHER = 3;
UNSATISFIED_CONDITION = 4;
Copy link
Contributor Author

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

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.

Comment on lines 20 to 26
/**
* 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 {
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 is the core part of the changes. This class verifies the mutation condition is satisfied without leveraging the underlying storage.

Comment on lines 203 to 213
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);
}
Copy link
Contributor Author

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.

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

Copy link
Contributor Author

@Torch3333 Torch3333 Jun 13, 2023

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

Comment on lines 87 to 93
public static class ConditionCheckerFactory {
public ConditionChecker create(TableMetadata tableMetadata) {
return new ConditionChecker(tableMetadata);
}

public ConditionCheckerFactory() {}
}
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 added this to facilitate mocking this class when for the ConsensusCommitMutationOperationChecker unit test.

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.

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.

@Torch3333 Torch3333 requested a review from brfrn169 June 13, 2023 03:02
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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// 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

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, see 562ca5f

@komamitsu
Copy link
Contributor

For regular transaction, when a mutation operation condition is not met, committing the transaction will throw a CommitUnsatisfiedConditionException.
For 2pc transaction, when a mutation operation condition is not met, preparing the transaction will throw a PreparationUnsatisfiedConditionException.

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

Copy link
Contributor

@komamitsu komamitsu left a comment

Choose a reason for hiding this comment

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

LGTM! 👍

@Torch3333
Copy link
Contributor Author

@komamitsu
Thanks for the question; I should have detailed the reasoning beforehand.

transaction.commit() and twoPCTransaction.prepare() do not throw a type of exception that is common between them.
commit throws CommitException and UnknownTransactionStatusException while prepare throws PreparationException.
So if we wanted to have a shared UnsatisfiedConditionException for these two methods, we would have needed to add this new exception type to the currently thrown exception.
We did not think it was worth adding a common exception type considering the trouble it would cause our users to have to handle a new checked exception type in their existing codebase.

@Torch3333 Torch3333 requested a review from komamitsu June 13, 2023 06:07
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 minor comments. Please take a look when you have time!

PrepareMutationComposer composer =
new PrepareMutationComposer(snapshot.getId(), tableMetadataManager);
snapshot.validateConditionalMutations();
Copy link
Collaborator

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?

Copy link
Contributor

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?

Copy link
Collaborator

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?

Copy link
Contributor

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.

Copy link
Collaborator

@brfrn169 brfrn169 Jun 15, 2023

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?

Copy link
Contributor Author

@Torch3333 Torch3333 Jun 16, 2023

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.

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 revised the PR as described above.
Thank you for the suggestion.

docs/api-guide.md Outdated Show resolved Hide resolved
docs/two-phase-commit-transactions.md Outdated Show resolved Hide resolved
…on_for_transaction_2

# Conflicts:
#	docs/api-guide.md
Copy link
Contributor

@komamitsu komamitsu left a comment

Choose a reason for hiding this comment

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

👍

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.

Overall, looking good. Thank you!
I made some comments and suggestions.
PTAL!
Since it's the core part, I will take another look later.

@@ -583,6 +583,47 @@ Put put =
.build();
```

##### Put with a condition
Copy link
Contributor

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?

@@ -583,6 +583,47 @@ Put put =
.build();
```

##### Put with a condition

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

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?

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

Copy link
Contributor

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.

Copy link
Contributor Author

@Torch3333 Torch3333 Jun 19, 2023

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.

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.

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

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* 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.

Copy link
Contributor Author

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto.

Copy link
Contributor Author

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
Copy link
Contributor

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.

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 sounds good, thank you.

Copy link
Contributor Author

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

Choose a reason for hiding this comment

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

Suggested change
public class ConditionalMutationValidator {
public class MutationConditionsValidator {

I think it validates conditions (not mutations), so should it be like 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.

Indeed, thank you.

Copy link
Contributor Author

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

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?

@josh-wong josh-wong self-requested a review June 16, 2023 10:58
@Torch3333 Torch3333 force-pushed the condition_for_transaction_2 branch from 09b2ae1 to 46441d2 Compare June 19, 2023 06:10
@Torch3333
Copy link
Contributor Author

@josh-wong I finished revising the documentation, so you can have a look when you get the chance. Thank you.

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.

LGTM! Thank you!

Copy link
Member

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

docs/api-guide.md Outdated Show resolved Hide resolved
docs/api-guide.md Outdated Show resolved Hide resolved
docs/api-guide.md Outdated Show resolved Hide resolved
docs/api-guide.md Outdated Show resolved Hide resolved
docs/api-guide.md Outdated Show resolved Hide resolved
Vincent Guilpain and others added 2 commits June 20, 2023 20:02
…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>
@Torch3333
Copy link
Contributor Author

@josh-wong Thank you for the suggestions. I applied them all.

@Torch3333 Torch3333 requested a review from josh-wong June 20, 2023 11:10
Copy link
Member

@josh-wong josh-wong 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!🙇🏻‍♂️

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.

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

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.

Copy link
Contributor Author

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

Copy link
Member

@josh-wong josh-wong 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!🙇🏻‍♂️

@brfrn169 brfrn169 merged commit 8bd43b9 into master Jun 23, 2023
12 checks passed
@brfrn169 brfrn169 deleted the condition_for_transaction_2 branch June 23, 2023 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants