Skip to content

Commit

Permalink
Add example for transaction with binary objects
Browse files Browse the repository at this point in the history
  • Loading branch information
avsej committed Jun 26, 2024
1 parent f9d4067 commit 54cce4b
Show file tree
Hide file tree
Showing 2 changed files with 338 additions and 0 deletions.
4 changes: 4 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ target_link_libraries(transactions_transfer_basic
add_executable(transactions_transfer_read_replica transactions_transfer_read_replica.cpp)
target_link_libraries(transactions_transfer_read_replica
PRIVATE couchbase_cxx_client::couchbase_cxx_client taocpp::json)

add_executable(transactions_transfer_with_binary_objects transactions_transfer_with_binary_objects.cpp)
target_link_libraries(transactions_transfer_with_binary_objects
PRIVATE couchbase_cxx_client::couchbase_cxx_client taocpp::json)
334 changes: 334 additions & 0 deletions examples/transactions_transfer_with_binary_objects.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
#include <couchbase/cluster.hxx>
#include <couchbase/codec/codec_flags.hxx>
#include <couchbase/codec/transcoder_traits.hxx>
#include <couchbase/durability_level.hxx>
#include <couchbase/logger.hxx>
#include <couchbase/transactions/attempt_context.hxx>

#include <cstddef>
#include <iterator>
#include <tao/json.hpp>
#include <tao/json/to_string.hpp>

#include <algorithm>
#include <cstdlib>
#include <iostream>

#include <arpa/inet.h>

struct program_config {
std::string connection_string{ "couchbase://127.0.0.1" };
std::string user_name{ "Administrator" };
std::string password{ "password" };
std::string bucket_name{ "default" };
std::string scope_name{ couchbase::scope::default_name };
std::string collection_name{ couchbase::collection::default_name };
std::optional<std::string> profile{};
bool verbose{ false };

static auto from_env() -> program_config;
static auto quote(std::string val) -> std::string;
void dump();
};

enum bank_error : int {
insufficient_funds = 1,
};

namespace
{
struct bank_error_category : std::error_category {
[[nodiscard]] auto name() const noexcept -> const char* override
{
return "bank_error";
}

[[nodiscard]] auto message(int ev) const noexcept -> std::string override
{
switch (static_cast<bank_error>(ev)) {
case insufficient_funds:
return "insufficient_funds (1): not enough funds on the account";
break;
}
return "unexpected error code in \"bank_error\" category, ev=" + std::to_string(ev);
}
};

const static bank_error_category instance{};
const std::error_category&
bank_error_category_instance() noexcept
{
return instance;
}
} // namespace

template<>
struct std::is_error_code_enum<bank_error> : std::true_type {
};

auto
make_error_code(bank_error e) -> std::error_code
{
return { static_cast<int>(e), bank_error_category_instance() };
}

struct bank_account {
std::string name;
std::int32_t balance{ 0 };
};

std::ostream&
operator<<(std::ostream& os, const bank_account& a)
{
os << "bank_account(name: \"" << a.name << "\", balance: " << a.balance << " USD)";
return os;
}

class bank_account_transcoder
{
public:
using document_type = bank_account;

static auto encode(document_type document) -> couchbase::codec::encoded_value
{
std::vector<std::byte> buffer;
buffer.reserve(256);

constexpr std::size_t max_name_length{ 250 };
std::size_t name_length = std::min(document.name.size(), max_name_length);
buffer.push_back(static_cast<std::byte>(name_length));
std::transform(document.name.begin(),
document.name.begin() + name_length,
std::back_inserter(buffer),
[](char c) {
return static_cast<std::byte>(c);
});

auto balance_nbo = htonl(document.balance);
auto balance_ptr = reinterpret_cast<const std::byte*>(&balance_nbo);
std::copy(balance_ptr, balance_ptr + sizeof(balance_nbo), std::back_inserter(buffer));

return { std::move(buffer), couchbase::codec::codec_flags::binary_common_flags };
}

static auto decode(const couchbase::codec::encoded_value& encoded) -> document_type
{
if (encoded.flags != 0 &&
!couchbase::codec::codec_flags::has_common_flags(
encoded.flags, couchbase::codec::codec_flags::binary_common_flags)) {
throw std::system_error(
couchbase::errc::common::decoding_failure,
"bank_account_transcoder expects document to have Binary common flags, flags=" +
std::to_string(encoded.flags));
}

bank_account result;
std::size_t name_length = static_cast<std::size_t>(encoded.data[0]);
result.name.reserve(name_length);
std::transform(encoded.data.begin() + 1,
encoded.data.begin() + 1 + name_length,
std::back_inserter(result.name),
[](std::byte c) {
return static_cast<char>(c);
});
std::int32_t balance_nbo{ 0 };
std::copy(encoded.data.begin() + 1 + name_length,
encoded.data.begin() + 1 + name_length + sizeof(balance_nbo),
reinterpret_cast<std::byte*>(&balance_nbo));
result.balance = ntohl(balance_nbo);

return result;
}
};

template<>
struct couchbase::codec::is_transcoder<bank_account_transcoder> : public std::true_type {
};

int
main()
{
auto config = program_config::from_env();
config.dump();

if (config.verbose) {
couchbase::logger::initialize_console_logger();
couchbase::logger::set_level(couchbase::logger::log_level::trace);
}

auto options = couchbase::cluster_options(config.user_name, config.password);
if (config.profile) {
options.apply_profile(config.profile.value());
}

auto [connect_err, cluster] =
couchbase::cluster::connect(config.connection_string, options).get();
if (connect_err) {
std::cout << "Unable to connect to the cluster. ec: " << connect_err.message() << "\n";
return EXIT_FAILURE;
}

auto collection =
cluster.bucket(config.bucket_name).scope(config.scope_name).collection(config.collection_name);

auto upsert_options =
couchbase::upsert_options{}.durability(couchbase::durability_level::majority);
{
bank_account alice{ "Alice", 124'000 };
std::cout << "Initialize account for Alice: " << alice << "\n";
auto [err, resp] =
collection.upsert<bank_account_transcoder>("alice", alice, upsert_options).get();
if (err.ec()) {
std::cout << "Unable to create an account for Alice: " << err.message() << "\n";
return EXIT_FAILURE;
}
std::cout << "Stored account for Alice (CAS=" << resp.cas().value() << ")\n";
}
{
bank_account bob{ "Bob", 42'000 };
std::cout << "Initialize account for Bob: " << bob << "\n";
auto [err, resp] = collection.upsert<bank_account_transcoder>("bob", bob, upsert_options).get();
if (err.ec()) {
std::cout << "Unable to create an account for Bob: " << err.message() << "\n";
return EXIT_FAILURE;
}
std::cout << "Stored account for Bob (CAS=" << resp.cas().value() << ")\n";
}

{
auto [err, res] = cluster.transactions()->run(
[collection](
std::shared_ptr<couchbase::transactions::attempt_context> ctx) -> couchbase::error {
auto [e1, alice] = ctx->get(collection, "alice");
if (e1.ec()) {
std::cout << "Unable to read account for Alice: " << e1.ec().message() << "\n";
return e1;
}
auto alice_content = alice.content_as<bank_account_transcoder>();

auto [e2, bob] = ctx->get(collection, "bob");
if (e2.ec()) {
std::cout << "Unable to read account for Bob: " << e2.ec().message() << "\n";
return e2;
}
auto bob_content = bob.content_as<bank_account_transcoder>();

const std::int64_t money_to_transfer = 1'234;
if (alice_content.balance < money_to_transfer) {
std::cout << "Alice does not have enough money to transfer " << money_to_transfer
<< " USD to Bob\n";
return {
bank_error::insufficient_funds,
"not enough funds on Alice's account",
};
}
alice_content.balance -= money_to_transfer;
bob_content.balance += money_to_transfer;

{
auto [e3, a] = ctx->replace<bank_account_transcoder>(alice, alice_content);
if (e3.ec()) {
std::cout << "Unable to read account for Alice: " << e3.ec().message() << "\n";
}
}
{
auto [e4, b] = ctx->replace<bank_account_transcoder>(bob, bob_content);
if (e4.ec()) {
std::cout << "Unable to update account for Bob: " << e4.ec().message() << "\n";
}
}
return {};
});

if (err.ec()) {
std::cout << "Transaction has failed: " << err.ec().message() << "\n";
if (auto cause = err.cause(); cause.has_value()) {
std::cout << "Cause: " << cause->ec().message() << "\n";
}
return EXIT_FAILURE;
}
}

{
auto [err, resp] = collection.get("alice", {}).get();
if (err.ec()) {
std::cout << "Unable to update account for Alice: " << err.message() << "\n";
return EXIT_FAILURE;
}
std::cout << "Alice (CAS=" << resp.cas().value()
<< "): " << resp.content_as<bank_account_transcoder>() << "\n";
}
{
auto [err, resp] = collection.get("bob", {}).get();
if (err.ec()) {
std::cout << "Unable to read account for Bob: " << err.message() << "\n";
return EXIT_FAILURE;
}
std::cout << "Bob (CAS=" << resp.cas().value()
<< "): " << resp.content_as<bank_account_transcoder>() << "\n";
}

cluster.close().get();

return 0;
}

auto
program_config::from_env() -> program_config
{
program_config config{};

if (const auto* val = getenv("CONNECTION_STRING"); val != nullptr) {
config.connection_string = val;
}
if (const auto* val = getenv("USER_NAME"); val != nullptr) {
config.user_name = val;
}
if (const auto* val = getenv("PASSWORD"); val != nullptr) {
config.password = val;
}
if (const auto* val = getenv("BUCKET_NAME"); val != nullptr) {
config.bucket_name = val;
}
if (const auto* val = getenv("SCOPE_NAME"); val != nullptr) {
config.scope_name = val;
}
if (const auto* val = getenv("COLLECTION_NAME"); val != nullptr) {
config.collection_name = val;
}
if (const auto* val = getenv("PROFILE"); val != nullptr) {
config.profile = val; // e.g. "wan_development"
}
if (const auto* val = getenv("VERBOSE"); val != nullptr) {
const std::array<std::string, 5> truthy_values = {
"yes", "y", "on", "true", "1",
};
for (const auto& truth : truthy_values) {
if (val == truth) {
config.verbose = true;
break;
}
}
}

return config;
}

auto
program_config::quote(std::string val) -> std::string
{
return "\"" + val + "\"";
}

void
program_config::dump()
{
std::cout << " CONNECTION_STRING: " << quote(connection_string) << "\n";
std::cout << " USER_NAME: " << quote(user_name) << "\n";
std::cout << " PASSWORD: [HIDDEN]\n";
std::cout << " BUCKET_NAME: " << quote(bucket_name) << "\n";
std::cout << " SCOPE_NAME: " << quote(scope_name) << "\n";
std::cout << " COLLECTION_NAME: " << quote(collection_name) << "\n";
std::cout << " VERBOSE: " << std::boolalpha << verbose << "\n";
std::cout << " PROFILE: " << (profile ? quote(*profile) : "[NONE]") << "\n\n";
}

0 comments on commit 54cce4b

Please sign in to comment.