-
Notifications
You must be signed in to change notification settings - Fork 40
Skip commit-state for read-only transactions in Consensus Commit #2765
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
Skip commit-state for read-only transactions in Consensus Commit #2765
Conversation
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.
Pull Request Overview
This PR updates the Consensus Commit implementation to skip the commit‐state step for read-only transactions. The key changes include:
- Adding a boolean parameter to commit methods to distinguish read-only transactions.
- Updating tests to verify the new behavior for read-only and non-read-only modes.
- Introducing helper methods in Snapshot and CrudHandler to support read-only checks and related conditional logic.
Reviewed Changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitTest.java | Updated test cases to pass the new readOnly flag in commit and rollback methods. |
| core/src/test/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManagerTest.java | Adjusted tests to verify proper group commit behavior based on transaction mode. |
| core/src/test/java/com/scalar/db/transaction/consensuscommit/CommitHandlerWithGroupCommitTest.java | Added tests for commit behavior with group commit functionality in various modes. |
| core/src/test/java/com/scalar/db/transaction/consensuscommit/CommitHandlerTest.java | Modified extensive test scenarios to use the new commit API with a readOnly parameter. |
| core/src/main/java/com/scalar/db/transaction/consensuscommit/Snapshot.java | Introduced helper methods to check for absence of writes/deletes and reads. |
| core/src/main/java/com/scalar/db/transaction/consensuscommit/CrudHandler.java | Added an accessor method for the read-only flag. |
| core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java | Updated logic to skip reserving group commits for read-only transactions. |
| core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommit.java | Adjusted commit and rollback logic to conditionally handle read-only transactions. |
| core/src/main/java/com/scalar/db/transaction/consensuscommit/CommitHandlerWithGroupCommit.java | Overrode commit to cancel group commit when appropriate based on transaction mode. |
| core/src/main/java/com/scalar/db/transaction/consensuscommit/CommitHandler.java | Modified the commit method to skip preparation, commit state, and record commits for read-only transactions. |
Comments suppressed due to low confidence (4)
core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java:224
- Consider adding an inline comment explaining why group commit reservation is skipped when the transaction is read-only.
if (!readOnly && isGroupCommitEnabled()) {
core/src/main/java/com/scalar/db/transaction/consensuscommit/CommitHandler.java:109
- Add Javadoc or inline comments describing the behavior of the new 'readOnly' flag and how it affects the commit process.
public void commit(Snapshot snapshot, boolean readOnly) throws CommitException, UnknownTransactionStatusException {
core/src/main/java/com/scalar/db/transaction/consensuscommit/CommitHandlerWithGroupCommit.java:44
- Provide an inline comment to clarify why group commit cancellation is only invoked for non-read-only transactions with no writes and deletes.
if (!readOnly && snapshot.hasNoWritesAndDeletes()) {
core/src/main/java/com/scalar/db/transaction/consensuscommit/Snapshot.java:220
- Consider adding Javadoc comments to the new helper methods (hasNoWritesAndDeletes and hasNoReads) to clarify their intended use and behavior.
public boolean hasNoWritesAndDeletes() {
| if (!hasNoWritesAndDeletesInSnapshot) { | ||
| try { | ||
| prepare(snapshot); | ||
| } catch (PreparationException e) { | ||
| safelyCallOnFailureBeforeCommit(snapshot); | ||
| abortState(snapshot.getId()); | ||
| rollbackRecords(snapshot); | ||
| if (e instanceof PreparationConflictException) { | ||
| throw new CommitConflictException(e.getMessage(), e, e.getTransactionId().orElse(null)); | ||
| } | ||
| throw new CommitException(e.getMessage(), e, e.getTransactionId().orElse(null)); | ||
| } catch (Exception e) { | ||
| safelyCallOnFailureBeforeCommit(snapshot); | ||
| throw e; | ||
| } | ||
| throw new CommitException(e.getMessage(), e, e.getTransactionId().orElse(null)); | ||
| } catch (Exception e) { | ||
| safelyCallOnFailureBeforeCommit(snapshot); | ||
| throw 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.
If no writes and deletes, skip prepare-records.
| if (!snapshot.hasNoReads()) { | ||
| try { | ||
| validate(snapshot); | ||
| } catch (ValidationException e) { | ||
| safelyCallOnFailureBeforeCommit(snapshot); | ||
| abortState(snapshot.getId()); | ||
| rollbackRecords(snapshot); | ||
| if (e instanceof ValidationConflictException) { | ||
| throw new CommitConflictException(e.getMessage(), e, e.getTransactionId().orElse(null)); | ||
| } | ||
| throw new CommitException(e.getMessage(), e, e.getTransactionId().orElse(null)); | ||
| } catch (Exception e) { | ||
| safelyCallOnFailureBeforeCommit(snapshot); | ||
| throw e; | ||
| } | ||
| throw new CommitException(e.getMessage(), e, e.getTransactionId().orElse(null)); | ||
| } catch (Exception e) { | ||
| safelyCallOnFailureBeforeCommit(snapshot); | ||
| throw 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.
If no reads, skip validate-record.
| if (!hasNoWritesAndDeletesInSnapshot) { | ||
| commitState(snapshot); | ||
| commitRecords(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.
If no writes and deletes, skip commit-state and commit-records.
| if (!readOnly && isGroupCommitEnabled()) { | ||
| assert groupCommitter != null; | ||
| txId = groupCommitter.reserve(txId); | ||
| } |
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.
In read-only mode, skip reserving a slot in the group commit.
jnmt
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.
Thanks for the heads up.
I checked the Auditor's recovery behavior. It first tries to abort a lock holder transaction regardless of read/write types. If a read-only transaction does not put the commit state by this PR, this abort process will always result in creating aborted status record and make the Auditor release locks.
Releasing itself is OK for read locks, but I think it's pretty confusing since the created status record is not just garbage but shows the wrong status (aborted) even if the read-only transaction is actually committed.
So, from the ScalarDL perspective, it would be better to configure disabling this optimization. The default enabled would be acceptable since we can disable it when creating the transaction manager on the ScalarDL side.
@feeblefakie Please correct me if I miss something and let us know your opinion.
|
@brfrn169 I missed one point.
This is caused by scalardb/core/src/main/java/com/scalar/db/transaction/consensuscommit/ConsensusCommitManager.java Line 486 in 19aac3d
|
|
@jnmt Thank you for reviewing this PR and checking the behavior of the Auditor!
Actually, we cannot determine whether the transaction associated with the specified transaction ID is read-only or not. So I don’t think we can handle that on the ScalarDB side. |
Unfortunately, I don’t think we can easily determine if a transaction is read-only in the recovery context. It might be possible by re-executing the transaction based on the request log, but it’s too much and not feasible for this purpose, I think. |
komamitsu
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! 👍
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!
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.
Overall, looking good.
Left some suggestions. PTAL!
| if (e instanceof PreparationConflictException) { | ||
| throw new CommitConflictException(e.getMessage(), e, e.getTransactionId().orElse(null)); | ||
|
|
||
| if (!hasNoWritesAndDeletesInSnapshot) { |
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 double negative is a bit hard to read.
Since all the usage of hasNoWritesAndDeletesInSnapshot is with negation (!), how about making the method like hasSomeWritesAndDeletesInSnapshot so that we don't have to negate all the if clauses?
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 6c6299d. Thanks!
| rollbackRecords(snapshot); | ||
| if (e instanceof ValidationConflictException) { | ||
| throw new CommitConflictException(e.getMessage(), e, e.getTransactionId().orElse(null)); | ||
| if (!snapshot.hasNoReads()) { |
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.
|
@feeblefakie What do you think about @jnmt's comment? Should we go with making the skipping commit-state behavior configurable? |
|
@brfrn169 As @jnmt mentioned, I think we need a coordinator write even for read-only operations. @jnmt |
…_on_read_only.enabled`
|
@feeblefakie @jnmt Added the |
jnmt
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 for adding the config option! LGTM, although I left a minor comment that doesn't block merging this.
| public void commit(Snapshot snapshot) throws CommitException, UnknownTransactionStatusException { | ||
| public void commit(Snapshot snapshot, boolean readOnly) | ||
| throws CommitException, UnknownTransactionStatusException { | ||
| boolean hasSomeWritesOrDeletesInSnapshot = !readOnly && snapshot.hasSomeWritesOrDeletes(); |
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. I feel "Some" is a bit redundant if you don't have any special intention.
| boolean hasSomeWritesOrDeletesInSnapshot = !readOnly && snapshot.hasSomeWritesOrDeletes(); | |
| boolean hasWritesOrDeletesInSnapshot = !readOnly && snapshot.hasWritesOrDeletes(); |
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 e783b13. Thanks!
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!
Just one suggestion for the naming.
| public void commit(Snapshot snapshot) throws CommitException, UnknownTransactionStatusException { | ||
| public void commit(Snapshot snapshot, boolean readOnly) | ||
| throws CommitException, UnknownTransactionStatusException { | ||
| boolean hasSomeWritesOrDeletesInSnapshot = !readOnly && snapshot.hasWritesOrDeletes(); |
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.
| boolean hasSomeWritesOrDeletesInSnapshot = !readOnly && snapshot.hasWritesOrDeletes(); | |
| boolean hasWritesOrDeletesInSnapshot = !readOnly && snapshot.hasWritesOrDeletes(); |
If Some is removed, should it be consistent?
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! Fixed in 2c13b1c. Thanks!
Description
This PR updates the code to omit writing the commit state for read-only transactions in Consensus Commit to improve performance. This behavior is enabled by default but can be disabled by setting the property
scalar.db.consensus_commit.coordinator.write_omission_on_read_only.enabledtofalse.Related issues and/or PRs
N/A
Changes made
Add inline comments. Please take a look for the details.
Checklist
Additional notes (optional)
N/A
Release notes
Changed to omit commit-state for read-only transactions in Consensus Commit to improve performance. This behavior is enabled by default but can be disabled by setting the property
scalar.db.consensus_commit.coordinator.write_omission_on_read_only.enabledtofalse.