Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions .github/workflows/transaction-ssi-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
name: Transaction SSI CI

# Triggered on every push / PR that touches the SSI implementation in the
# TransactionManager / LockManager sources or the dedicated test file.
# Targets v1.8.0
# (roadmap:122:transaction:v1.8.0:serializable-snapshot-isolation-ssi).
on:
push:
branches:
- main
- develop
paths:
- 'include/transaction/isolation_level.h'
- 'include/transaction/lock_manager.h'
- 'include/transaction/transaction_manager.h'
- 'src/transaction/lock_manager.cpp'
- 'src/transaction/transaction_manager.cpp'
- 'src/storage/transaction_retry_manager.cpp'
- 'include/storage/transaction_retry_manager.h'
- 'tests/test_transaction_ssi.cpp'
- 'cmake/CMakeLists.txt'
- 'cmake/ModularBuild.cmake'
- 'tests/CMakeLists.txt'
- '.github/workflows/transaction-ssi-ci.yml'
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'include/transaction/isolation_level.h'
- 'include/transaction/lock_manager.h'
- 'include/transaction/transaction_manager.h'
- 'src/transaction/lock_manager.cpp'
- 'src/transaction/transaction_manager.cpp'
- 'src/storage/transaction_retry_manager.cpp'
- 'include/storage/transaction_retry_manager.h'
- 'tests/test_transaction_ssi.cpp'
- 'cmake/CMakeLists.txt'
- 'cmake/ModularBuild.cmake'
- 'tests/CMakeLists.txt'
- '.github/workflows/transaction-ssi-ci.yml'
workflow_dispatch:

concurrency:
group: transaction-ssi-${{ github.ref }}
cancel-in-progress: true

jobs:
ci-scope-classifier:
permissions:
contents: read
uses: ./.github/workflows/ci-scope-classifier.yml

# ---------------------------------------------------------------------------
# Build and run the SSI test suite (TransactionSSITests).
# Tests cover all acceptance criteria for v1.8.0:
# AC-1 Predicate lock tracking for range queries
# AC-2 Read-write conflict detection
# AC-3 Write-write conflict detection
# AC-4 Automatic serialization failure detection
# AC-5 Transaction retry with exponential backoff
# AC-6 SIREAD locks for reads that may cause conflicts
# AC-7 Commit-time validation of read/write sets
# AC-8 IsolationLevel::SerializableSnapshot alias
# AC-9 SSIConfig setSSIConfig() / getSSIConfig()
# AC-10 SSIConfig max_predicate_locks enforcement
# AC-11 SSIConfig enable_predicate_locking = false disables SSI
# AC-12 detectConflicts() returns conflicts for SERIALIZABLE txn
# AC-13 detectConflicts() returns empty for non-SERIALIZABLE txn
# AC-14 Write-skew prevention detected by predicate locking
# AC-15 Predicate locks released on commit
# AC-16 Predicate locks released on rollback
# ---------------------------------------------------------------------------
transaction-ssi-unit-tests:
needs: ci-scope-classifier
if: needs.ci-scope-classifier.outputs.has_code_changes == 'true'
name: Transaction SSI tests (${{ matrix.os }} / ${{ matrix.compiler }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60

permissions:
contents: read

strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
compiler: gcc-12
cc: gcc-12
cxx: g++-12
- os: ubuntu-22.04
compiler: clang-15
cc: clang-15
cxx: clang++-15
- os: ubuntu-24.04
compiler: gcc-13
cc: gcc-13
cxx: g++-13

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up C++ build environment
uses: ./.github/actions/setup-cpp-build
with:
cc: ${{ matrix.cc }}
cxx: ${{ matrix.cxx }}
extra-packages: libssl-dev libboost-all-dev librocksdb-dev libfmt-dev libtbb-dev libspdlog-dev nlohmann-json3-dev

- name: Configure and build (SSI test target)
uses: ./.github/actions/configure-themis
with:
cc: ${{ matrix.cc }}
cxx: ${{ matrix.cxx }}
build-target: test_transaction_ssi

- name: Run Transaction SSI unit tests
run: |
set -o pipefail
cd build
ctest --test-dir . \
--tests-regex TransactionSSITests \
--output-on-failure \
--timeout 120 \
2>&1 | tee transaction_ssi_test_output.txt

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: transaction-ssi-results-${{ matrix.os }}-${{ matrix.compiler }}
path: |
build/transaction_ssi_test_output.txt
retention-days: 14

- name: Write job summary
if: always()
run: |
echo "## 🔒 Transaction SSI – Unit Tests" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Parameter | Value |" >> "$GITHUB_STEP_SUMMARY"
echo "|-----------|-------|" >> "$GITHUB_STEP_SUMMARY"
echo "| **OS** | \`${{ matrix.os }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Compiler** | \`${{ matrix.compiler }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Event** | \`${{ github.event_name }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Branch** | \`${{ github.ref_name }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Commit** | \`${{ github.sha }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| **Triggered by** | ${{ github.actor }} |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "SSI: IsolationLevel::SerializableSnapshot, predicate lock tracking, read-write conflict detection, write-write conflict detection, serialization failure, transaction retry with exponential backoff, SSIConfig, detectConflicts, write-skew prevention." >> "$GITHUB_STEP_SUMMARY"
7 changes: 4 additions & 3 deletions include/transaction/isolation_level.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ namespace themis {
///
/// Note: value 2 is intentionally reserved (gap between READ_COMMITTED=1 and
/// REPEATABLE_READ=3) to preserve backward compatibility with the legacy Snapshot=3
/// alias.
/// alias. SerializableSnapshot is an alias for SERIALIZABLE (both equal 4).
enum class IsolationLevel {
// Legacy aliases preserved for backward compatibility
ReadCommitted = 1, ///< Same as READ_COMMITTED
Snapshot = 3, ///< Same as REPEATABLE_READ (snapshot isolation)
ReadCommitted = 1, ///< Same as READ_COMMITTED
Snapshot = 3, ///< Same as REPEATABLE_READ (snapshot isolation)
SerializableSnapshot = 4, ///< Same as SERIALIZABLE (SSI – predicate locking)

// Standard SQL names
READ_UNCOMMITTED = 0, ///< Lowest isolation; no extra read locks
Expand Down
44 changes: 41 additions & 3 deletions include/transaction/lock_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,37 @@ class LockManager {
/// @param start_key Lower bound of the predicate range (inclusive).
/// @param end_key Upper bound of the predicate range (inclusive; may
/// equal @p start_key for a single-key predicate).
/// @return Always returns true; the predicate lock is always recorded.
/// Conflict detection is performed lazily at write time via
/// checkPredicateConflict(), not at acquire time.
/// @return true when the lock was recorded; false when the global limit
/// set by setMaxPredicateLocks() has been reached (the lock is
/// silently dropped in that case – the false-positive abort rate
/// may increase but correctness is preserved).
bool acquirePredicateLock(TransactionId txn_id,
const std::string& start_key,
const std::string& end_key);

/// Set the maximum total number of predicate locks that may be held
/// simultaneously across all active transactions.
///
/// Once the limit is reached, acquirePredicateLock() returns false and
/// does not record the lock. Pass 0 to disable the limit (default).
///
/// Thread-safe.
void setMaxPredicateLocks(size_t max_locks);

/// Return the current maximum predicate-lock limit (0 = unlimited).
size_t getMaxPredicateLocks() const;

/// Enable or disable predicate-lock tracking globally.
///
/// When disabled, acquirePredicateLock() is a no-op (returns false) and
/// checkPredicateConflict() always returns 0.
///
/// Thread-safe.
void setPredicateLockingEnabled(bool enabled);

/// Return whether predicate-lock tracking is currently enabled.
bool isPredicateLockingEnabled() const;

/// Release all predicate locks held by @p txn_id.
///
/// Must be called when a SERIALIZABLE transaction commits or rolls back.
Expand All @@ -178,6 +202,14 @@ class LockManager {
/// Return the number of predicate locks currently held by @p txn_id.
size_t getPredicateLockCount(TransactionId txn_id) const;

/// Return all predicate-lock ranges held by @p txn_id as (start_key, end_key) pairs.
///
/// The returned vector is a snapshot copy; the caller may iterate it without
/// holding any lock. An empty vector is returned when txn_id has no
/// predicate locks or when predicate locking is disabled.
std::vector<std::pair<std::string, std::string>> getPredicateLockRanges(
TransactionId txn_id) const;

private:
/// One active lock holder for a key.
struct LockEntry {
Expand Down Expand Up @@ -254,6 +286,12 @@ class LockManager {

/// All active predicate locks, protected by mutex_.
std::vector<PredicateLock> predicate_locks_;

/// Maximum total predicate locks allowed (0 = unlimited), protected by mutex_.
std::atomic<size_t> max_predicate_locks_{0};

/// Whether predicate-lock tracking is enabled (default: true).
std::atomic<bool> predicate_locking_enabled_{true};
};

} // namespace themis
91 changes: 90 additions & 1 deletion include/transaction/transaction_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -263,13 +263,18 @@ class TransactionManager {
// ── Serializable Snapshot Isolation (SSI) / Predicate Locking ────────

/**
* @brief Track a range predicate read for SERIALIZABLE isolation (SSI).
* @brief Acquire a SIREAD (predicate) lock for SERIALIZABLE isolation (SSI).
*
* Records that this transaction has read all keys in the closed interval
* [@p start_key, @p end_key]. Any other SERIALIZABLE transaction that
* subsequently writes a key inside this range will be detected as a
* serialization conflict and aborted.
*
* This is the SIREAD ("Serializable Isolation READ") lock described in
* the SSI literature. Unlike 2PL read locks, SIREAD locks do not block
* concurrent writers; conflicts are detected lazily at write time, which
* makes this approach better than traditional 2PL for read-heavy workloads.
*
* No-op when the isolation level is not SERIALIZABLE.
*
* @param start_key Lower bound of the range (inclusive).
Expand Down Expand Up @@ -1082,6 +1087,86 @@ class TransactionManager {
std::string_view table,
std::string_view pk) const;

// ── Serializable Snapshot Isolation (SSI) configuration ──────────────────

/**
* @brief Tuning parameters for Serializable Snapshot Isolation (SSI).
*
* These settings control the predicate-lock subsystem used by
* SERIALIZABLE transactions. Adjust them to balance memory usage, false-
* positive abort rate, and conflict-detection latency.
*/
struct SSIConfig {
/// Enable or disable predicate lock tracking. When false, SERIALIZABLE
/// transactions behave identically to REPEATABLE_READ (snapshot isolation
/// only; write-skew anomalies are not detected).
bool enable_predicate_locking = true;

/// Maximum total number of predicate locks that may be held
/// simultaneously across all active transactions. Once the limit is
/// reached, new acquirePredicateLock() calls are silently dropped,
/// which may increase the false-positive abort rate.
size_t max_predicate_locks = 10000;

/// How often the background conflict-detection sweep (if any) is
/// triggered. Currently informational; no background sweep is
/// implemented – conflict detection is performed inline at write time.
std::chrono::milliseconds conflict_detection_interval{100};
};

/**
* @brief Update the SSI configuration.
*
* Thread-safe. New values take effect immediately for all subsequent
* predicate-lock operations; existing in-flight locks are unaffected.
*
* @param config New SSI tuning parameters.
*/
void setSSIConfig(const SSIConfig& config);

/**
* @brief Return the currently active SSI configuration.
*/
SSIConfig getSSIConfig() const;

/**
* @brief Describes a single read-write or write-write serialization
* conflict detected for a SERIALIZABLE transaction.
*/
struct SerializationConflict {
/// The transaction ID of the other transaction involved in the conflict.
TransactionId other_txn_id{0};

/// The storage key that triggered the conflict.
std::string key;

/// Human-readable description of the conflict kind.
/// "read-write" – this transaction's read range overlaps a write by
/// @p other_txn_id (phantom / write-skew risk).
/// "write-write" – both transactions wrote the same key concurrently
/// (lost-update risk).
std::string conflict_type;

/// Human-readable explanation.
std::string message;
};

/**
* @brief Enumerate predicate-lock conflicts for a SERIALIZABLE transaction.
*
* Scans every predicate lock held by @p txn_id against the predicate locks
* held by all other active SERIALIZABLE transactions and returns one
* SerializationConflict entry for each key range that would produce a
* serialization failure.
*
* Returns an empty vector for non-SERIALIZABLE transactions or when
* predicate locking is disabled.
*
* @param txn_id Transaction to analyse.
* @return List of detected conflicts; empty when none exist.
*/
std::vector<SerializationConflict> detectConflicts(TransactionId txn_id) const;

private:
RocksDBWrapper& db_;
SecondaryIndexManager& secIdx_;
Expand Down Expand Up @@ -1185,6 +1270,10 @@ class TransactionManager {
/// Stored atomically so setDeadlockPredictor() can be called concurrently
/// with predict/recommend helpers without introducing a data race.
std::atomic<DeadlockPredictor*> deadlock_predictor_{nullptr};

// SSI configuration – protected by ssi_config_mutex_
mutable std::mutex ssi_config_mutex_;
SSIConfig ssi_config_;
};

} // namespace themis
4 changes: 3 additions & 1 deletion src/storage/transaction_retry_manager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ ErrorType TransactionRetryManager::classifyError(const std::string& error_messag
// Retryable errors
if (lower_msg.find("conflict") != std::string::npos ||
lower_msg.find("write conflict") != std::string::npos ||
lower_msg.find("concurrent") != std::string::npos) {
lower_msg.find("concurrent") != std::string::npos ||
lower_msg.find("serialization failure") != std::string::npos ||
lower_msg.find("transaction must be retried") != std::string::npos) {
return ErrorType::WRITE_CONFLICT;
}

Expand Down
Loading
Loading