Skip to content

Commit 8a2d260

Browse files
committed
JSCBC-1522: Close and destroy transactions objects off the JS thread
Changes ------- * Run the core's transactions close() from Transactions::jsClose on a detached thread and call back through a CallCookie, so removing the client record from each bucket no longer blocks the event loop; close inline, as before, when no thread can be started * Instance now owns every core transactions object created on its connection; the Transactions and Transaction wrappers hold aliasing pointers that keep the instance alive instead, so collecting a Transactions wrapper never frees an object a live transaction still uses, and no finalizer runs the core's destructor on the JS thread * Close each object through Instance::closeTransactions, which holds a lock for the whole close, so an explicit close and the connection's shutdown are serialized and close it once (two concurrent closes end the process) * Instance::shutdown closes every transactions object still open before the cluster, and destroys them while the cluster is open, or once the IO thread has stopped when an operation is still in flight and so still uses its object * Count a transaction operation from its start until its callback is invoked or its handler is destroyed, through a TransactionOp shared by every copy of its RefCallCookie * Document on Transactions.run that an operation in flight at a worker terminate or process exit is cancelled, and lost transaction cleanup decides the outcome * Add a vars option to expectProcessExits for passing extra environment variables to an exit fixture * Add tests that the close callback runs after the call returns, that two concurrent closes both resolve, and that terminating a worker while its cluster is closing still exits * Add a test that commits after the Transactions wrapper that started the transaction has been collected * Add a test that terminates workers with transactions still open, and after collecting their connection, and checks that each client is removed from the client record * Add test/suites/transactions/teardownstall.test.ts, which stalls a worker's IO thread, with a commit queued behind the stall and with its transactions closed, and checks each terminate returns at its bound Change-Id: I2c7d8a4deb1098d805f1b7e902177ad70b747add Reviewed-on: https://review.couchbase.org/c/couchnode/+/256439 Reviewed-by: Mateusz <matt.wozakowski@couchbase.com> Tested-by: Jared Casey <jared.casey@couchbase.com>
1 parent 7f7480a commit 8a2d260

24 files changed

Lines changed: 1371 additions & 32 deletions

‎lib/transactions.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,12 @@ export class Transactions {
12711271
/**
12721272
* Executes a transaction.
12731273
*
1274+
* When the worker thread running a transaction is terminated, or the process
1275+
* exits, while one of its operations is in flight, that operation is
1276+
* cancelled. The outcome is then decided by lost transaction cleanup rather
1277+
* than by the caller, so a transaction past its commit point may still commit
1278+
* after `terminate()` has returned.
1279+
*
12741280
* @param logicFn The transaction lambda to execute.
12751281
* @param config Configuration operations for the transaction.
12761282
*/

‎src/connection.hpp‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,11 @@ class Connection : public Napi::ObjectWrap<Connection>
133133
return _instance->_cluster;
134134
}
135135

136+
const std::shared_ptr<Instance> &instance() const
137+
{
138+
return _instance;
139+
}
140+
136141
std::pair<std::optional<std::string>, std::optional<std::string>>
137142
getClusterLabels();
138143

‎src/instance.cpp‎

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,34 @@
11
#include "instance.hpp"
2+
#include <algorithm>
3+
#include <core/transactions.hxx>
24
#include <future>
35
#include <system_error>
46

57
namespace couchnode
68
{
79

10+
TransactionOp::TransactionOp(std::shared_ptr<Count> count)
11+
: _count(std::move(count))
12+
{
13+
++*_count;
14+
}
15+
16+
TransactionOp::~TransactionOp()
17+
{
18+
complete();
19+
}
20+
21+
void TransactionOp::complete()
22+
{
23+
if (_completed.exchange(true)) {
24+
return;
25+
}
26+
--*_count;
27+
}
28+
829
Instance::Instance()
930
: _cluster(couchbase::core::cluster(_io))
31+
, _transactionOps(std::make_shared<TransactionOp::Count>(0))
1032
{
1133
_ioThread = std::thread([this]() {
1234
try {
@@ -25,6 +47,58 @@ Instance::~Instance()
2547
{
2648
}
2749

50+
std::shared_ptr<cbcoretxns::transactions> Instance::addTransactions(
51+
std::shared_ptr<cbcoretxns::transactions> transactions)
52+
{
53+
std::lock_guard<std::mutex> lock(_transactionsMutex);
54+
auto *raw = transactions.get();
55+
_transactions.push_back({std::move(transactions)});
56+
return std::shared_ptr<cbcoretxns::transactions>(shared_from_this(), raw);
57+
}
58+
59+
void Instance::closeTransactions(const cbcoretxns::transactions *transactions)
60+
{
61+
std::lock_guard<std::mutex> lock(_transactionsMutex);
62+
auto owned = std::find_if(
63+
_transactions.begin(), _transactions.end(),
64+
[transactions](const auto &o) { return o.impl.get() == transactions; });
65+
if (owned == _transactions.end() || owned->closed) {
66+
return;
67+
}
68+
owned->impl->close();
69+
owned->closed = true;
70+
}
71+
72+
std::shared_ptr<TransactionOp> Instance::beginTransactionOp()
73+
{
74+
return std::make_shared<TransactionOp>(_transactionOps);
75+
}
76+
77+
bool Instance::transactionOpsInFlight() const
78+
{
79+
return *_transactionOps != 0;
80+
}
81+
82+
void Instance::closeAllTransactions()
83+
{
84+
std::lock_guard<std::mutex> lock(_transactionsMutex);
85+
for (auto &owned : _transactions) {
86+
if (!owned.closed) {
87+
owned.impl->close();
88+
owned.closed = true;
89+
}
90+
}
91+
}
92+
93+
void Instance::destroyTransactions()
94+
{
95+
std::lock_guard<std::mutex> lock(_transactionsMutex);
96+
// The core's destructor closes each object again. That repeats the client
97+
// record removal: a round trip while the cluster is open, and retries
98+
// against `cluster_closed` for up to 500 ms per keyspace once it is not.
99+
_transactions.clear();
100+
}
101+
28102
void Instance::shutdown(std::function<void()> done)
29103
{
30104
std::call_once(_shutdownOnce, [this, &done]() {
@@ -34,11 +108,20 @@ void Instance::shutdown(std::function<void()> done)
34108
// reference keeps the last release off the IO thread.
35109
try {
36110
std::thread([self = shared_from_this(), done]() mutable {
111+
self->closeAllTransactions();
112+
// An operation still in flight uses its transactions object, so
113+
// that is destroyed once the IO thread has stopped instead. The
114+
// cluster close cancels the operation, and a transaction past its
115+
// commit point is then settled by lost transaction cleanup.
116+
if (!self->transactionOpsInFlight()) {
117+
self->destroyTransactions();
118+
}
37119
auto closed = std::make_shared<std::promise<void>>();
38120
auto closing = closed->get_future();
39121
self->_cluster.close([closed]() { closed->set_value(); });
40122
closing.wait();
41123
self->_ioThread.join();
124+
self->destroyTransactions();
42125
// Released before `done`, so a caller that waits on `done` and
43126
// holds its own reference destroys the instance itself.
44127
self.reset();

‎src/instance.hpp‎

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,47 @@
11
#pragma once
22
#include <asio/io_context.hpp>
3+
#include <atomic>
34
#include <core/cluster.hxx>
45
#include <core/logger/logger.hxx>
6+
#include <cstddef>
57
#include <functional>
68
#include <memory>
79
#include <mutex>
810
#include <thread>
11+
#include <vector>
12+
13+
namespace couchbase::core::transactions
14+
{
15+
class transactions;
16+
} // namespace couchbase::core::transactions
17+
namespace cbcoretxns = couchbase::core::transactions;
918

1019
namespace couchnode
1120
{
1221

22+
// One transaction operation in flight. It counts from when the operation
23+
// starts until it completes or its handler is destroyed without completing it,
24+
// whichever comes first, so a handler the core keeps after calling it does not
25+
// count as in flight for as long as the core keeps it.
26+
class TransactionOp
27+
{
28+
public:
29+
using Count = std::atomic<std::size_t>;
30+
31+
explicit TransactionOp(std::shared_ptr<Count> count);
32+
~TransactionOp();
33+
34+
TransactionOp(const TransactionOp &) = delete;
35+
TransactionOp &operator=(const TransactionOp &) = delete;
36+
37+
// Stops counting the operation. Only the first call does anything.
38+
void complete();
39+
40+
private:
41+
std::shared_ptr<Count> _count;
42+
std::atomic<bool> _completed{false};
43+
};
44+
1345
// Owned through `std::shared_ptr`. The last reference must never be dropped on
1446
// the IO thread, since destroying the instance destroys the IO context that
1547
// thread is running.
@@ -19,19 +51,54 @@ class Instance : public std::enable_shared_from_this<Instance>
1951
Instance();
2052
~Instance();
2153

22-
// Closes the cluster and joins the IO thread, on a helper thread that holds
23-
// a reference until the join returns. Only the first call does anything;
24-
// `done` runs on the helper thread after the join and may be empty. When no
25-
// helper thread can be started, the close is abandoned, the instance is
26-
// leaked, and `done` runs before this returns.
54+
// Takes ownership of a transactions object and returns a pointer to it
55+
// that keeps this instance alive instead. The instance destroys the object
56+
// in `shutdown`, on its helper thread, so a wrapper's finalizer never runs
57+
// the core's destructor on the JS thread. That destructor closes the
58+
// object again, which waits on the cluster.
59+
std::shared_ptr<cbcoretxns::transactions>
60+
addTransactions(std::shared_ptr<cbcoretxns::transactions> transactions);
61+
62+
// Closes one transactions object, unless it was already closed. Waits on
63+
// cluster requests, so it belongs on a thread of its own. Holds a lock for
64+
// the whole close, since the core's close cannot run concurrently and
65+
// `shutdown` must not close the cluster under it.
66+
void closeTransactions(const cbcoretxns::transactions *transactions);
67+
68+
// Counts a transaction operation as in flight until the returned
69+
// operation completes or is destroyed.
70+
std::shared_ptr<TransactionOp> beginTransactionOp();
71+
72+
// On a helper thread that holds a reference until it is done: closes the
73+
// transactions objects, then the cluster, and joins the IO thread. Only
74+
// the first call does anything; `done` runs on the helper thread after the
75+
// join and may be empty. When no helper thread can be started, the close
76+
// is abandoned, the instance is leaked, and `done` runs before this
77+
// returns.
78+
//
79+
// It runs once nothing on the JS side can reach this instance's objects:
80+
// at environment teardown, or when the connection is collected, which an
81+
// operation's pending callback prevents.
2782
void shutdown(std::function<void()> done);
2883

2984
asio::io_context _io;
3085
std::thread _ioThread;
3186
couchbase::core::cluster _cluster;
3287

3388
private:
89+
struct OwnedTransactions {
90+
std::shared_ptr<cbcoretxns::transactions> impl;
91+
bool closed{false};
92+
};
93+
94+
bool transactionOpsInFlight() const;
95+
void closeAllTransactions();
96+
void destroyTransactions();
97+
3498
std::once_flag _shutdownOnce;
99+
std::mutex _transactionsMutex;
100+
std::vector<OwnedTransactions> _transactions;
101+
std::shared_ptr<TransactionOp::Count> _transactionOps;
35102
};
36103

37104
} // namespace couchnode

0 commit comments

Comments
 (0)