Skip to content

Conversation

@chuzhe-as-a-dev
Copy link

Ⅰ. Describe what this PR did

This PR integrates the global serializability protocols from the Soanta paper to offer better isolation/consistency guarantees in XA mode. This discussion gives more details.

Ⅱ. Does this pull request fix one issue?

No.

Ⅲ. Why don't you add test cases (unit test/integration test)?

Little familiarity with the test structure and limited time budget.

Ⅳ. Describe how to verify it

The correctness of the protocols is proven in the research paper and the implementation is mainly a rework of the research prototype, which has been extensively evaluated under various complex workloads, including TPC-C.

Ⅴ. Special notes for reviews

@funky-eyes funky-eyes requested a review from Copilot November 1, 2025 13:48
Copy link
Contributor

Copilot AI left a 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 adds support for Sonata's global serializability feature to Seata's XA transaction mode. The implementation introduces configuration for Sonata-specific behavior, including dummy table writes for MySQL (S2PL) and PostgreSQL (SSI) databases to enable strict serializable isolation.

Key changes:

  • New configuration properties for Sonata features (enable flag, dummy table settings, retry thresholds, batch sizes)
  • Database-specific shim logic in XA datasource and connection proxies to handle dummy writes and helper transactions
  • Enhanced XA error handling for MySQL and PostgreSQL edge cases (deadlocks, non-existing branches, etc.)

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
StarterConstants.java Added SONATA_PREFIX constant for Spring Boot autoconfiguration
SonataProperties.java New configuration properties class for Sonata settings
ResourceManagerXA.java Added XA_RETRY error code handling for retryable commit/rollback failures
DataSourceProxyXA.java Added Sonata shim initialization, helper transaction management, and data structures for tracking dummy keys
ConnectionProxyXA.java Implemented Sonata pre-prepare logic with dummy writes, helper transaction lifecycle, and enhanced error handling
pom.xml Added PostgreSQL JDBC driver dependency
DefaultValues.java Added default values for Sonata configuration properties
ConfigurationKeys.java Added configuration keys for Sonata features

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1286 to +1291
String SONATA_PREFIX = "sonata.";
String SONATA_ENABLE_GLOBAL_SERIALIZABILITY = SONATA_PREFIX + "enableGlobalSerializability";
String SONATA_DUMMY_TABLE = SONATA_PREFIX + "dummyTable";
String SONATA_DUMMY_TABLE_SIZE = SONATA_PREFIX + "dummyTableSize";
String SONATA_S2PL_DUMMY_WRITE_RETRY_WARNING_THRESHOLD = SONATA_PREFIX + "s2plDummyWriteRetryWarningThreshold";
String SONATA_SSI_HELPER_BATCH_SIZE = SONATA_PREFIX + "ssiHelperBatchSize";
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

The SONATA_PREFIX uses a different pattern than other configuration prefixes in this file. Most other prefixes in ConfigurationKeys don't include a trailing dot and use hierarchical structure (e.g., SERVER_PREFIX, CLIENT_PREFIX). However, in StarterConstants.java line 46, SONATA_PREFIX is defined as SEATA_PREFIX + '.sonata', which would make it 'seata.sonata'. This inconsistency could lead to configuration key mismatches. Consider either removing the trailing dot here to match the pattern, or ensure the prefix properly aligns with the StarterConstants definition.

Suggested change
String SONATA_PREFIX = "sonata.";
String SONATA_ENABLE_GLOBAL_SERIALIZABILITY = SONATA_PREFIX + "enableGlobalSerializability";
String SONATA_DUMMY_TABLE = SONATA_PREFIX + "dummyTable";
String SONATA_DUMMY_TABLE_SIZE = SONATA_PREFIX + "dummyTableSize";
String SONATA_S2PL_DUMMY_WRITE_RETRY_WARNING_THRESHOLD = SONATA_PREFIX + "s2plDummyWriteRetryWarningThreshold";
String SONATA_SSI_HELPER_BATCH_SIZE = SONATA_PREFIX + "ssiHelperBatchSize";
String SONATA_PREFIX = "sonata";
String SONATA_ENABLE_GLOBAL_SERIALIZABILITY = SONATA_PREFIX + ".enableGlobalSerializability";
String SONATA_DUMMY_TABLE = SONATA_PREFIX + ".dummyTable";
String SONATA_DUMMY_TABLE_SIZE = SONATA_PREFIX + ".dummyTableSize";
String SONATA_S2PL_DUMMY_WRITE_RETRY_WARNING_THRESHOLD = SONATA_PREFIX + ".s2plDummyWriteRetryWarningThreshold";
String SONATA_SSI_HELPER_BATCH_SIZE = SONATA_PREFIX + ".ssiHelperBatchSize";

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +14
public class SonataProperties {
private boolean enableGlobalSerializability;
private String dummyTable;
private int dummyTableSize;
private int s2plDummyWriteRetryWarningThreshold;
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

The SonataProperties class lacks documentation explaining the purpose of each configuration property. Consider adding JavaDoc comments to describe what 'enableGlobalSerializability', 'dummyTable', 'dummyTableSize', 's2plDummyWriteRetryWarningThreshold', and 'ssiHelperBatchSize' represent and how they affect Sonata's behavior.

Suggested change
public class SonataProperties {
private boolean enableGlobalSerializability;
private String dummyTable;
private int dummyTableSize;
private int s2plDummyWriteRetryWarningThreshold;
public class SonataProperties {
/**
* Whether to enable global serializability for transactions.
* If true, Sonata will enforce serializability across all transactions, which may impact performance.
*/
private boolean enableGlobalSerializability;
/**
* The name of the dummy table used for certain concurrency control mechanisms.
* This table is used internally by Sonata for lock management or dummy writes.
*/
private String dummyTable;
/**
* The number of rows in the dummy table.
* This determines the size of the dummy table used for lock striping or contention reduction.
*/
private int dummyTableSize;
/**
* The threshold for logging a warning when dummy write retries exceed this value in S2PL (Strict Two-Phase Locking) mode.
* Helps to detect and alert on excessive contention or retry attempts.
*/
private int s2plDummyWriteRetryWarningThreshold;
/**
* The batch size used by the SSI (Serializable Snapshot Isolation) helper.
* Controls how many records are processed in a single batch for SSI-related operations.
*/

Copilot uses AI. Check for mistakes.
Comment on lines +578 to +579
throw new RuntimeException(
String.format("Global txn branch (%s) already associated with dummy key (%d)", xid, prev));
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

The error message in getS2plDummyKey includes a comment (line 577) stating 'We don't know why this would happen (though programmatically possible) so we do not handle it.' This suggests the error is unexpected. Consider using a more specific exception type (e.g., IllegalStateException) and providing more actionable guidance in the error message for debugging purposes.

Suggested change
throw new RuntimeException(
String.format("Global txn branch (%s) already associated with dummy key (%d)", xid, prev));
throw new IllegalStateException(
String.format("Invariant violation: Global txn branch (%s) is already associated with dummy key (%d) in XID_TO_DUMMY_KEY. This indicates a possible logic error or concurrency issue. Please check the transaction lifecycle and report this stack trace to the Seata maintainers. [ACTIVE_DUMMY_KEYS=%s, XID_TO_DUMMY_KEY.size=%d]",
xid, prev, ((DataSourceProxyXA) resource).ACTIVE_DUMMY_KEYS, ((DataSourceProxyXA) resource).XID_TO_DUMMY_KEY.size()));

Copilot uses AI. Check for mistakes.
Comment on lines +664 to +665
throw new RuntimeException(
String.format("Global txn branch (%s) already associated with dummy key (%d)", xid, prevDummy));
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

Similar to getS2plDummyKey, this RuntimeException in getSsiDummyKey should use a more specific exception type like IllegalStateException to better convey that this represents an unexpected internal state violation.

Copilot uses AI. Check for mistakes.
Integer prevHelper =
((DataSourceProxyXA) resource).XID_TO_HELPER_ID.putIfAbsent(xid, keyAndHelperId.getValue());
if (prevHelper != null) {
throw new RuntimeException(String.format(
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

This RuntimeException should also use a more specific exception type like IllegalStateException for consistency with similar state violation errors.

Suggested change
throw new RuntimeException(String.format(
throw new IllegalStateException(String.format(

Copilot uses AI. Check for mistakes.
// release the pending helpers.
public void releaseAllHelpers() throws SQLException {
if (!sonataSsiShimEnabled) {
throw new RuntimeException("Sonata SSI helper transactions are not allowed for the datasource");
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

The error message in releaseAllHelpers should be more specific about why helper transactions are not allowed. Consider including information about the database type requirement (PostgreSQL) and the configuration state (sonataSsiShimEnabled).

Suggested change
throw new RuntimeException("Sonata SSI helper transactions are not allowed for the datasource");
throw new RuntimeException("Sonata SSI helper transactions are only allowed when using a PostgreSQL database and when sonataSsiShimEnabled is true.");

Copilot uses AI. Check for mistakes.

protected Connection getSsiHelperConnection() throws SQLException {
if (!sonataSsiShimEnabled) {
throw new RuntimeException("Sonata SSI helper transactions are not allowed for the datasource");
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

The error message in getSsiHelperConnection should be more specific about the requirements, similar to the issue in releaseAllHelpers. Additionally, both methods throw the same generic error message, which could be extracted as a constant for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +741 to +752
while (affected == 0) {
affected =
stmt.executeUpdate("insert into `" + DUMMY_TABLE + "` (`key`, value) values (" + key + ","
+ value + ") on duplicate key update value="
+ value);
retry++;
if (retry > S2PL_UPDATE_RETRY_WARNING_THRESHOLD) {
LOGGER.warn(
"MySQL random dummy writes generated conflict with existing rows in {} consecutive attempts! This is super unlikely to occur, please investigate.",
retry);
}
}
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

The SQL query construction in sonataPrePrepare uses string concatenation with user-controlled values ('key' and 'value'), which could potentially lead to SQL injection if DUMMY_TABLE name is user-configurable. Consider using PreparedStatement instead of Statement with string concatenation for better security and performance.

Copilot uses AI. Check for mistakes.
Comment on lines +758 to +765
try (Statement stmt = createStatement()) {
// Unlike MySQL, PostgreSQL does not distinguish matched but value-unchanged rows.
int affected;
try {
affected = stmt.executeUpdate(
"insert into \"" + DUMMY_TABLE + "\" (key, value) values (" + key + "," + value
+ ") on conflict (key) do update set value="
+ value);
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

Similar to the MySQL case, this PostgreSQL SQL query uses string concatenation which poses a SQL injection risk. Use PreparedStatement instead for safer and more efficient query execution.

Suggested change
try (Statement stmt = createStatement()) {
// Unlike MySQL, PostgreSQL does not distinguish matched but value-unchanged rows.
int affected;
try {
affected = stmt.executeUpdate(
"insert into \"" + DUMMY_TABLE + "\" (key, value) values (" + key + "," + value
+ ") on conflict (key) do update set value="
+ value);
String sql = "insert into \"" + DUMMY_TABLE + "\" (key, value) values (?, ?) "
+ "on conflict (key) do update set value = ?";
try (java.sql.PreparedStatement pstmt = getTargetConnection().prepareStatement(sql)) {
// Unlike MySQL, PostgreSQL does not distinguish matched but value-unchanged rows.
int affected;
try {
pstmt.setInt(1, key);
pstmt.setInt(2, value);
pstmt.setInt(3, value);
affected = pstmt.executeUpdate();

Copilot uses AI. Check for mistakes.
try (Statement helperStmt = helperConn.createStatement()) {
for (Integer dummyKey : newBatch) {
try (ResultSet rs = helperStmt.executeQuery(
"select count(*) from \"" + DUMMY_TABLE + "\" where key=" + dummyKey)) {
Copy link

Copilot AI Nov 1, 2025

Choose a reason for hiding this comment

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

The query construction in getSsiDummyKey uses string concatenation for the dummy key value. While 'dummyKey' is an integer (less risky), using PreparedStatement would be more consistent with SQL best practices and provide better protection against potential issues.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants