couchbase 3.8.1 → 3.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +2 -2
- data/ext/cache/extconf_include.rb +4 -4
- data/ext/cache/mozilla-ca-bundle.crt +95 -102
- data/ext/cache/mozilla-ca-bundle.sha256 +1 -1
- data/ext/couchbase/CMakeLists.txt +1 -1
- data/ext/couchbase/core/impl/public_cluster.cxx +118 -5
- data/ext/couchbase/core/io/configuration_belongs_to_session.hxx +67 -0
- data/ext/couchbase/core/io/mcbp_session.cxx +7 -16
- data/ext/couchbase/core/io/streams.cxx +143 -12
- data/ext/couchbase/core/io/streams.hxx +6 -0
- data/ext/couchbase/core/operations/management/bucket_create.cxx +37 -38
- data/ext/couchbase/core/operations/management/bucket_update.cxx +26 -28
- data/ext/couchbase/core/transactions/attempt_context_impl.cxx +254 -178
- data/ext/couchbase/core/transactions/attempt_context_impl.hxx +5 -1
- data/ext/couchbase/core/transactions/transactions_cleanup.cxx +42 -7
- data/ext/couchbase/core/transactions/waitable_op_list.hxx +13 -2
- data/ext/couchbase/couchbase/cluster.hxx +33 -0
- data/lib/couchbase/cluster.rb +16 -14
- data/lib/couchbase/options.rb +7 -2
- data/lib/couchbase/version.rb +1 -1
- metadata +6 -5
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
#include "internal/utils.hxx"
|
|
43
43
|
#include "staged_mutation.hxx"
|
|
44
44
|
|
|
45
|
+
#include <cassert>
|
|
45
46
|
#include <optional>
|
|
46
47
|
|
|
47
48
|
namespace couchbase::core::transactions
|
|
@@ -233,10 +234,12 @@ attempt_context_impl::get(const core::document_id& id) -> transaction_get_result
|
|
|
233
234
|
void
|
|
234
235
|
attempt_context_impl::get(const core::document_id& id, Callback&& cb)
|
|
235
236
|
{
|
|
236
|
-
if (op_list_.get_mode().is_query()) {
|
|
237
|
-
return get_with_query(id, false, std::move(cb));
|
|
238
|
-
}
|
|
239
237
|
cache_error_async(cb, [self = shared_from_this(), id, cb]() mutable {
|
|
238
|
+
// Mode is read after cache_error_async incremented the op count, closing the
|
|
239
|
+
// race with set_query_mode() -- see the comment in insert() for the rationale.
|
|
240
|
+
if (self->op_list_.get_mode().is_query()) {
|
|
241
|
+
return self->get_with_query(id, false, std::move(cb));
|
|
242
|
+
}
|
|
240
243
|
self->check_if_done(cb);
|
|
241
244
|
self->do_get(
|
|
242
245
|
id,
|
|
@@ -323,10 +326,12 @@ void
|
|
|
323
326
|
attempt_context_impl::get_optional(const core::document_id& id, Callback&& cb)
|
|
324
327
|
{
|
|
325
328
|
|
|
326
|
-
if (op_list_.get_mode().is_query()) {
|
|
327
|
-
return get_with_query(id, true, std::move(cb));
|
|
328
|
-
}
|
|
329
329
|
cache_error_async(cb, [self = shared_from_this(), id, cb]() mutable {
|
|
330
|
+
// Mode is read after cache_error_async incremented the op count, closing the
|
|
331
|
+
// race with set_query_mode() -- see the comment in insert() for the rationale.
|
|
332
|
+
if (self->op_list_.get_mode().is_query()) {
|
|
333
|
+
return self->get_with_query(id, true, std::move(cb));
|
|
334
|
+
}
|
|
330
335
|
self->ensure_open_bucket(
|
|
331
336
|
id.bucket(), [self, id, cb = std::move(cb)](std::error_code ec) mutable {
|
|
332
337
|
if (ec) {
|
|
@@ -886,11 +891,13 @@ attempt_context_impl::replace(const transaction_get_result& document,
|
|
|
886
891
|
Callback&& cb)
|
|
887
892
|
{
|
|
888
893
|
|
|
889
|
-
if (op_list_.get_mode().is_query()) {
|
|
890
|
-
return replace_raw_with_query(document, std::move(content), std::move(cb));
|
|
891
|
-
}
|
|
892
894
|
return cache_error_async(
|
|
893
895
|
cb, [self = shared_from_this(), cb, document, content = std::move(content)]() mutable {
|
|
896
|
+
// Mode is read after cache_error_async incremented the op count, closing the
|
|
897
|
+
// race with set_query_mode() -- see the comment in insert() for the rationale.
|
|
898
|
+
if (self->op_list_.get_mode().is_query()) {
|
|
899
|
+
return self->replace_raw_with_query(document, std::move(content), std::move(cb));
|
|
900
|
+
}
|
|
894
901
|
self->ensure_open_bucket(
|
|
895
902
|
document.bucket(),
|
|
896
903
|
[self, cb = std::move(cb), document, content = std::move(content)](
|
|
@@ -1026,6 +1033,13 @@ attempt_context_impl::create_staged_replace(
|
|
|
1026
1033
|
const std::optional<document_metadata>& document_metadata,
|
|
1027
1034
|
Handler&& cb)
|
|
1028
1035
|
{
|
|
1036
|
+
// ExtThreadSafety invariant -- see create_staged_insert() for the full rationale and
|
|
1037
|
+
// the spec reference (couchbase-transactions-specs/transactions-extensions.md,
|
|
1038
|
+
// "ExtThreadSafety"). replace() reads the mode after this op is counted, so reaching
|
|
1039
|
+
// this staging point in query mode means that ordering has regressed.
|
|
1040
|
+
assert(!op_list_.query_mode_entered() &&
|
|
1041
|
+
"ExtThreadSafety: KV replace staged after query mode entered -- would be missing "
|
|
1042
|
+
"from BEGIN WORK and lost at commit");
|
|
1029
1043
|
operations::mutate_in_request req{ id };
|
|
1030
1044
|
const bool binary =
|
|
1031
1045
|
codec::codec_flags::has_common_flags(content.flags, codec::codec_flags::binary_common_flags);
|
|
@@ -1274,11 +1288,20 @@ attempt_context_impl::insert(const core::document_id& id,
|
|
|
1274
1288
|
codec::encoded_value content,
|
|
1275
1289
|
Callback&& cb)
|
|
1276
1290
|
{
|
|
1277
|
-
if (op_list_.get_mode().is_query()) {
|
|
1278
|
-
return insert_raw_with_query(id, std::move(content), std::move(cb));
|
|
1279
|
-
}
|
|
1280
1291
|
return cache_error_async(
|
|
1281
1292
|
cb, [self = shared_from_this(), id, cb, content = std::move(content)]() mutable {
|
|
1293
|
+
// The op count was incremented by cache_error_async before this lambda runs.
|
|
1294
|
+
// Reading the mode here -- after the increment -- closes a race with
|
|
1295
|
+
// set_query_mode(): a query switching to query mode first waits for all
|
|
1296
|
+
// in-flight KV ops to drain before issuing BEGIN WORK, so once this op is
|
|
1297
|
+
// counted, either we observe KV mode and our staged mutation is guaranteed to
|
|
1298
|
+
// be part of BEGIN WORK, or we observe query mode and route through query.
|
|
1299
|
+
// Checking the mode before the increment (as the code used to) left a window
|
|
1300
|
+
// where a query could flip the mode in the gap, after which this op would
|
|
1301
|
+
// stage a KV mutation that BEGIN WORK had already missed -- silently losing it.
|
|
1302
|
+
if (self->op_list_.get_mode().is_query()) {
|
|
1303
|
+
return self->insert_raw_with_query(id, std::move(content), std::move(cb));
|
|
1304
|
+
}
|
|
1282
1305
|
self->ensure_open_bucket(
|
|
1283
1306
|
id.bucket(),
|
|
1284
1307
|
[self, id, content = std::move(content), cb = std::move(cb)](std::error_code ec) mutable {
|
|
@@ -1452,10 +1475,12 @@ void
|
|
|
1452
1475
|
attempt_context_impl::remove(const transaction_get_result& document, VoidCallback&& cb)
|
|
1453
1476
|
{
|
|
1454
1477
|
|
|
1455
|
-
if (op_list_.get_mode().is_query()) {
|
|
1456
|
-
return remove_with_query(document, std::move(cb));
|
|
1457
|
-
}
|
|
1458
1478
|
return cache_error_async(cb, [self = shared_from_this(), document, cb]() mutable {
|
|
1479
|
+
// Mode is read after cache_error_async incremented the op count, closing the
|
|
1480
|
+
// race with set_query_mode() -- see the comment in insert() for the rationale.
|
|
1481
|
+
if (self->op_list_.get_mode().is_query()) {
|
|
1482
|
+
return self->remove_with_query(document, std::move(cb));
|
|
1483
|
+
}
|
|
1459
1484
|
self->check_if_done(cb);
|
|
1460
1485
|
self->ensure_open_bucket(
|
|
1461
1486
|
document.bucket(), [self, document, cb = std::move(cb)](std::error_code ec) mutable {
|
|
@@ -1539,6 +1564,15 @@ attempt_context_impl::remove(const transaction_get_result& document, VoidCallbac
|
|
|
1539
1564
|
return error_handler(
|
|
1540
1565
|
*ec, "before_staged_remove hook raised error", std::move(cb));
|
|
1541
1566
|
}
|
|
1567
|
+
// ExtThreadSafety invariant -- see create_staged_insert() for the
|
|
1568
|
+
// full rationale and spec reference
|
|
1569
|
+
// (couchbase-transactions-specs/transactions-extensions.md,
|
|
1570
|
+
// "ExtThreadSafety"). remove() reads the mode after this op is
|
|
1571
|
+
// counted, so reaching this staging point in query mode means that
|
|
1572
|
+
// ordering has regressed.
|
|
1573
|
+
assert(!self->op_list_.query_mode_entered() &&
|
|
1574
|
+
"ExtThreadSafety: KV remove staged after query mode entered -- "
|
|
1575
|
+
"would be missing from BEGIN WORK and lost at commit");
|
|
1542
1576
|
CB_ATTEMPT_CTX_LOG_TRACE(self,
|
|
1543
1577
|
"about to remove doc {} with cas {}",
|
|
1544
1578
|
document.id(),
|
|
@@ -2203,59 +2237,69 @@ make_kv_txdata(std::optional<transaction_get_result> doc = std::nullopt) -> tao:
|
|
|
2203
2237
|
void
|
|
2204
2238
|
attempt_context_impl::get_with_query(const core::document_id& id, bool optional, Callback&& cb)
|
|
2205
2239
|
{
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2240
|
+
// Called only from get()/get_optional(), already within their cache_error_async()
|
|
2241
|
+
// (op counted, existing_error() checked); see insert_raw_with_query() for why we do
|
|
2242
|
+
// not re-wrap.
|
|
2243
|
+
auto self = shared_from_this();
|
|
2244
|
+
couchbase::transactions::transaction_query_options opts;
|
|
2245
|
+
opts.readonly(true);
|
|
2246
|
+
return self->wrap_query(
|
|
2247
|
+
KV_GET,
|
|
2248
|
+
opts,
|
|
2249
|
+
make_params(id, {}),
|
|
2250
|
+
make_kv_txdata(),
|
|
2251
|
+
STAGE_QUERY_KV_GET,
|
|
2252
|
+
true,
|
|
2253
|
+
{},
|
|
2254
|
+
[self, id, optional, cb = std::move(cb)](std::exception_ptr err,
|
|
2255
|
+
core::operations::query_response resp) mutable {
|
|
2256
|
+
if (resp.ctx.ec == couchbase::errc::key_value::document_not_found) {
|
|
2257
|
+
// A non-optional get() must fail rather than report an empty result: its callback contract
|
|
2258
|
+
// (and the synchronous get() wrapper) dereferences the returned optional, so err==nullptr
|
|
2259
|
+
// with res==nullopt would be a null dereference. Mirror the rows-empty handling below and
|
|
2260
|
+
// the KV get() path.
|
|
2261
|
+
if (optional) {
|
|
2220
2262
|
return self->op_completed_with_callback(std::move(cb),
|
|
2221
2263
|
std::optional<transaction_get_result>());
|
|
2222
2264
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
return self->
|
|
2232
|
-
|
|
2233
|
-
transaction_operation_failed(FAIL_DOC_NOT_FOUND, "document not found"));
|
|
2265
|
+
return self->op_completed_with_error(
|
|
2266
|
+
std::move(cb), transaction_operation_failed(FAIL_DOC_NOT_FOUND, "document not found"));
|
|
2267
|
+
}
|
|
2268
|
+
if (!err) {
|
|
2269
|
+
// make a transaction_get_result from the row...
|
|
2270
|
+
try {
|
|
2271
|
+
if (resp.rows.empty()) {
|
|
2272
|
+
if (optional) {
|
|
2273
|
+
return self->op_completed_with_callback(std::move(cb),
|
|
2274
|
+
std::optional<transaction_get_result>());
|
|
2234
2275
|
}
|
|
2235
|
-
CB_ATTEMPT_CTX_LOG_TRACE(self, "get_with_query got: {}", resp.rows.front());
|
|
2236
|
-
transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
|
|
2237
|
-
return self->op_completed_with_callback(std::move(cb),
|
|
2238
|
-
std::optional<transaction_get_result>(doc));
|
|
2239
|
-
} catch (const std::exception& e) {
|
|
2240
|
-
// TODO(SA): unsure what to do here, but this is pretty fatal, so
|
|
2241
2276
|
return self->op_completed_with_error(
|
|
2242
|
-
std::move(cb),
|
|
2277
|
+
std::move(cb),
|
|
2278
|
+
transaction_operation_failed(FAIL_DOC_NOT_FOUND, "document not found"));
|
|
2243
2279
|
}
|
|
2280
|
+
CB_ATTEMPT_CTX_LOG_TRACE(self, "get_with_query got: {}", resp.rows.front());
|
|
2281
|
+
transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
|
|
2282
|
+
return self->op_completed_with_callback(std::move(cb),
|
|
2283
|
+
std::optional<transaction_get_result>(doc));
|
|
2284
|
+
} catch (const std::exception& e) {
|
|
2285
|
+
// Unexpected error here: fail with FAIL_OTHER so the transaction rolls back.
|
|
2286
|
+
return self->op_completed_with_error(std::move(cb),
|
|
2287
|
+
transaction_operation_failed(FAIL_OTHER, e.what()));
|
|
2244
2288
|
}
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2289
|
+
}
|
|
2290
|
+
// for get_optional. <sigh>
|
|
2291
|
+
if (optional) {
|
|
2292
|
+
try {
|
|
2293
|
+
std::rethrow_exception(err);
|
|
2294
|
+
} catch (const document_not_found&) {
|
|
2295
|
+
return self->op_completed_with_callback(std::move(cb),
|
|
2296
|
+
std::optional<transaction_get_result>());
|
|
2297
|
+
} catch (...) {
|
|
2298
|
+
return self->op_completed_with_error(std::move(cb), std::current_exception());
|
|
2255
2299
|
}
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2300
|
+
}
|
|
2301
|
+
return self->op_completed_with_error(std::move(cb), std::move(err));
|
|
2302
|
+
});
|
|
2259
2303
|
}
|
|
2260
2304
|
|
|
2261
2305
|
void
|
|
@@ -2263,46 +2307,55 @@ attempt_context_impl::insert_raw_with_query(const core::document_id& id,
|
|
|
2263
2307
|
codec::encoded_value content,
|
|
2264
2308
|
Callback&& cb)
|
|
2265
2309
|
{
|
|
2266
|
-
cache_error_async(
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
}
|
|
2310
|
+
// Called only from insert(), already within its cache_error_async(): the op has
|
|
2311
|
+
// been counted in the wait-group and existing_error() has been checked, so we must
|
|
2312
|
+
// not wrap again here -- double-counting would leave the op permanently in-flight
|
|
2313
|
+
// and deadlock the commit/rollback wait.
|
|
2314
|
+
auto self = shared_from_this();
|
|
2315
|
+
const couchbase::transactions::transaction_query_options opts;
|
|
2316
|
+
return self->wrap_query(
|
|
2317
|
+
KV_INSERT,
|
|
2318
|
+
opts,
|
|
2319
|
+
make_params(id, std::move(content)),
|
|
2320
|
+
make_kv_txdata(),
|
|
2321
|
+
STAGE_QUERY_KV_INSERT,
|
|
2322
|
+
true,
|
|
2323
|
+
{},
|
|
2324
|
+
[self, id, cb = std::move(cb)](const std::exception_ptr& err,
|
|
2325
|
+
core::operations::query_response resp) mutable {
|
|
2326
|
+
if (err) {
|
|
2327
|
+
try {
|
|
2328
|
+
std::rethrow_exception(err);
|
|
2329
|
+
} catch (const transaction_operation_failed& e) {
|
|
2330
|
+
return self->op_completed_with_error(std::move(cb), e);
|
|
2331
|
+
} catch (const op_exception& ex) {
|
|
2332
|
+
return self->op_completed_with_error(std::move(cb), ex);
|
|
2333
|
+
} catch (const std::exception& e) {
|
|
2334
|
+
return self->op_completed_with_error(std::move(cb),
|
|
2335
|
+
transaction_operation_failed(FAIL_OTHER, e.what()));
|
|
2336
|
+
} catch (...) {
|
|
2337
|
+
return self->op_completed_with_error(
|
|
2338
|
+
std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected error"));
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
// make a transaction_get_result from the row...
|
|
2342
|
+
try {
|
|
2343
|
+
if (resp.rows.empty()) {
|
|
2344
|
+
// A successful insert query must return the staged document row; no rows is an
|
|
2345
|
+
// unexpected protocol condition. front() on an empty vector is UB, so guard explicitly.
|
|
2346
|
+
return self->op_completed_with_error(
|
|
2347
|
+
std::move(cb),
|
|
2348
|
+
transaction_operation_failed(FAIL_OTHER, "insert query returned no rows"));
|
|
2349
|
+
}
|
|
2350
|
+
CB_ATTEMPT_CTX_LOG_TRACE(self, "insert_raw_with_query got: {}", resp.rows.front());
|
|
2351
|
+
transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
|
|
2352
|
+
return self->op_completed_with_callback(std::move(cb),
|
|
2353
|
+
std::optional<transaction_get_result>(doc));
|
|
2354
|
+
} catch (const std::exception& e) {
|
|
2355
|
+
// Unexpected error here: fail with FAIL_OTHER so the transaction rolls back.
|
|
2356
|
+
return self->op_completed_with_error(std::move(cb),
|
|
2357
|
+
transaction_operation_failed(FAIL_OTHER, e.what()));
|
|
2358
|
+
}
|
|
2306
2359
|
});
|
|
2307
2360
|
}
|
|
2308
2361
|
|
|
@@ -2311,91 +2364,103 @@ attempt_context_impl::replace_raw_with_query(const transaction_get_result& docum
|
|
|
2311
2364
|
codec::encoded_value content,
|
|
2312
2365
|
Callback&& cb)
|
|
2313
2366
|
{
|
|
2314
|
-
cache_error_async(
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
}
|
|
2367
|
+
// Called only from replace(), already within its cache_error_async() (op counted,
|
|
2368
|
+
// existing_error() checked); see insert_raw_with_query() for why we do not re-wrap.
|
|
2369
|
+
auto self = shared_from_this();
|
|
2370
|
+
const couchbase::transactions::transaction_query_options opts;
|
|
2371
|
+
return self->wrap_query(
|
|
2372
|
+
KV_REPLACE,
|
|
2373
|
+
opts,
|
|
2374
|
+
make_params(document.id(), std::move(content)),
|
|
2375
|
+
make_kv_txdata(document),
|
|
2376
|
+
STAGE_QUERY_KV_REPLACE,
|
|
2377
|
+
true,
|
|
2378
|
+
{},
|
|
2379
|
+
[self, id = document.id(), cb = std::move(cb)](const std::exception_ptr& err,
|
|
2380
|
+
core::operations::query_response resp) mutable {
|
|
2381
|
+
if (err) {
|
|
2382
|
+
try {
|
|
2383
|
+
std::rethrow_exception(err);
|
|
2384
|
+
} catch (const query_cas_mismatch& e) {
|
|
2385
|
+
return self->op_completed_with_error(
|
|
2386
|
+
std::move(cb), transaction_operation_failed(FAIL_CAS_MISMATCH, e.what()).retry());
|
|
2387
|
+
} catch (const document_not_found& e) {
|
|
2388
|
+
return self->op_completed_with_error(
|
|
2389
|
+
std::move(cb), transaction_operation_failed(FAIL_DOC_NOT_FOUND, e.what()).retry());
|
|
2390
|
+
} catch (const transaction_operation_failed& e) {
|
|
2391
|
+
return self->op_completed_with_error(std::move(cb), e);
|
|
2392
|
+
} catch (const op_exception& ex) {
|
|
2393
|
+
return self->op_completed_with_error(std::move(cb), ex);
|
|
2394
|
+
} catch (std::exception& e) {
|
|
2395
|
+
return self->op_completed_with_error(std::move(cb),
|
|
2396
|
+
transaction_operation_failed(FAIL_OTHER, e.what()));
|
|
2397
|
+
} catch (...) {
|
|
2398
|
+
return self->op_completed_with_error(
|
|
2399
|
+
std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected exception"));
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
// make a transaction_get_result from the row...
|
|
2403
|
+
try {
|
|
2404
|
+
if (resp.rows.empty()) {
|
|
2405
|
+
// A successful replace query must return the staged document row; no rows is an
|
|
2406
|
+
// unexpected protocol condition. front() on an empty vector is UB, so guard explicitly.
|
|
2407
|
+
return self->op_completed_with_error(
|
|
2408
|
+
std::move(cb),
|
|
2409
|
+
transaction_operation_failed(FAIL_OTHER, "replace query returned no rows"));
|
|
2410
|
+
}
|
|
2411
|
+
CB_ATTEMPT_CTX_LOG_TRACE(self, "replace_raw_with_query got: {}", resp.rows.front());
|
|
2412
|
+
transaction_get_result doc(id, core::utils::json::parse(resp.rows.front()));
|
|
2413
|
+
return self->op_completed_with_callback(std::move(cb),
|
|
2414
|
+
std::optional<transaction_get_result>(doc));
|
|
2415
|
+
} catch (const std::exception& e) {
|
|
2416
|
+
// Unexpected error here: fail with FAIL_OTHER so the transaction rolls back.
|
|
2417
|
+
return self->op_completed_with_error(std::move(cb),
|
|
2418
|
+
transaction_operation_failed(FAIL_OTHER, e.what()));
|
|
2419
|
+
}
|
|
2358
2420
|
});
|
|
2359
2421
|
}
|
|
2360
2422
|
|
|
2361
2423
|
void
|
|
2362
2424
|
attempt_context_impl::remove_with_query(const transaction_get_result& document, VoidCallback&& cb)
|
|
2363
2425
|
{
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2426
|
+
// Called only from remove(), already within its cache_error_async() (op counted,
|
|
2427
|
+
// existing_error() checked); see insert_raw_with_query() for why we do not re-wrap.
|
|
2428
|
+
auto self = shared_from_this();
|
|
2429
|
+
const couchbase::transactions::transaction_query_options opts;
|
|
2430
|
+
return self->wrap_query(
|
|
2431
|
+
KV_REMOVE,
|
|
2432
|
+
opts,
|
|
2433
|
+
make_params(document.id(), {}),
|
|
2434
|
+
make_kv_txdata(document),
|
|
2435
|
+
STAGE_QUERY_KV_REMOVE,
|
|
2436
|
+
true,
|
|
2437
|
+
{},
|
|
2438
|
+
[self, id = document.id(), cb = std::move(cb)](
|
|
2439
|
+
const std::exception_ptr& err, const core::operations::query_response& /* resp */) mutable {
|
|
2440
|
+
if (err) {
|
|
2441
|
+
try {
|
|
2442
|
+
std::rethrow_exception(err);
|
|
2443
|
+
} catch (const transaction_operation_failed& e) {
|
|
2444
|
+
return self->op_completed_with_error(std::move(cb), e);
|
|
2445
|
+
} catch (const document_not_found& e) {
|
|
2446
|
+
return self->op_completed_with_error(
|
|
2447
|
+
std::move(cb), transaction_operation_failed(FAIL_DOC_NOT_FOUND, e.what()).retry());
|
|
2448
|
+
} catch (const query_cas_mismatch& e) {
|
|
2449
|
+
return self->op_completed_with_error(
|
|
2450
|
+
std::move(cb), transaction_operation_failed(FAIL_CAS_MISMATCH, e.what()).retry());
|
|
2451
|
+
} catch (const op_exception& ex) {
|
|
2452
|
+
return self->op_completed_with_error(std::move(cb), ex);
|
|
2453
|
+
} catch (const std::exception& e) {
|
|
2454
|
+
return self->op_completed_with_error(std::move(cb),
|
|
2455
|
+
transaction_operation_failed(FAIL_OTHER, e.what()));
|
|
2456
|
+
} catch (...) {
|
|
2457
|
+
return self->op_completed_with_error(
|
|
2458
|
+
std::move(cb), transaction_operation_failed(FAIL_OTHER, "unexpected exception"));
|
|
2394
2459
|
}
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2460
|
+
}
|
|
2461
|
+
// make a transaction_get_result from the row...
|
|
2462
|
+
return self->op_completed_with_callback(std::move(cb));
|
|
2463
|
+
});
|
|
2399
2464
|
}
|
|
2400
2465
|
|
|
2401
2466
|
void
|
|
@@ -3798,6 +3863,17 @@ attempt_context_impl::create_staged_insert(const core::document_id& id,
|
|
|
3798
3863
|
UNKNOWN,
|
|
3799
3864
|
"before_staged_insert hook threw error");
|
|
3800
3865
|
}
|
|
3866
|
+
// ExtThreadSafety invariant (couchbase-transactions-specs/transactions-extensions.md,
|
|
3867
|
+
// "ExtThreadSafety"): "no KV operations are staged but not sent in the BEGIN WORK" and
|
|
3868
|
+
// "no KV operations are performed regularly after queryMode is entered". insert()
|
|
3869
|
+
// reads the mode only after this op is counted in the wait-group, and set_query_mode()
|
|
3870
|
+
// drains all in-flight KV ops before issuing BEGIN WORK -- so reaching this staging
|
|
3871
|
+
// point with query mode already entered means that ordering has regressed and the
|
|
3872
|
+
// mutation would be silently lost at commit. Fires loudly in debug/FIT builds;
|
|
3873
|
+
// compiled out in release.
|
|
3874
|
+
assert(!op_list_.query_mode_entered() &&
|
|
3875
|
+
"ExtThreadSafety: KV insert staged after query mode entered -- would be missing "
|
|
3876
|
+
"from BEGIN WORK and lost at commit");
|
|
3801
3877
|
CB_ATTEMPT_CTX_LOG_DEBUG(this, "about to insert staged doc {} with cas {}", id, cas);
|
|
3802
3878
|
core::operations::mutate_in_request req{ id };
|
|
3803
3879
|
const bool binary =
|
|
@@ -289,7 +289,11 @@ private:
|
|
|
289
289
|
err.cause(TRANSACTION_ALREADY_COMMITTED);
|
|
290
290
|
break;
|
|
291
291
|
default:
|
|
292
|
-
|
|
292
|
+
// Ops are only blocked once commit or rollback has begun (wait_and_block_ops), but the
|
|
293
|
+
// attempt state is published later -- and the empty-commit fast path never reaches
|
|
294
|
+
// COMMITTED at all. So a racing op can land here with the state still PENDING: report it
|
|
295
|
+
// as a concurrent operation rather than an opaque unknown cause.
|
|
296
|
+
err.cause(CONCURRENT_OPERATIONS_DETECTED_ON_SAME_DOCUMENT);
|
|
293
297
|
}
|
|
294
298
|
op_completed_with_error_no_cache(std::forward<Handler>(cb), std::make_exception_ptr(err));
|
|
295
299
|
} catch (const transaction_operation_failed& e) {
|
|
@@ -32,7 +32,10 @@
|
|
|
32
32
|
#include <couchbase/fmt/transaction_keyspace.hxx>
|
|
33
33
|
|
|
34
34
|
#include <functional>
|
|
35
|
+
#include <future>
|
|
36
|
+
#include <list>
|
|
35
37
|
#include <memory>
|
|
38
|
+
#include <thread>
|
|
36
39
|
#include <utility>
|
|
37
40
|
|
|
38
41
|
namespace couchbase::core::transactions
|
|
@@ -586,10 +589,17 @@ transactions_cleanup::add_collection(const couchbase::transactions::transaction_
|
|
|
586
589
|
auto it = std::find(collections_.begin(), collections_.end(), keyspace);
|
|
587
590
|
if (it == collections_.end()) {
|
|
588
591
|
collections_.emplace_back(keyspace);
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
592
|
+
// Spawn only while running. stop() takes the worker handles out and joins them, so a
|
|
593
|
+
// handle appended after that point is one nothing has agreed to join -- and a joinable
|
|
594
|
+
// std::thread reaching its destructor terminates. Registering the keyspace is still
|
|
595
|
+
// right: start() spawns a worker for everything in collections_, so cleanup resumes for
|
|
596
|
+
// it on the next start.
|
|
597
|
+
if (running_) {
|
|
598
|
+
// start cleaning right away
|
|
599
|
+
lost_attempt_cleanup_workers_.emplace_back([this, keyspace = collections_.back()]() {
|
|
600
|
+
this->clean_collection(keyspace);
|
|
601
|
+
});
|
|
602
|
+
}
|
|
593
603
|
}
|
|
594
604
|
lock.unlock();
|
|
595
605
|
CB_ATTEMPT_CLEANUP_LOG_DEBUG("added {} to lost transaction cleanup", keyspace);
|
|
@@ -599,8 +609,24 @@ transactions_cleanup::add_collection(const couchbase::transactions::transaction_
|
|
|
599
609
|
void
|
|
600
610
|
transactions_cleanup::start()
|
|
601
611
|
{
|
|
602
|
-
|
|
603
|
-
|
|
612
|
+
{
|
|
613
|
+
const std::unique_lock<std::mutex> lock(mutex_);
|
|
614
|
+
running_ = config_.cleanup_config.cleanup_client_attempts ||
|
|
615
|
+
config_.cleanup_config.cleanup_lost_attempts;
|
|
616
|
+
|
|
617
|
+
if (config_.cleanup_config.cleanup_lost_attempts) {
|
|
618
|
+
// Re-spawn a worker for every keyspace still registered from before a previous stop().
|
|
619
|
+
// add_collection() below cannot do this: it dedupes on collections_, so an
|
|
620
|
+
// already-registered keyspace is skipped and its lost-attempt cleanup stays dead for the
|
|
621
|
+
// rest of the process. notify_fork(prepare) calls stop() and notify_fork(parent) calls
|
|
622
|
+
// start() again, so every fork leaves exactly that state behind unless it's undone here.
|
|
623
|
+
for (const auto& keyspace : collections_) {
|
|
624
|
+
lost_attempt_cleanup_workers_.emplace_back([this, keyspace]() {
|
|
625
|
+
this->clean_collection(keyspace);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
604
630
|
if (config_.cleanup_config.cleanup_client_attempts) {
|
|
605
631
|
cleanup_thr_ = std::thread([this] {
|
|
606
632
|
attempts_loop();
|
|
@@ -628,7 +654,16 @@ transactions_cleanup::stop()
|
|
|
628
654
|
cleanup_thr_.join();
|
|
629
655
|
CB_ATTEMPT_CLEANUP_LOG_DEBUG("cleanup attempt thread closed");
|
|
630
656
|
}
|
|
631
|
-
|
|
657
|
+
// Take the handles out under mutex_, then join with the lock released: add_collection() and
|
|
658
|
+
// start() append to this list while holding mutex_, so walking it live would race with them;
|
|
659
|
+
// and the workers themselves take mutex_ (is_running(), interruptable_wait()), so joining
|
|
660
|
+
// while still holding it would deadlock.
|
|
661
|
+
std::list<std::thread> workers;
|
|
662
|
+
{
|
|
663
|
+
const std::unique_lock<std::mutex> lock(mutex_);
|
|
664
|
+
workers.swap(lost_attempt_cleanup_workers_);
|
|
665
|
+
}
|
|
666
|
+
for (auto& t : workers) {
|
|
632
667
|
CB_LOST_ATTEMPT_CLEANUP_LOG_DEBUG("shutting down all lost attempt threads...");
|
|
633
668
|
if (t.joinable()) {
|
|
634
669
|
t.join();
|