diff --git a/cc/ss2pl/CMakeLists.txt b/cc/ss2pl/CMakeLists.txt
index 4906a488..5aa2e28f 100644
--- a/cc/ss2pl/CMakeLists.txt
+++ b/cc/ss2pl/CMakeLists.txt
@@ -1,7 +1,15 @@
+if(CCBENCH_SS2PL_DLR STREQUAL "0")
+ set(SS2PL_DLR_MARKER DLR0)
+elseif(CCBENCH_SS2PL_DLR STREQUAL "1")
+ set(SS2PL_DLR_MARKER DLR1)
+else()
+ message(FATAL_ERROR "CCBENCH_SS2PL_DLR must be 0 or 1")
+endif()
+
ccbench_add_protocol(ss2pl
SOURCES transaction.cc util.cc
WORKLOADS ycsb bomb tpcc
OPTIONS
- DLR1
+ ${SS2PL_DLR_MARKER}
KEY_SORT=${CCBENCH_KEY_SORT}
)
diff --git a/cc/ss2pl/README.md b/cc/ss2pl/README.md
index 73c470b1..d559f175 100644
--- a/cc/ss2pl/README.md
+++ b/cc/ss2pl/README.md
@@ -40,8 +40,15 @@ default : `0`
default : `1`
- `VAL_SIZE` : Value of key-value size. In other words, payload size.
default : `4`
-- `DLR0` : Dead lock resolution is timeout.
-- `DLR1` : Dead lock resolution is no-wait.
+- Dead lock resolution is selected at configure time with the
+`CCBENCH_SS2PL_DLR` cache variable, which defines exactly one of the
+markers below.
+default : `1`
+- `DLR0` : Timeout. Every record lock acquisition retries until it
+succeeds or `-ss2pl_dlr0_timeout_us` elapses; on expiry the transaction
+aborts through the same path as a no-wait abort.
+- `DLR1` : No-wait. A lock acquisition that cannot succeed immediately
+aborts the transaction.
## Optimizations
- Backoff.
diff --git a/cc/ss2pl/include/common.hh b/cc/ss2pl/include/common.hh
index 5b504661..0918982b 100644
--- a/cc/ss2pl/include/common.hh
+++ b/cc/ss2pl/include/common.hh
@@ -52,4 +52,13 @@ DECLARE_bool(ycsb);
DECLARE_double(zipf_skew);
#endif
+#if defined(DLR0)
+#ifdef GLOBAL_VALUE_DEFINE
+DEFINE_uint64(ss2pl_dlr0_timeout_us, 1000,
+ "ss2pl DLR0 lock timeout in microseconds.");
+#else
+DECLARE_uint64(ss2pl_dlr0_timeout_us);
+#endif
+#endif
+
alignas(CACHE_LINE_SIZE) GLOBAL uint32_t TotalThreadNum;
diff --git a/cc/ss2pl/include/dlr0_timeout.hh b/cc/ss2pl/include/dlr0_timeout.hh
new file mode 100644
index 00000000..805121bb
--- /dev/null
+++ b/cc/ss2pl/include/dlr0_timeout.hh
@@ -0,0 +1,49 @@
+#pragma once
+
+#if defined(DLR0)
+
+#include
+#include
+
+#include "../../../include/tsc.hh"
+#include "common.hh"
+
+inline bool ss2pl_dlr0_try_read_once(ReaderWriteLock& lock) {
+ int expected = lock.counter.load(std::memory_order_acquire);
+ if (expected == -1) return false;
+
+ return lock.counter.compare_exchange_strong(expected, expected + 1,
+ std::memory_order_acq_rel,
+ std::memory_order_acquire);
+}
+
+inline bool ss2pl_dlr0_try_write_once(ReaderWriteLock& lock) {
+ int expected = lock.counter.load(std::memory_order_acquire);
+ if (expected != 0) return false;
+
+ return lock.counter.compare_exchange_strong(
+ expected, -1, std::memory_order_acq_rel, std::memory_order_acquire);
+}
+
+inline bool ss2pl_dlr0_try_upgrade_once(ReaderWriteLock& lock) {
+ int expected = lock.counter.load(std::memory_order_acquire);
+ if (expected != 1) return false;
+
+ return lock.counter.compare_exchange_strong(
+ expected, -1, std::memory_order_acq_rel, std::memory_order_acquire);
+}
+
+template
+inline bool ss2pl_dlr0_lock_until_timeout(TryOnce&& try_once) {
+ const std::uint64_t timeout_clocks =
+ FLAGS_ss2pl_dlr0_timeout_us * FLAGS_clocks_per_us;
+ const std::uint64_t start = rdtscp();
+
+ for (;;) {
+ if (try_once()) return true;
+ if (rdtscp() - start >= timeout_clocks) return false;
+ _mm_pause();
+ }
+}
+
+#endif
diff --git a/cc/ss2pl/transaction.cc b/cc/ss2pl/transaction.cc
index 4e5a7ed8..8936b2f1 100644
--- a/cc/ss2pl/transaction.cc
+++ b/cc/ss2pl/transaction.cc
@@ -9,6 +9,9 @@
#include "../../include/procedure.hh"
#include "../../include/result.hh"
#include "include/common.hh"
+#if defined(DLR0)
+#include "include/dlr0_timeout.hh"
+#endif
#include "include/transaction.hh"
using namespace std;
@@ -155,6 +158,11 @@ Status TxExecutor::read(Storage s, std::string_view key, TupleBody** body) {
if (tuple == nullptr) return Status::WARN_NOT_FOUND;
read_internal(s, key, tuple);
+#if defined(DLR0)
+ if (this->status_ == TransactionStatus::aborted) {
+ return Status::ERROR_LOCK_FAILED;
+ }
+#endif
*body = &(read_set_.back().body_);
FINISH_READ:
@@ -169,11 +177,12 @@ void TxExecutor::read_internal(Storage s, std::string_view key, Tuple* tuple) {
if (reconnoitering_) goto FINISH_READ_LOCK;
-#ifdef DLR0
- /**
- * Acquire lock with wait.
- */
- tuple->lock_.r_lock();
+#if defined(DLR0)
+ if (!ss2pl_dlr0_lock_until_timeout(
+ [&]() { return ss2pl_dlr0_try_read_once(tuple->lock_); })) {
+ this->status_ = TransactionStatus::aborted;
+ goto FINISH_READ;
+ }
r_lock_list_.emplace_back(&tuple->lock_);
#elif defined(DLR1)
if (tuple->lock_.r_trylock()) {
@@ -260,9 +269,10 @@ Status TxExecutor::update(Storage s, std::string_view key, TupleBody&& body) {
for (auto rItr = read_set_.begin(); rItr != read_set_.end(); ++rItr) {
if ((*rItr).storage_ != s) continue;
if ((*rItr).key_ == key) { // hit
-#if DLR0
- // Workaround for handling static BoMB properly
- if (!(*rItr).rcdptr_->lock_.tryupgrade()) {
+#if defined(DLR0)
+ if (!ss2pl_dlr0_lock_until_timeout([&]() {
+ return ss2pl_dlr0_try_upgrade_once((*rItr).rcdptr_->lock_);
+ })) {
this->status_ = TransactionStatus::aborted;
goto FINISH_WRITE;
}
@@ -306,11 +316,12 @@ Status TxExecutor::update(Storage s, std::string_view key, TupleBody&& body) {
#endif
if (tuple == nullptr) return Status::WARN_NOT_FOUND;
-#if DLR0
- /**
- * Lock with wait.
- */
- tuple->lock_.w_lock();
+#if defined(DLR0)
+ if (!ss2pl_dlr0_lock_until_timeout(
+ [&]() { return ss2pl_dlr0_try_write_once(tuple->lock_); })) {
+ this->status_ = TransactionStatus::aborted;
+ goto FINISH_WRITE;
+ }
#elif defined(DLR1)
if (!tuple->lock_.w_trylock()) {
/**
@@ -331,6 +342,11 @@ Status TxExecutor::update(Storage s, std::string_view key, TupleBody&& body) {
#if ADD_ANALYSIS
result_->local_write_latency_ += rdtscp() - start;
#endif // ADD_ANALYSIS
+#if defined(DLR0)
+ if (this->status_ == TransactionStatus::aborted) {
+ return Status::ERROR_LOCK_FAILED;
+ }
+#endif
return Status::OK;
}
@@ -408,8 +424,12 @@ Status TxExecutor::read_lock(Storage s, std::string_view key) {
if (w_lock == &tuple->lock_) { return Status::OK; }
}
-#ifdef DLR0
- tuple->lock_.r_lock();
+#if defined(DLR0)
+ if (!ss2pl_dlr0_lock_until_timeout(
+ [&]() { return ss2pl_dlr0_try_read_once(tuple->lock_); })) {
+ this->status_ = TransactionStatus::aborted;
+ return Status::ERROR_LOCK_FAILED;
+ }
#elif defined(DLR1)
if (!tuple->lock_.r_trylock()) {
this->status_ = TransactionStatus::aborted;
@@ -433,8 +453,12 @@ Status TxExecutor::write_lock(Storage s, std::string_view key) {
if (w_lock == &tuple->lock_) { return Status::OK; }
}
-#if DLR0
- tuple->lock_.w_lock();
+#if defined(DLR0)
+ if (!ss2pl_dlr0_lock_until_timeout(
+ [&]() { return ss2pl_dlr0_try_write_once(tuple->lock_); })) {
+ this->status_ = TransactionStatus::aborted;
+ return Status::ERROR_LOCK_FAILED;
+ }
#elif defined(DLR1)
if (!tuple->lock_.w_trylock()) {
this->status_ = TransactionStatus::aborted;
diff --git a/cc/ss2pl/util.cc b/cc/ss2pl/util.cc
index a2b20f4e..9ca5ca53 100644
--- a/cc/ss2pl/util.cc
+++ b/cc/ss2pl/util.cc
@@ -30,6 +30,14 @@ void chkArg() {
if (FLAGS_rratio > 100) { ERR; }
+#if defined(DLR0)
+ if (FLAGS_clocks_per_us == 0 ||
+ FLAGS_ss2pl_dlr0_timeout_us >
+ std::numeric_limits::max() / FLAGS_clocks_per_us) {
+ ERR;
+ }
+#endif
+
TotalThreadNum = FLAGS_thread_num;
if (FLAGS_clocks_per_us < 100) {
@@ -50,6 +58,10 @@ void displayParameter() {
cout << "#FLAGS_tuple_num:\t" << FLAGS_tuple_num << endl;
cout << "#FLAGS_ycsb:\t\t" << FLAGS_ycsb << endl;
cout << "#FLAGS_zipf_skew:\t" << FLAGS_zipf_skew << endl;
+#if defined(DLR0)
+ cout << "#FLAGS_ss2pl_dlr0_timeout_us:\t" << FLAGS_ss2pl_dlr0_timeout_us
+ << endl;
+#endif
}
void partTableInit([[maybe_unused]] size_t thid,
diff --git a/cmake/Options.cmake b/cmake/Options.cmake
index 194a8cf6..842c1bf3 100644
--- a/cmake/Options.cmake
+++ b/cmake/Options.cmake
@@ -42,6 +42,7 @@ set(CCBENCH_PROCEDURE_SORT 0 CACHE STRING "silo")
set(CCBENCH_WAL 0 CACHE STRING "silo")
set(CCBENCH_TEMPERATURE_RESET_OPT 1 CACHE STRING "mocc")
set(CCBENCH_WORKER1_INSERT_DELAY_RPHASE 0 CACHE STRING "cicada")
+set(CCBENCH_SS2PL_DLR 1 CACHE STRING "ss2pl: 0=timeout, 1=no-wait")
# Cicada and Oze share many flags but disagree on this one's default.
# Keep two separate cache entries; protocols pick the one they want.
diff --git a/docs/runtime-args_en.md b/docs/runtime-args_en.md
index 34f25510..45f1446d 100644
--- a/docs/runtime-args_en.md
+++ b/docs/runtime-args_en.md
@@ -64,6 +64,17 @@ BoMB has many knobs; the ones below are the most common. See
| `-bomb_use_cache` | `false` | Use cached BoM tree (static setting). |
| `-bomb_rate_control` | `false` | Enable rate-limited request injection. |
+## SS2PL-specific flags (`*_ss2pl.exe`, only when built with `CCBENCH_SS2PL_DLR=0`)
+
+The deadlock resolution strategy is chosen at configure time with
+`-DCCBENCH_SS2PL_DLR=<0|1>`. The default is `1` (no-wait), which is the
+historical behaviour. Only a binary built with `0` (timeout) accepts the flag
+below.
+
+| Flag | Default | Meaning |
+|---|---|---|
+| `-ss2pl_dlr0_timeout_us` | `1000` | Upper bound, in microseconds, on how long a single record lock acquisition waits. The transaction aborts when it expires. The bound is converted to TSC ticks with `-clocks_per_us`, so that value has to be correct. |
+
## Examples
TPC-C on Silo with 8 warehouses:
diff --git a/docs/runtime-args_ja.md b/docs/runtime-args_ja.md
index 06e00536..09170a3f 100644
--- a/docs/runtime-args_ja.md
+++ b/docs/runtime-args_ja.md
@@ -56,6 +56,16 @@ BoMB には多くの knob がある。下記はよく使うものだけ。ワー
| `-bomb_use_cache` | `false` | キャッシュ済みの BoM tree を使う (静的設定)。 |
| `-bomb_rate_control` | `false` | レート制限付きの request injection を有効化。 |
+## SS2PL 固有のフラグ (`*_ss2pl.exe`、`CCBENCH_SS2PL_DLR=0` でビルドしたときだけ)
+
+デッドロック解決方式は configure 時に `-DCCBENCH_SS2PL_DLR=<0|1>` で選ぶ。既定は `1`
+(no-wait) で、これは従来の挙動である。`0` (timeout) でビルドしたバイナリだけが下記の
+フラグを受け付ける。
+
+| Flag | デフォルト | 意味 |
+|---|---|---|
+| `-ss2pl_dlr0_timeout_us` | `1000` | 1 回のレコードロック取得を待つ上限 (マイクロ秒)。到達するとトランザクションを abort する。`-clocks_per_us` を使って TSC tick へ換算するので、その値が正しいことが前提。 |
+
## 例
8 warehouse で Silo の TPC-C: