Distributed Transactions from the C++ SDK
A practical guide to using Couchbase’s distributed ACID transactions, via the C++ API.
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
The C++ Transactions API is built upon the Couchbase C SDK, libcouchbase (LCB), which is automatically installed by the transactions library. Applications built using C SDK and C Transactions can run in parallel without interfering with each other.
Below we show you how to create Transactions, step-by-step. You may also want to start with our transactions examples repository, which features useful downloadable examples of using Distributed Transactions.
API docs are available online.
Requirements
-
Couchbase Server 6.6.1 or above. Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
Getting Started
Couchbase transactions require no additional components or services to be configured. Simply add the transactions library into your project. The latest version, as of 19 April 2021, is 1.0.0.
Installing on Linux
We are currently distributing our linux libraries in tar files, which can be found:
-
RHEL7/Centos7 -
https://packages.couchbase.com/clients/transactions-cxx/couchbase-transactions-1.0.0-1.253.el7.x86_64.tar -
RHEL8/Centos8 -
https://packages.couchbase.com/clients/transactions-cxx/couchbase-transactions-1.0.0-1.253.el8.x86_64.tar -
Ubuntu 20.04 -
https://packages.couchbase.com/clients/transactions-cxx/couchbase-transactions-1.0.0~r253-ubuntu2004-focal.tar
The following steps show how to install transactions on RHEL/CentOS 8. Other linux platforms will be similar:
$ sudo yum groupinstall "Development Tools"
$ sudo yum install boost-devel
$ wget https://packages.couchbase.com/clients/transactions-cxx/couchbase-transactions-1.0.0-1.253.el8.x86_64.tar
$ tar xf couchbase-transactions-1.0.0-1.253.el8.x86_64.tar
$ sudo yum install couchbase-transactions*.rpm
Installing on Mac OS X
Mac libraries are available through homebrew. Once you have homebrew, add our tap, and install:
$ brew tap couchbaselabs/homebrew-couchbase-transactions-cxx
$ brew install couchbase-transactions-cxx
Initializing Transactions
The starting point is the transactions object.
It is very important that the application ensures that only one of these is created, as it performs automated background processes that should not be duplicated.
// Initialize the Couchbase cluster
couchbase::cluster cluster("couchbase://localhost", "transactor", "mypass");
auto bucket = cluster.bucket("transact");
auto collection = bucket->default_collection();
// Create the single Transactions object
couchbase::transactions::transactions transactions(cluster, {});
Configuration
Transactions can optionally be configured at the point of creating the transactions object:
couchbase::transactions::transaction_config configuration;
configuration.durability_level(couchbase::transactions::durability_level::PERSIST_TO_MAJORITY);
couchbase::transactions::transactions transactions(cluster, configuration);
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
try {
transactions.run([&](couchbase::transactions::attempt_context& ctx) {
// 'ctx' permits getting, inserting, removing and replacing documents,
// along with committing and rolling back the transaction
// ... Your transaction logic here ...
// This call is optional -- if you leave it off,
// the transaction will be committed anyway.
ctx.commit();
});
} catch (couchbase::transactions::transaction_failed& e) {
std::cerr << "Transaction did not reach commit point: " << e.what() << "\n";
}
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
try {
transactions.run([&](couchbase::transactions::attempt_context& ctx) {
// Inserting a doc:
ctx.insert(collection, "doc-a", nlohmann::json({}));
// Getting documents:
// Use ctx.get_optional() if the document may or may not exist
auto doc_opt = ctx.get_optional(collection, "doc-a");
if (doc_opt) {
couchbase::transactions::transaction_get_result& doc = doc_opt.value();
}
// Use ctx.get if the document should exist, and the transaction
// will fail if it does not
couchbase::transactions::transaction_get_result doc_a = ctx.get(collection, "doc-a");
// Replacing a doc:
couchbase::transactions::transaction_get_result doc_b = ctx.get(collection, "doc-b");
nlohmann::json content = doc_b.content<nlohmann::json>();
content["transactions"] = "are awesome";
ctx.replace(collection, doc_b, content);
// Removing a doc:
couchbase::transactions::transaction_get_result doc_c = ctx.get(collection, "doc-c");
ctx.remove(collection, doc_c);
ctx.commit();
});
} catch (couchbase::transactions::transaction_failed& e) {
std::cerr << "Transaction did not reach commit point: " << e.what() << "\n";
}
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
Key-Value Mutations
Replacing
Replacing a document requires a ctx.get() call first.
This is necessary to ensure that the document is not involved in another transaction.
(If it is, then the transaction will handle this, generally by rolling back what has been done so far, and retrying the lambda.)
transactions.run([&](couchbase::transactions::attempt_context& ctx) {
std::string id = "doc-a";
couchbase::transactions::transaction_get_result doc = ctx.get(collection, id);
nlohmann::json content = doc.content<nlohmann::json>();
content["transactions"] = "are awesome";
ctx.replace(collection, doc, content);
});
Key-Value Reads
There are two ways to get a document, get and getOptional:
transactions.run([&](couchbase::transactions::attempt_context& ctx) {
std::string id = "doc-a";
auto doc_opt = ctx.get_optional(collection, id);
if (doc_opt) {
couchbase::transactions::transaction_get_result& doc = doc_opt.value();
}
});
get will cause the transaction to fail with transaction_failed (after rolling back any changes, of course).
It is provided as a convenience method so the developer does not have to check the optional if the document must exist for the transaction to succeed.
Gets will 'read your own writes', e.g. this will succeed:
transactions.run([&](couchbase::transactions::attempt_context& ctx) {
std::string id = "doc_id";
nlohmann::json value{
{ "foo", "bar" },
};
ctx.insert(collection, id, value);
// document must be accessible
couchbase::transactions::transaction_get_result doc = ctx.get(collection, id);
});
Committing
Committing is automatic: if there is no explicit call to ctx.commit() at the end of the transaction logic callback, and no exception is thrown, it will be committed.
As soon as the transaction is committed, all its changes will be atomically visible to reads from other transactions. The changes will also be committed (or "unstaged") so they are visible to non-transactional actors, in an eventually consistent fashion.
Commit is final: after the transaction is committed, it cannot be rolled back, and no further operations are allowed on it.
An asynchronous cleanup process ensures that once the transaction reaches the commit point, it will be fully committed - even if the application crashes.
Threads
The cluster, bucket, collection, and transactions objects all are safe to use across multiple threads. When creating
the cluster, you can specify the maximum number of libcouchbase instances the cluster and bucket can use. Transactions
currently only using KV operations from the bucket. Specifying max_bucket_instances in the cluster_options when creating
the cluster is sufficient. This will be the maximum number of concurrent transaction operations which can be made:
couchbase::cluster c("couchbase://localhost", "transactor", "mypass", cluster_options().max_bucket_instances(10));
auto coll = c.bucket("transact")->default_collection();
The example below allows for up to 10 instances to be created, then has 10 threads get and replace the content in a document:
std::list<std::thread> threads;
std::atomic<int> counter {0};
for (int i=0; i<10; i++) {
threads.emplace_back([&]() {
transactions.run([&](couchbase::transactions::attempt_context& ctx) {
std::string id = "doc_a";
auto doc = ctx.get(coll, id);
auto doc_content = doc.value();
doc_content["counter"] = ++counter;
ctx.replace(coll, doc, doc_content);
});
});
}
for (auto& t: threads) {
if (t.joinable()) {
t.join();
}
}
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
A complete version of this example is available on our GitHub transactions examples page.
try {
transactions.run([&](couchbase::transactions::attempt_context& ctx) {
auto monster = ctx.get(collection, monster_id);
const Monster& monster_body = monster.content<Monster>();
int monster_hitpoints = monster_body.hitpoints;
int monster_new_hitpoints = monster_hitpoints - damage;
auto player = ctx.get(collection, player_id);
if (monster_new_hitpoints <= 0) {
// Monster is killed. The remove is just for demoing, and a more realistic examples would set a "dead" flag or similar.
ctx.remove(collection, monster);
const Player& player_body = player.content<Player>();
// the player earns experience for killing the monster
int experience_for_killing_monster = monster_body.experience_when_killed;
int player_experience = player_body.experience;
int player_new_experience = player_experience + experience_for_killing_monster;
int player_new_level = calculate_level_for_experience(player_new_experience);
Player player_new_body = player_body;
player_new_body.experience = player_new_experience;
player_new_body.level = player_new_level;
ctx.replace(collection, player, player_new_body);
} else {
Monster monster_new_body = monster_body;
monster_new_body.hitpoints = monster_new_hitpoints;
ctx.replace(collection, monster, monster_new_body);
}
});
} catch (couchbase::transactions::transaction_failed& e) {
// The operation failed. Both the monster and the player will be untouched
// Situations that can cause this would include either the monster
// or player not existing (as get is used), or a persistent
// failure to be able to commit the transaction, for example on
// prolonged node failure.
}
Concurrency with Non-Transactional Writes
This release of transactions for Couchbase requires a degree of co-operation from the application. Specifically, the application should ensure that non-transactional writes (such as key-value writes or N1QL UPDATES) are never done concurrently with transactional writes, on the same document.
This requirement is to ensure that the strong Key-Value performance of Couchbase was not compromised. A key philosophy of our transactions is that you 'pay only for what you use'.
If two such writes do conflict then the transactional write will 'win', overwriting the non-transactional write.
Note this only applies to writes. Any non-transactional reads concurrent with transactions are fine, and are at a Read Committed level.
Rollback
If an exception is thrown, either by the application from the lambda, or by the transactions library, then that attempt is rolled back. The transaction logic may or may not be retried, depending on the exception.
If the transaction is not retried then it will throw a transaction_failed exception, and its cause method can be used for more details on the failure.
The application can use this to signal why it triggered a rollback.
The transaction can also be explicitly rolled back:
transactions.run([&](couchbase::transactions::attempt_context& ctx) {
couchbase::transactions::transaction_get_result customer = ctx.get(collection, "customer-name");
auto content = customer.content<nlohmann::json>();
int balance = content["balance"].get<int>();
if (balance < cost_of_item) {
ctx.rollback();
}
// else continue transaction
});
In this case, if ctx.rollback() is reached, then the transaction will be regarded as successfully rolled back and no TransactionFailed will be thrown.
After a transaction is rolled back, it cannot be committed, no further operations are allowed on it, and the library will not try to automatically commit it at the end of the code block.
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
There are three exceptions that Couchbase transactions can return to the application: transaction_failed, transaction_expired and transaction_commit_ambiguous.
All exceptions derive from transaction_exception for backwards-compatibility purposes.
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
couchbase::transactions::transaction_config configuration;
configuration.expiration_time(std::chrono::seconds(120));
couchbase::transactions::transactions transactions(cluster, configuration);
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
Unresolved include directive in modules/ROOT/pages/distributed-acid-transactions-from-the-sdk.adoc - include::7.0@sdk:shared:partial$acid-transactions.adoc[]
Logging
To aid troubleshooting, the transactions library logs information to stdout. The default logging level is INFO, but can
be changed to produce more or less detailed output. For instance, to see very detailed logging:
// Set logging level to Trace
couchbase::transactions::set_transactions_logging_level(log_level::TRACE);
Further Reading
-
There’s plenty of explanation about how Transactions work in Couchbase in our Transactions documentation.
-
You can find further code examples on our transactions examples repository.