]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
neorados/cls: Client for log objclass
authorAdam C. Emerson <aemerson@redhat.com>
Sat, 21 Jan 2023 06:03:48 +0000 (01:03 -0500)
committerAdam Emerson <aemerson@redhat.com>
Thu, 14 Sep 2023 21:48:00 +0000 (17:48 -0400)
Signed-off-by: Adam C. Emerson <aemerson@redhat.com>
src/neorados/CMakeLists.txt
src/neorados/cls/log.cc [new file with mode: 0644]
src/neorados/cls/log.h [new file with mode: 0644]
src/test/cls_log/CMakeLists.txt
src/test/cls_log/test_neocls_log.cc [new file with mode: 0644]

index 599e8c0c28000e6ac9407d834063e1ec2e67e8b3..89fff9b5df5983de3f88bb3e6c864c55cd021e85 100644 (file)
@@ -15,3 +15,4 @@ target_link_libraries(libneorados PRIVATE
   ${BLKID_LIBRARIES} ${CRYPTO_LIBS} ${EXTRALIBS})
 
 add_library(neorados_cls_version STATIC cls/version.cc)
+add_library(neorados_cls_log STATIC cls/log.cc)
diff --git a/src/neorados/cls/log.cc b/src/neorados/cls/log.cc
new file mode 100644 (file)
index 0000000..055694c
--- /dev/null
@@ -0,0 +1,135 @@
+// -*- 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 "log.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "include/buffer.h"
+#include "common/ceph_time.h"
+
+#include "include/neorados/RADOS.hpp"
+
+
+#include "cls/log/cls_log_types.h"
+#include "cls/log/cls_log_ops.h"
+
+namespace neorados::cls::log {
+using boost::system::error_code;
+namespace buffer = ceph::buffer;
+
+void add(neorados::WriteOp& op, std::vector<entry> entries)
+{
+  buffer::list in;
+  ::cls::log::ops::add_op call;
+  call.entries = std::move(entries);
+  encode(call, in);
+  op.exec("log", "add", in);
+}
+
+void add(neorados::WriteOp& op, entry e)
+{
+  bufferlist in;
+  ::cls::log::ops::add_op call;
+  call.entries.push_back(std::move(e));
+  encode(call, in);
+  op.exec("log", "add", in);
+}
+
+void add(neorados::WriteOp& op, ceph::real_time timestamp,
+        std::string section, std::string name,
+        buffer::list&& bl)
+{
+  bufferlist in;
+  ::cls::log::ops::add_op call;
+  call.entries.emplace_back(timestamp, std::move(section),
+                           std::move(name), std::move(bl));
+  encode(call, in);
+  op.exec("log", "add", in);
+}
+
+void list(ReadOp& op, ceph::real_time from, ceph::real_time to,
+         std::optional<std::string> in_marker, std::span<entry> entries,
+         std::span<entry>* result,
+         std::optional<std::string>* const out_marker)
+{
+  bufferlist inbl;
+  ::cls::log::ops::list_op call;
+  call.from_time = from;
+  call.to_time = to;
+  call.marker = std::move(in_marker).value_or("");
+  call.max_entries = entries.size();
+
+  encode(call, inbl);
+  op.exec("log", "list", inbl,
+          [entries, result, out_marker](error_code ec, const buffer::list& bl) {
+            ::cls::log::ops::list_ret ret;
+            if (!ec) {
+             auto iter = bl.cbegin();
+             decode(ret, iter);
+             if (result) {
+               *result = entries.first(ret.entries.size());
+               std::move(ret.entries.begin(), ret.entries.end(),
+                         entries.begin());
+             }
+             if (out_marker) {
+               *out_marker = (ret.truncated ?
+                              std::move(ret.marker) :
+                              std::optional<std::string>{});
+             }
+            }
+          });
+}
+
+void trim(neorados::WriteOp& op, ceph::real_time from_time,
+         ceph::real_time to_time)
+{
+  bufferlist in;
+  ::cls::log::ops::trim_op call;
+  call.from_time = from_time;
+  call.to_time = to_time;
+  encode(call, in);
+  op.exec("log", "trim", in);
+}
+
+void trim(neorados::WriteOp& op, std::string from_marker,
+         std::string to_marker)
+{
+  bufferlist in;
+  ::cls::log::ops::trim_op call;
+  call.from_marker = std::move(from_marker);
+  call.to_marker = std::move(to_marker);
+  encode(call, in);
+  op.exec("log", "trim", in);
+}
+
+void info(neorados::ReadOp& op, header* const h)
+{
+  buffer::list inbl;
+  ::cls::log::ops::info_op call;
+
+  encode(call, inbl);
+
+  op.exec("log", "info", inbl,
+          [h](error_code ec,
+              const buffer::list& bl) {
+            ::cls::log::ops::info_ret ret;
+            if (!ec) {
+             auto iter = bl.cbegin();
+             decode(ret, iter);
+             if (h)
+               *h = std::move(ret.header);
+            }
+          });
+}
+} // namespace neorados::cls::log
diff --git a/src/neorados/cls/log.h b/src/neorados/cls/log.h
new file mode 100644 (file)
index 0000000..7d07c0c
--- /dev/null
@@ -0,0 +1,306 @@
+// -*- 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 neodrados/cls/log.h
+///
+/// \brief NeoRADOS interface to OMAP based log class
+///
+/// The `log` object class stores a time-indexed series of entries in
+/// the OMAP of a given object.
+
+#include <span>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include <boost/asio/awaitable.hpp>
+#include <boost/asio/co_spawn.hpp>
+#include <boost/asio/detached.hpp>
+#include <boost/asio/redirect_error.hpp>
+#include <boost/asio/use_awaitable.hpp>
+
+#include <boost/asio/experimental/co_composed.hpp>
+
+#include <boost/system/error_code.hpp>
+#include <boost/system/errc.hpp>
+
+#include "include/buffer.h"
+
+#include "include/neorados/RADOS.hpp"
+
+#include "common/ceph_time.h"
+
+#include "cls/log/cls_log_ops.h"
+#include "cls/log/cls_log_types.h"
+
+#include "common.h"
+
+namespace neorados::cls::log {
+using ::cls::log::entry;
+using ::cls::log::header;
+static constexpr auto max_list_entries = 1000u;
+
+/// \brief Push entries to the log
+///
+/// Append a call to a write operation that adds a set of entries to the log
+///
+/// \param op Write operation to modify
+/// \param entries Entries to push
+void add(WriteOp& op, std::vector<entry> entries);
+
+/// \brief Push an entry to the log
+///
+/// Append a call to a write operation that adds an entry to the log
+///
+/// \param op Write operation to modify
+/// \param entry Entry to push
+void add(WriteOp& op, const entry& entry);
+
+/// \brief List log entries
+///
+/// Append a call to a write operation that adds an entry to the log
+///
+/// \param op Write operation to modify
+/// \param timestamp Timestamp of the log entry
+/// \param section Annotation string included in the entry
+/// \param name Log entry name
+/// \param bl Data held in the log entry
+void add(WriteOp& op, ceph::real_time timestamp, std::string section,
+        std::string name, buffer::list&& bl);
+
+/// \brief List log entries
+///
+/// Append a call to a read operation that lists log entries
+///
+/// \param op Write operation to modify
+/// \param from Start of range
+/// \param to End of range
+/// \param in_marker Point to resume truncated listing
+/// \param entries Span giving location to store entries
+/// \param result Span giving entries actually stored
+/// \param marker Place to store marker to resume truncated listing
+/// \param truncated Place to store truncation status (true means
+///                  there's more to list)
+void list(ReadOp& op, ceph::real_time from, ceph::real_time to,
+         std::optional<std::string> in_marker, std::span<entry> entries,
+         std::span<entry>* result,
+         std::optional<std::string>* const out_marker);
+
+/// \brief List log entries
+///
+/// Execute an asynchronous operation that lists log entries
+///
+/// \param r RADOS handle
+/// \param o Object associated with log
+/// \param ioc Object locator context
+/// \param from Start of range
+/// \param to End of range
+/// \param in_marker Point to resume truncated listing
+///
+/// \return (entries, marker) in a way appropriate to the
+/// completion token. See Boost.Asio documentation.
+template<boost::asio::completion_token_for<
+          void(boost::system::error_code, std::span<entry>,
+                std::optional<std::string>)> CompletionToken>
+auto list(RADOS& r, Object o, IOContext ioc, ceph::real_time from,
+         ceph::real_time to, std::optional<std::string> in_marker,
+         std::span<entry> entries, CompletionToken&& token)
+{
+  using namespace std::literals;
+  ::cls::log::ops::list_op req;
+  req.from_time = from;
+  req.to_time = to;
+  req.marker = std::move(in_marker).value_or("");
+  req.max_entries = entries.size();
+  return exec<::cls::log::ops::list_ret>(
+    r, std::move(o), std::move(ioc),
+    "log"s, "list"s, req,
+    [entries](const ::cls::log::ops::list_ret& ret) {
+      auto res = entries.first(ret.entries.size());
+      std::move(ret.entries.begin(), ret.entries.end(),
+               res.begin());
+      std::optional<std::string> marker;
+      if (ret.truncated) {
+       marker = std::move(ret.marker);
+      }
+      return std::make_tuple(res, std::move(marker));
+    }, std::forward<CompletionToken>(token));
+}
+
+/// \brief Get log header
+///
+/// Append a call to a read operation that returns the log header
+///
+/// \param op Write operation to modify
+/// \param header Place to store the log header
+void info(ReadOp& op, header* const header);
+
+/// \brief Get log header
+///
+/// Execute an asynchronous operation that returns the log header
+///
+/// \param r RADOS handle
+/// \param o Object associated with log
+/// \param ioc Object locator context
+///
+/// \return The log header in a way appropriate to the completion
+/// token. See Boost.Asio documentation.
+template<typename CompletionToken>
+auto info(RADOS& r, Object o, IOContext ioc, CompletionToken&& token)
+{
+  using namespace std::literals;
+  return exec<::cls::log::ops::list_ret>(
+    r, std::move(o), std::move(ioc),
+    "log"s, "info"s,
+    [](const ::cls::log::ops::info_ret& ret) {
+      return ret.header;
+    }, std::forward<CompletionToken>(token));
+}
+
+// Since trim uses the string markers and ignores the time if the
+// string markers are present, there's no benefit to having a function
+// that takes both.
+
+/// \brief Trim entries from the log
+///
+/// Append a call to a write operation that trims a range of entries
+/// from the log.
+///
+/// \param op Write operation to modify
+/// \param from_time Start of range, based on the timestamp supplied to add
+/// \param to_time End of range, based on the timestamp supplied to add
+///
+/// \warning This operation may succeed even if not all entries have been trimmed.
+/// to ensure completion, call repeatedly until the operation returns
+/// boost::system::errc::no_message_available
+void trim(WriteOp& op, ceph::real_time from_time, ceph::real_time to_time);
+
+/// \brief Beginning marker for trim
+///
+/// A before-the-beginning marker for log entries, comparing less than
+/// any possible entry.
+inline constexpr std::string begin_marker{""};
+
+/// \brief End marker for trim
+///
+/// An after-the-end marker for log entries, comparing greater than
+/// any possible entry.
+inline constexpr std::string end_marker{"9"};
+
+/// \brief Trim entries from the log
+///
+/// Append a call to a write operation that trims a range of entries
+/// from the log.
+///
+/// \param op Write operation to modify
+/// \param from_marker Start of range, based on markers from list
+/// \param to_marker End of range, based on markers from list
+///
+/// \note Use \ref begin_marker to trim everything up to a given point.
+/// Use \ref end_marker to trim everything after a given point. Use them
+/// both together to trim all entries.
+///
+/// \warning This operation may succeed even if not all entries have been trimmed.
+/// to ensure completion, call repeatedly until the operation returns
+/// boost::system::errc::no_message_available
+void trim(WriteOp& op, std::string from_marker, std::string to_marker);
+
+/// \brief Trim entries from the log
+///
+/// Execute an asynchronous operation that trims a range of entries
+/// from the log.
+///
+/// \param op Write operation to modify
+/// \param from_marker Start of range, based on markers from list
+/// \param to_marker End of range, based on markers from list
+///
+/// \note Use \ref begin_marker to trim everything up to a given point.
+/// Use \ref end_marker to trim everything after a given point. Use them
+/// both together to trim all entries.
+///
+/// \return As appropriate to the completion token. See Boost.Asio
+/// documentation.
+template<typename CompletionToken>
+auto trim(RADOS& r, Object oid, IOContext ioc, std::string from_marker,
+         std::string to_marker, CompletionToken&& token)
+{
+  namespace asio = boost::asio;
+  using boost::system::error_code;
+  using boost::system::system_error;
+  using ceph::real_time;
+  using boost::system::errc::no_message_available;
+
+  return asio::async_initiate<CompletionToken,
+                             void(error_code)>
+    (asio::experimental::co_composed<void(error_code)>
+     ([](auto state, RADOS& r, Object oid, IOContext ioc,
+        std::string from_marker, std::string to_marker) -> void {
+       try {
+        for (;;) {
+          WriteOp op;
+          trim(op, from_marker, to_marker);
+          co_await r.execute(oid, ioc, std::move(op), asio::deferred);
+        }
+       } catch (const system_error& e) {
+        if (e.code() != no_message_available) {
+          co_return e.code();
+        }
+       }
+       co_return error_code{};
+     }, r.get_executor()),
+     token, std::ref(r), std::move(oid), std::move(ioc),
+     std::move(from_marker), std::move(to_marker));
+}
+
+/// \brief Trim entries from the log
+///
+/// Execute an asynchronous operation that trims a range of entries
+/// from the log.
+///
+/// \param op Write operation to modify
+/// \param from_time Start of range, based on the timestamp supplied to add
+/// \param to_time End of range, based on the timestamp supplied to add
+///
+/// \return As appropriate to the completion token. See Boost.Asio
+/// documentation.
+template<typename CompletionToken>
+auto trim(RADOS& r, Object oid, IOContext ioc, ceph::real_time from_time,
+         ceph::real_time to_time, CompletionToken&& token)
+{
+  namespace asio = boost::asio;
+  using boost::system::error_code;
+  using boost::system::system_error;
+  using ceph::real_time;
+  using boost::system::errc::no_message_available;
+
+  return asio::async_initiate<CompletionToken,
+                             void(error_code)>
+    (asio::experimental::co_composed<void(error_code)>
+     ([](auto state, RADOS& r, Object oid, IOContext ioc,
+        real_time from_time, real_time to_time) -> void {
+       try {
+        for (;;) {
+          WriteOp op;
+          trim(op, from_time, to_time);
+          co_await r.execute(oid, ioc, std::move(op), asio::deferred);
+        }
+       } catch (const system_error& e) {
+        if (e.code() != no_message_available) {
+          co_return e.code();
+        }
+       }
+       co_return error_code{};
+     }, r.get_executor()),
+     token, std::ref(r), std::move(oid), std::move(ioc), from_time, to_time);
+}
+} // namespace neorados::cls::log
index b5a88d47c173d3b7975f747e6c734d126fb6c0a6..dd4309d2ac3c498925e130e7d2b5da9df9ee03ae 100644 (file)
@@ -14,3 +14,20 @@ target_link_libraries(ceph_test_cls_log
 install(TARGETS
   ceph_test_cls_log
   DESTINATION ${CMAKE_INSTALL_BINDIR})
+
+add_executable(ceph_test_neocls_log
+  test_neocls_log.cc
+  )
+target_link_libraries(ceph_test_neocls_log
+  neorados_cls_log
+  libneorados
+  ${BLKID_LIBRARIES}
+  ${CMAKE_DL_LIBS}
+  ${CRYPTO_LIBS}
+  ${EXTRALIBS}
+  neoradostest-support
+  ${UNITTEST_LIBS}
+  )
+install(TARGETS
+  ceph_test_neocls_log
+  DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/src/test/cls_log/test_neocls_log.cc b/src/test/cls_log/test_neocls_log.cc
new file mode 100644 (file)
index 0000000..1aa79cc
--- /dev/null
@@ -0,0 +1,503 @@
+// -*- 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/log.h"
+
+#include <coroutine>
+#include <chrono>
+#include <map>
+#include <string>
+#include <string_view>
+
+#include <boost/asio/use_awaitable.hpp>
+#include <boost/asio/redirect_error.hpp>
+
+#include <boost/system/errc.hpp>
+#include <boost/system/error_code.hpp>
+
+#include <fmt/format.h>
+
+#include "include/neorados/RADOS.hpp"
+
+#include "cls/version/cls_version_types.h"
+
+#include "test/neorados/common_tests.h"
+
+#include "gtest/gtest.h"
+
+using namespace std::literals;
+
+namespace chrono = std::chrono;
+
+namespace asio = boost::asio;
+
+namespace buffer = ceph::buffer;
+
+using boost::system::error_code;
+using boost::system::system_error;
+using boost::system::errc::no_message_available;
+using ceph::real_clock;
+using ceph::real_time;
+
+using neorados::RADOS;
+using neorados::IOContext;
+using neorados::Object;
+using neorados::WriteOp;
+using neorados::ReadOp;
+
+using neorados::cls::log::entry;
+
+inline constexpr auto section = "global"s;
+inline constexpr auto oid = "obj"sv;
+
+template<typename T>
+auto encode(const T& v)
+{
+  using ceph::encode;
+  buffer::list bl;
+  encode(v, bl);
+  return bl;
+}
+
+template<typename T>
+auto decode(const buffer::list& bl)
+{
+  using ceph::decode;
+  T v;
+  auto bi = bl.cbegin();
+  decode(v, bi);
+  return v;
+}
+
+auto get_time(real_time start_time, chrono::seconds i, bool modify_time)
+{
+  return modify_time ? start_time + i : start_time;
+}
+
+auto get_name(int i)
+{
+  static constexpr auto prefix = "data-source-"sv;
+  return fmt::format("{}{}", prefix, i);
+}
+
+template<boost::asio::completion_token_for<void(error_code)> CompletionToken>
+auto generate_log(RADOS& r, Object oid, IOContext ioc,
+                 int max, real_time start_time,
+                 bool modify_time, CompletionToken&& token)
+{
+// In this case, the warning is spurious as the 'mismatched' `operator
+// new` calls directly into the matching `operator new`, returning its
+// result.
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmismatched-new-delete"
+  return asio::async_initiate<CompletionToken, void(error_code)>
+    (asio::experimental::co_composed<void(error_code)>
+     ([](auto state, RADOS& r, Object oid, IOContext ioc,
+        int max, real_time start_time, bool modify_time) -> void {
+       static constexpr auto maxops = 50;
+       try {
+        for (auto i = 0; i < max;) {
+          std::vector<entry> entries;
+          for (auto ops = 0; (ops < maxops) && (i < max); ++i, ++ops) {
+            entries.emplace_back(get_time(start_time, i * 1s, modify_time),
+                                 section, get_name(i), encode(i));
+          }
+          WriteOp op;
+          neorados::cls::log::add(op, std::move(entries));
+          co_await r.execute(oid, ioc, std::move(op), asio::deferred);
+        }
+       } catch (const system_error& e) {
+        co_return {e.code()};
+       }
+       co_return {error_code{}};
+     }, r.get_executor()),
+     token, std::ref(r), std::move(oid),
+     std::move(ioc), max, start_time, modify_time);
+#pragma GCC diagnostic pop
+}
+
+void check_entry(const entry& entry, real_time start_time,
+                int i, bool modified_time)
+{
+  auto name = get_name(i);
+  auto ts = get_time(start_time, i * 1s, modified_time);
+
+  ASSERT_EQ(section, entry.section);
+  ASSERT_EQ(name, entry.name);
+  ASSERT_EQ(ts, entry.timestamp);
+}
+
+template<boost::asio::completion_token_for<void(error_code)> CompletionToken>
+auto check_log(RADOS& r, Object oid, IOContext ioc, real_time start_time,
+              int max, CompletionToken&& token)
+{
+// In this case, the warning is spurious as the 'mismatched' `operator
+// new` calls directly into the matching `operator new`, returning its
+// result.
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmismatched-new-delete"
+  return asio::async_initiate<CompletionToken, void(error_code)>
+    (asio::experimental::co_composed<void(error_code)>
+     ([](auto state, RADOS& rados, Object oid, IOContext ioc,
+        real_time start_time, int max) -> void {
+       try {
+        std::vector<entry> entries{neorados::cls::log::max_list_entries};
+        std::optional<std::string> marker;
+        int i = 0;
+        do {
+          std::span<entry> result;
+          std::tie(result, marker) =
+            co_await neorados::cls::log::list(rados, oid, ioc, {}, {},
+                                              marker, entries,
+                                              asio::deferred);
+          for (const auto& entry : result) {
+            auto num = decode<int>(entry.data);
+            EXPECT_EQ(i, num);
+            check_entry(entry, start_time, i, true);
+            ++i;
+          }
+        } while (marker);
+        EXPECT_EQ(i, max);
+       } catch (const system_error& e) {
+        co_return {e.code()};
+       }
+       co_return {error_code{}};
+     }, r.get_executor()),
+     token, std::ref(r), std::move(oid),
+     std::move(ioc), start_time, max);
+#pragma GCC diagnostic pop
+}
+
+template<typename CompletionToken>
+auto trim(RADOS& rados, Object oid, const IOContext ioc,
+         real_time from, real_time to, CompletionToken&& token)
+{
+  WriteOp op;
+  neorados::cls::log::trim(op, from, to);
+  return rados.execute(std::move(oid), std::move(ioc), std::move(op),
+                      std::forward<CompletionToken>(token));
+}
+
+template<typename CompletionToken>
+auto trim(RADOS& rados, Object oid, IOContext ioc,
+         std::string from, std::string to, CompletionToken&& token)
+{
+  neorados::WriteOp op;
+  neorados::cls::log::trim(op, std::move(from), std::move(to));
+  return rados.execute(std::move(oid), std::move(ioc), std::move(op),
+                      std::forward<CompletionToken>(token));
+}
+
+template<typename CompletionToken>
+auto list(RADOS& rados, Object oid, IOContext ioc,
+         std::span<entry> entries, std::span<entry>* result,
+         CompletionToken&& token)
+{
+  ReadOp rop;
+  neorados::cls::log::list(rop, {}, {}, {}, entries, result, nullptr);
+  return rados.execute(oid, ioc, std::move(rop), nullptr,
+                      asio::use_awaitable);
+}
+
+template<typename CompletionToken>
+auto list(RADOS& rados, Object oid, IOContext ioc,
+         real_time from, real_time to,
+         std::optional<std::string> in_marker,
+         std::span<entry> entries, std::span<entry>* result,
+         std::optional<std::string>* marker, CompletionToken&& token)
+{
+  ReadOp rop;
+  neorados::cls::log::list(rop, from, to, std::move(in_marker),
+                          entries, result, marker);
+  return rados.execute(oid, ioc, std::move(rop), nullptr,
+                      asio::use_awaitable);
+}
+
+CORO_TEST_F(neocls_log, test_log_add_same_time, NeoRadosTest)
+{
+  co_await create_obj(oid);
+
+  auto start_time = real_clock::now();
+  auto to_time = start_time + 1s;
+  co_await generate_log(rados(), oid, pool(), 10, start_time, false,
+                       asio::use_awaitable);
+
+  std::vector<entry> entries{neorados::cls::log::max_list_entries};
+  auto [res, marker] =
+    co_await neorados::cls::log::list(rados(), oid, pool(), start_time, to_time,
+                                     {}, entries, asio::use_awaitable);
+  EXPECT_EQ(10u, res.size());
+  EXPECT_FALSE(marker);
+
+  /* need to sort returned entries, all were using the same time as key */
+  std::map<int, entry> check_ents;
+
+  for (const auto& entry : res) {
+    auto num = decode<int>(entry.data);
+    check_ents[num] = entry;
+  }
+
+  EXPECT_EQ(10u, check_ents.size());
+
+  decltype(check_ents)::iterator ei;
+  int i;
+
+  for (i = 0, ei = check_ents.begin(); i < 10; i++, ++ei) {
+    const auto& entry = ei->second;
+
+    EXPECT_EQ(i, ei->first);
+    check_entry(entry, start_time, i, false);
+  }
+
+
+  res = std::span{entries}.first(1);
+  co_await list(rados(), oid, pool(), start_time, to_time, {},
+               res, &res, &marker, asio::use_awaitable);
+
+  EXPECT_EQ(1u, res.size());
+  EXPECT_TRUE(marker);
+}
+
+CORO_TEST_F(neocls_log, test_log_add_different_time, NeoRadosTest)
+{
+  co_await create_obj(oid);
+
+  /* generate log */
+  auto start_time = real_clock::now();
+  co_await generate_log(rados(), oid, pool(), 10, start_time, true,
+                       asio::use_awaitable);
+
+  std::vector<entry> entries{neorados::cls::log::max_list_entries};
+  std::optional<std::string> marker;
+  std::span<entry> result;
+
+  auto to_time = start_time + (10 * 1s);
+
+  {
+    /* check list */
+    std::tie(result, marker) =
+      co_await neorados::cls::log::list(rados(), oid, pool(), start_time,
+                                       to_time, {}, entries,
+                                       asio::use_awaitable);
+    EXPECT_EQ(10u, result.size());
+    EXPECT_FALSE(marker);
+  }
+
+  decltype(result)::iterator iter;
+  int i;
+
+  for (i = 0, iter = result.begin(); iter != result.end(); ++iter, ++i) {
+    auto& entry = *iter;
+    auto num = decode<int>(entry.data);
+    EXPECT_EQ(i, num);
+    check_entry(entry, start_time, i, true);
+  }
+
+  /* check list again with shifted time */
+  {
+    auto next_time = get_time(start_time, 1s, true);
+    std::tie(result, marker) =
+      co_await neorados::cls::log::list(rados(), oid, pool(), next_time,
+                                       to_time, {}, entries,
+                                       asio::use_awaitable);
+
+    EXPECT_EQ(9u, result.size());
+    EXPECT_FALSE(marker);
+  }
+
+  i = 0;
+  marker.reset();
+  do {
+    auto old_marker = std::move(marker);
+    std::tie(result, marker) =
+      co_await neorados::cls::log::list(rados(), oid, pool(), start_time, to_time,
+                                       old_marker, std::span{entries}.first(1),
+                                       asio::use_awaitable);
+    EXPECT_NE(old_marker, marker);
+    EXPECT_EQ(1u, result.size());
+
+    ++i;
+    EXPECT_GE(10, i);
+  } while (marker);
+
+  EXPECT_EQ(10, i);
+}
+
+CORO_TEST_F(neocls_log, trim_by_time, NeoRadosTest)
+{
+  co_await create_obj(oid);
+
+  /* generate log */
+  auto start_time = real_clock::now();
+  co_await generate_log(rados(), oid, pool(), 10, start_time, true,
+                       asio::use_awaitable);
+
+  std::vector<entry> entries{neorados::cls::log::max_list_entries};
+  std::optional<std::string> marker;
+
+  /* trim */
+  auto to_time = get_time(start_time, 10s, true);
+
+  for (int i = 0; i < 10; i++) {
+    auto trim_time = get_time(start_time, i * 1s, true);
+    co_await trim(rados(), oid, pool(), {}, trim_time, asio::use_awaitable);
+    error_code ec;
+    co_await trim(rados(), oid, pool(), {}, trim_time,
+                 asio::redirect_error(asio::use_awaitable, ec));
+    EXPECT_EQ(no_message_available, ec);
+
+    std::span<entry> result;
+    co_await list(rados(), oid, pool(), start_time, to_time, {},
+                 entries, &result, &marker, asio::use_awaitable);
+    EXPECT_EQ(9u - i, result.size());
+    EXPECT_FALSE(marker);
+  }
+}
+
+CORO_TEST_F(neocls_log, trim_by_marker, NeoRadosTest)
+{
+  co_await create_obj(oid);
+
+  auto start_time = real_clock::now();
+  co_await generate_log(rados(), oid, pool(), 10, start_time, true,
+                       asio::use_awaitable);
+  std::vector<entry> log1;
+  {
+    std::vector<entry> entries{neorados::cls::log::max_list_entries};
+    std::span<entry> result;
+    co_await list(rados(), oid, pool(), entries, &result, asio::use_awaitable);
+    EXPECT_EQ(10u, result.size());
+    log1.assign(std::make_move_iterator(result.begin()),
+                std::make_move_iterator(result.end()));
+  }
+  // trim front of log
+  {
+    const std::string from = neorados::cls::log::begin_marker;
+    const std::string to = log1[0].id;
+    co_await trim(rados(), oid, pool(), from, to, asio::use_awaitable);
+
+    std::vector<entry> entries{neorados::cls::log::max_list_entries};
+    std::span<entry> result;
+    co_await list(rados(), oid, pool(), entries, &result, asio::use_awaitable);
+
+    EXPECT_EQ(9u, result.size());
+    EXPECT_EQ(log1[1].id, result.begin()->id);
+
+    error_code ec;
+    co_await trim(rados(), oid, pool(), from, to,
+                 asio::redirect_error(asio::use_awaitable, ec));
+    EXPECT_EQ(no_message_available, ec);
+  }
+  // trim back of log
+  {
+    const std::string from = log1[8].id;
+    const std::string to = neorados::cls::log::end_marker;
+    co_await trim(rados(), oid, pool(), from, to, asio::use_awaitable);
+
+    std::vector<entry> entries{neorados::cls::log::max_list_entries};
+    std::span<entry> result;
+    co_await list(rados(), oid, pool(), entries, &result, asio::use_awaitable);
+    EXPECT_EQ(8u, result.size());
+    EXPECT_EQ(log1[8].id, result.rbegin()->id);
+
+    error_code ec;
+    co_await trim(rados(), oid, pool(), from, to,
+                 asio::redirect_error(asio::use_awaitable, ec));
+    EXPECT_EQ(no_message_available, ec);
+  }
+  // trim a key from the middle
+  {
+    const std::string from = log1[3].id;
+    const std::string to = log1[4].id;
+    co_await trim(rados(), oid, pool(), from, to, asio::use_awaitable);
+
+    std::vector<entry> entries{neorados::cls::log::max_list_entries};
+    std::span<entry> result;
+    co_await list(rados(), oid, pool(), entries, &result, asio::use_awaitable);
+
+    EXPECT_EQ(7u, result.size());
+
+    error_code ec;
+    co_await trim(rados(), oid, pool(), from, to,
+                 asio::redirect_error(asio::use_awaitable, ec));
+    EXPECT_EQ(no_message_available, ec);
+  }
+  // trim full log
+  {
+    const std::string from = neorados::cls::log::begin_marker;
+    const std::string to = neorados::cls::log::end_marker;
+    co_await trim(rados(), oid, pool(), from, to, asio::use_awaitable);
+
+    std::vector<entry> entries{neorados::cls::log::max_list_entries};
+    std::span<entry> result;
+    co_await list(rados(), oid, pool(), entries, &result, asio::use_awaitable);
+    EXPECT_EQ(0u, result.size());
+
+    error_code ec;
+    co_await trim(rados(), oid, pool(), from, to,
+                 asio::redirect_error(asio::use_awaitable, ec));
+    EXPECT_EQ(no_message_available, ec);
+  }
+}
+
+TEST(neocls_log_bare, lambdata)
+{
+  asio::io_context c;
+
+  std::string_view oid = "obj";
+
+  const auto now = real_clock::now();
+  static constexpr auto max = 10'000;
+
+  std::optional<neorados::RADOS> rados;
+  neorados::IOContext pool;
+  std::vector<entry> entries{neorados::cls::log::max_list_entries};
+
+  bool completed = false;
+  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);
+       generate_log(
+         *rados, oid, pool, max, now, true,
+         [&](error_code ec) {
+           ASSERT_FALSE(ec);
+           check_log(
+             *rados, oid, pool, now, max,
+             [&](error_code ec) {
+               ASSERT_FALSE(ec);
+               neorados::cls::log::trim(
+                 *rados, oid, pool, neorados::cls::log::begin_marker,
+                 neorados::cls::log::end_marker,
+                 [&](error_code ec) {
+                   ASSERT_FALSE(ec);
+                   neorados::cls::log::list(
+                     *rados, oid, pool, {}, {}, {}, entries,
+                     [&](error_code ec,
+                         std::span<entry> result,
+                         std::optional<std::string> marker) {
+                       ASSERT_FALSE(ec);
+                       ASSERT_FALSE(marker);
+                       ASSERT_EQ(0u, result.size());
+                       completed = true;
+                     });
+                 });
+             });
+         });
+      });
+  });
+  c.run();
+  ASSERT_TRUE(completed);
+}