--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2023 IBM
+ *
+ * See file COPYING for license information.
+ *
+ */
+
+#pragma once
+
+/// \file neorados/cls/version.h
+///
+/// \brief NeoRADOS interface to object versioning class
+///
+/// The `version` object class stores a version in the extended
+/// attributes of an object to help coordinate multiple writers. This
+/// version comprises a byte string and an integer. The integers are
+/// comparable and can be compared with the operators in
+/// `VersionCond`. The byte strings are incomparable. Two versions
+/// with different strings will always conflict.
+
+#include <coroutine>
+#include <string>
+#include <utility>
+
+#include <boost/asio/async_result.hpp>
+#include <boost/system/error_code.hpp>
+
+#include "include/neorados/RADOS.hpp"
+
+#include "cls/version/cls_version_ops.h"
+#include "cls/version/cls_version_types.h"
+
+#include "neorados/cls/common.h"
+
+namespace neorados::cls::version {
+/// \brief Set the object version
+///
+/// Append a call to a write operation to set the object's version.
+///
+/// \param ver Version to set
+///
+/// \return The ClsWriteOp to be passed to WriteOp::exec
+[[nodiscard]] inline auto set(const obj_version& ver)
+{
+ buffer::list in;
+ cls_version_set_op call;
+ call.objv = ver;
+ encode(call, in);
+ return ClsWriteOp{[in = std::move(in)](WriteOp& op) {
+ op.exec("version", "set", in);
+ }};
+}
+
+/// \brief Unconditional increment version
+///
+/// Append a call to a write operation to increment the integral
+/// portion of a version.
+///
+/// \return The ClsWriteOp to be passed to WriteOp::exec
+[[nodiscard]] inline auto inc()
+{
+ buffer::list in;
+ cls_version_inc_op call;
+ encode(call, in);
+ return ClsWriteOp{[in = std::move(in)](WriteOp& op) {
+ op.exec("version", "inc", in);
+ }};
+}
+
+/// \brief Conditionally increment version
+///
+/// Append a call to a write operation to increment the object's
+/// version if condition is met. If the condition is not met, the
+/// operation fails with `std::errc::resource_unavailable_try_again`.
+///
+/// \param ver Version to compare stored object version against
+/// \param cond Comparison operator
+///
+/// \return The ClsWriteOp to be passed to WriteOp::exec
+[[nodiscard]] inline auto inc(const obj_version& objv, const VersionCond cond)
+{
+ buffer::list in;
+ cls_version_inc_op call;
+ call.objv = objv;
+
+ obj_version_cond c;
+ c.cond = cond;
+ c.ver = objv;
+
+ call.conds.push_back(c);
+
+ encode(call, in);
+ return ClsWriteOp{[in = std::move(in)](WriteOp& op) {
+ op.exec("version", "inc_conds", in);
+ }};
+}
+
+/// \brief Assert condition on stored version
+///
+/// Append a call to an operation that verifies the stored version has
+/// the specified relationship to the supplied version. If the
+/// condition is not met, the operation fails with
+/// `std::errc::operation_canceled`.
+///
+/// \param ver Version to compare stored object version against
+/// \param cond Comparison operator
+///
+/// \return The ClsOp to be passed to {Read,Write}Op::exec
+[[nodiscard]] inline auto check(const obj_version& ver, const VersionCond cond)
+{
+ buffer::list in;
+ cls_version_check_op call;
+ call.objv = ver;
+
+ obj_version_cond c;
+ c.cond = cond;
+ c.ver = ver;
+
+ call.conds.push_back(c);
+
+ encode(call, in);
+ return ClsOp{[in = std::move(in)](Op& op) {
+ op.exec("version", "check_conds", in);
+ }};
+}
+
+/// \brief Read the stored object version
+///
+/// Append a call to a read operation that reads the stored version.
+///
+/// \param objv Location to store the version
+///
+/// \return The ClsReadOp to be passed to ReadOp::exec
+[[nodiscard]] inline auto read(obj_version* const objv)
+{
+ using boost::system::error_code;
+ return ClsReadOp{[objv](Op& op) {
+ namespace sys = boost::system;
+ op.exec("version", "read", {},
+ [objv](error_code ec,
+ const buffer::list& bl) {
+ cls_version_read_ret ret;
+ if (!ec) {
+ auto iter = bl.cbegin();
+ try {
+ decode(ret, iter);
+ } catch (const sys::system_error& e) {
+ // This works by accident in the paleorados version,
+ // since they just don't report decode errors.
+ if (e.code() == buffer::errc::end_of_buffer) {
+ if (objv) {
+ objv->clear();
+ }
+ } else {
+ throw;
+ }
+ }
+ if (objv)
+ *objv = std::move(ret.objv);
+ }
+ });
+ }};
+}
+
+/// \brief Read the stored object version
+///
+/// Execute an asynchronous operation that reads the stored version.
+///
+/// \param r RADOS handle
+/// \param o Object to query
+/// \param ioc IOContext determining the object location
+/// \param token Boost.Asio CompletionToken
+///
+/// \return The object version in a way appropriate to the completion
+/// token. See Boost.Asio documentation.
+template<boost::asio::completion_token_for<
+ void(boost::system::error_code, obj_version)> CompletionToken>
+inline auto read(RADOS& r, Object o, IOContext ioc,
+ CompletionToken&& token)
+{
+ using namespace std::literals;
+ return exec<cls_version_read_ret>(
+ r, std::move(o), std::move(ioc),
+ "version"s, "read"s, nullptr,
+ [](cls_version_read_ret&& ret) {
+ return std::move(ret.objv);
+ }, std::forward<CompletionToken>(token));
+}
+} // namespace neorados::cls::version
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2023 IBM
+ *
+ * See file COPYING for license information.
+ *
+ */
+
+#include "neorados/cls/version.h"
+
+#include <boost/asio/post.hpp>
+#include <coroutine>
+#include <memory>
+#include <string_view>
+#include <utility>
+
+#include <boost/asio/use_awaitable.hpp>
+
+#include <boost/system/errc.hpp>
+#include <boost/system/error_code.hpp>
+
+#include "include/neorados/RADOS.hpp"
+
+#include "cls/version/cls_version_types.h"
+
+#include "test/neorados/common_tests.h"
+
+#include "gtest/gtest.h"
+
+namespace asio = boost::asio;
+namespace version = neorados::cls::version;
+using neorados::ReadOp;
+using neorados::WriteOp;
+
+using boost::system::error_code;
+using boost::system::errc::operation_canceled;
+
+CORO_TEST_F(neocls_version, test_version_inc_read, NeoRadosTest)
+{
+ std::string_view oid = "obj";
+ co_await create_obj(oid);
+
+ auto ver = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_EQ(0u, ver.ver);
+ EXPECT_EQ(0u, ver.tag.size());
+
+ // Increment version
+ co_await execute(oid, WriteOp{}.exec(version::inc()));
+
+ ver = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_GT(ver.ver, 0u);
+ EXPECT_NE(0u, ver.tag.size());
+
+ co_await execute(oid, WriteOp{}.exec(version::inc()));
+
+ auto ver2 = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+
+ EXPECT_GT(ver2.ver, ver.ver);
+ EXPECT_EQ(0u, ver2.tag.compare(ver.tag));
+
+ obj_version ver3;
+ co_await execute(oid, ReadOp{}.exec(version::read(&ver3)));
+ EXPECT_EQ(ver2.ver, ver3.ver);
+ EXPECT_EQ(1u, ver2.compare(&ver3));
+ co_return;
+}
+
+CORO_TEST_F(neocls_version, test_version_set, NeoRadosTest)
+{
+ std::string_view oid = "obj";
+ co_await create_obj(oid);
+
+ auto ver = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_EQ(0u, ver.ver);
+ EXPECT_EQ(0u, ver.tag.size());
+
+ ver.ver = 123;
+ ver.tag = "foo";
+
+ // Set version
+ co_await execute(oid, WriteOp{}.exec(version::set(ver)));
+
+ auto ver2 = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+
+ EXPECT_EQ(ver2.ver, ver.ver);
+ EXPECT_EQ(0, ver2.tag.compare(ver.tag));
+ co_return;
+}
+
+CORO_TEST_F(neocls_version, test_version_inc_cond, NeoRadosTest)
+{
+ std::string_view oid = "obj";
+ co_await create_obj(oid);
+
+ auto ver = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+
+ EXPECT_EQ(0u, ver.ver);
+ EXPECT_EQ(0u, ver.tag.size());
+
+ // Increment version
+ co_await execute(oid, WriteOp{}.exec(version::inc()));
+ ver = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_GT(ver.ver, 0u);
+ EXPECT_NE(0, ver.tag.size());
+
+ auto cond_ver = ver;
+
+ co_await execute(oid, WriteOp{}.exec(version::inc()));
+
+ auto ver2 = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_GT(ver2.ver, ver.ver);
+ EXPECT_EQ(0u, ver2.tag.compare(ver.tag));
+
+ // Now check various condition tests
+ co_await execute(oid, WriteOp{}.exec(version::inc(cond_ver, VER_COND_NONE)));
+
+ ver2 = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_GT(ver2.ver, ver.ver);
+ EXPECT_EQ(0u, ver2.tag.compare(ver.tag));
+
+ // A bunch of conditions that should fail
+ co_await expect_error_code(
+ execute(oid, WriteOp{}.exec(version::inc(cond_ver, VER_COND_EQ))),
+ operation_canceled);
+
+ co_await expect_error_code(
+ execute(oid, WriteOp{}.exec(version::inc(cond_ver, VER_COND_LT))),
+ operation_canceled);
+
+ co_await expect_error_code(
+ execute(oid, WriteOp{}.exec(version::inc(cond_ver, VER_COND_LE))),
+ operation_canceled);
+
+ co_await expect_error_code(
+ execute(oid, WriteOp{}.exec(version::inc(cond_ver, VER_COND_TAG_NE))),
+ operation_canceled);
+
+ ver2 = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_GT(ver2.ver, ver.ver);
+ EXPECT_EQ(0u, ver2.tag.compare(ver.tag));
+
+ /* a bunch of conditions that should succeed */
+ co_await execute(oid, WriteOp{}.exec(version::inc(ver2, VER_COND_EQ)));
+ co_await execute(oid, WriteOp{}.exec(version::inc(cond_ver, VER_COND_GT)));
+ co_await execute(oid, WriteOp{}.exec(version::inc(cond_ver, VER_COND_GE)));
+
+ co_await execute(oid, WriteOp{}
+ .exec(version::inc(cond_ver, VER_COND_TAG_EQ)));
+}
+
+CORO_TEST_F(neocls_version, test_version_inc_check, NeoRadosTest)
+{
+ std::string_view oid = "obj";
+ co_await create_obj(oid);
+
+ auto ver = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_EQ(0u, ver.ver);
+ EXPECT_EQ(0u, ver.tag.size());
+
+ // Increment version
+ co_await execute(oid, WriteOp{}.exec(version::inc()));
+
+ ver = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_GT(ver.ver, 0u);
+ EXPECT_NE(0u, ver.tag.size());
+
+ obj_version cond_ver = ver;
+
+ // a bunch of conditions that should succeed
+ co_await execute(oid, ReadOp{}.exec(version::check(cond_ver, VER_COND_EQ)));
+
+ co_await execute(oid, ReadOp{}.exec(version::check(cond_ver, VER_COND_GE)));
+
+ co_await execute(oid, ReadOp{}.exec(version::check(cond_ver, VER_COND_LE)));
+
+ co_await execute(oid, ReadOp{}
+ .exec(version::check(cond_ver, VER_COND_TAG_EQ)));
+
+ co_await execute(oid, WriteOp{}.exec(version::inc()));
+
+ auto ver2 = co_await version::read(rados(), oid, pool(), asio::use_awaitable);
+ EXPECT_GT(ver2.ver, ver.ver);
+ EXPECT_EQ(0, ver2.tag.compare(ver.tag));
+
+ // A bunch of conditions that should fail
+ co_await expect_error_code(
+ execute(oid, ReadOp{}.exec(version::check(ver, VER_COND_LT))),
+ operation_canceled);
+
+ co_await expect_error_code(
+ execute(oid, ReadOp{}.exec(version::check(ver, VER_COND_LE))),
+ operation_canceled);
+
+ co_await expect_error_code(
+ execute(oid, ReadOp{}.exec(version::check(ver, VER_COND_TAG_NE))),
+ operation_canceled);
+}
+
+TEST(neocls_version_bare, lambdata)
+{
+ asio::io_context c;
+
+ std::string_view oid = "obj";
+
+ obj_version iver{123, "foo"};
+ obj_version ever;
+
+ std::optional<neorados::RADOS> rados;
+ neorados::IOContext pool;
+ neorados::RADOS::Builder{}.build(c, [&](error_code ec, neorados::RADOS r_) {
+ ASSERT_FALSE(ec);
+ rados = std::move(r_);
+ create_pool(*rados, get_temp_pool_name(),
+ [&](error_code ec, int64_t poolid) {
+ ASSERT_FALSE(ec);
+ pool.set_pool(poolid);
+ neorados::WriteOp op;
+ op.create(true);
+ op.exec(version::set(iver));
+ rados->execute(oid, pool, std::move(op), [&](error_code ec) {
+ ASSERT_FALSE(ec);
+ version::read(*rados, oid, pool,
+ [&](error_code ec, obj_version over) {
+ ASSERT_FALSE(ec);
+ ASSERT_EQ(iver, over);
+ ever = over;
+ });
+ });
+ });
+ });
+ c.run();
+ ASSERT_EQ(iver, ever);
+}