option(WITH_RADOSGW_MOTR "CORTX-Motr backend for RADOS Gateway" OFF)
option(WITH_RADOSGW_DAOS "DAOS backend for RADOS Gateway" OFF)
option(WITH_RADOSGW_D4N "D4N wrapper for RADOS Gateway" ON)
-cmake_dependent_option(WITH_RADOSGW_POSIX "POSIX backend for RADOS Gateway" ON WITH_RADOSGW_DBSTORE OFF) # posix depends on dbstore
+option(WITH_RADOSGW_FDB "FoundationDB support for RADOS Gateway (experimental)" OFF)
option(WITH_RADOSGW_RADOS "RADOS backend for Rados Gateway" ON)
option(WITH_RADOSGW_SELECT_PARQUET "Support for s3 select on parquet objects" ON)
option(WITH_RADOSGW_ARROW_FLIGHT "Build arrow flight when not using system-provided arrow" OFF)
option(WITH_SYSTEM_ARROW "Use system-provided arrow" OFF)
option(WITH_SYSTEM_UTF8PROC "Use system-provided utf8proc" OFF)
+# POSIX depends on DBStore:
+cmake_dependent_option(WITH_RADOSGW_POSIX "POSIX backend for RADOS Gateway" ON WITH_RADOSGW_DBSTORE OFF)
+
if(WITH_RADOSGW)
find_package(EXPAT REQUIRED)
find_package(OATH REQUIRED)
message(STATUS "crypto soname: ${LIBCRYPTO_SONAME}")
endif (WITH_RADOSGW)
+if(WITH_RADOSGW_FDB)
+ include(${CMAKE_MODULE_PATH}/CPM.cmake)
+
+ CPMAddPackage("gh:eyalz800/zpp_bits@4.5.1")
+ message("-- enabled zpp_bits")
+
+# JFW: sadly, I'm having trouble getting FoundationDB to build, but this is VERY close
+# to "just working". But, how would we install the RPMs, etc.? This will need looking at
+# before this can be non-experimental:
+# CPMAddPackage("gh:apple/foundationdb@7.3.63")
+ message("FoundationDB support is EXPERIMENTAL and requires manual setup:")
+ message(" wget https://github.com/apple/foundationdb/releases/download/7.3.63/foundationdb-clients-7.3.63-1.el7.x86_64.rpm")
+ message(" wget https://github.com/apple/foundationdb/releases/download/7.3.63/foundationdb-server-7.3.63-1.el7.x86_64.rpm")
+ message("-- enabled FoundationDB (whether its there or not)")
+
+endif()
+
#option for CephFS
option(WITH_CEPHFS "CephFS is enabled" ON)
EXCLUDE_OPTS ${CTAG_EXCLUDES}
EXCLUDES "*.js" "*.css" ".tox" "python-common/build")
add_custom_target(tags DEPENDS ctags)
-
--- /dev/null
+# Mini-example for libfdb
+
+CXX?=g++
+CXXFLAGS+=-std=c++23 -Wno-unused-parameter -Wall -Wextra -Werror -g
+
+CEPH_SRC?=../../src
+CEPH_BUILD?=../../build
+
+INCLUDES=-I$(CEPH_SRC) -I$(CEPH_BUILD)/_deps/zpp_bits-src
+LIBRARY_PATH=-L$(CEPH_BUILD)/lib/ -Wl,-rpath,$(CEPH_BUILD)/lib
+LIBRARIES=-lfdb_c -lfmtd
+
+all: mini_example
+
+.PHONY: clean
+
+mini_example: mini_example.cc
+ $(CXX) $(CXXFLAGS) $(INCLUDES) $(LIBRARY_PATH) -o mini_example mini_example.cc $(LIBRARIES)
+
+clean:
+ rm -f mini_example
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+ * Whee! A mini-demo for libfdb!
+ *
+ * This shows how to take a user-defined data stucture and both store and
+ * retrieve it in FoundationDB.
+ *
+*/
+
+#include "rgw/fdb/interface.h"
+
+#include <print>
+#include <vector>
+#include <string>
+#include <cstdio>
+
+namespace lfdb = ceph::libfdb;
+
+using namespace std;
+
+struct person
+{
+ string name;
+
+ int year_born = 2000;
+
+ vector<string> titles;
+};
+
+static const vector<person> nifty_people {
+ {
+ .name = "Albrecht Duerer",
+ .year_born = 1471,
+ .titles = { "Painter", "Printmaker", "Northern Renaissance artist" },
+ },
+ {
+ .name = "Maria Theresa",
+ .year_born = 1717,
+ .titles = { "Habsburg ruler", "Archduchess of Austria", "Queen of Hungary and Bohemia" },
+ }
+};
+
+int main()
+try
+{
+ auto dbh = lfdb::create_database();
+
+ lfdb::set(dbh, "example/people", nifty_people);
+
+ vector<person> people;
+ if(lfdb::get(dbh, "example/people", people)) {
+ println("read {} people, some of them are likely nifty", people.size());
+ }
+}
+catch(const lfdb::libfdb_exception& e) {
+ println(stderr, "libfdb exception: {}", e.what());
+ return 1;
+}
+catch(const exception& e) {
+ println(stderr, "exception: {}", e.what());
+ return 1;
+}
+catch(...) {
+ println(stderr, "Well, now we're in a real jam, huh?");
+ return 1;
+}
const char *end_c_str() const;
char *end_c_str();
unsigned length() const { return _len; }
+ unsigned size() const { return length(); }
unsigned offset() const { return _off; }
unsigned start() const { return _off; }
unsigned end() const { return _off + _len; }
const buffers_t& buffers() const { return _buffers; }
buffers_t& mut_buffers() { return _buffers; }
void swap(list& other) noexcept;
+
unsigned length() const {
#if 0
// DEBUG: verify _len
#endif
return _len;
}
+ unsigned size() const { return length(); }
bool contents_equal(const buffer::list& other) const;
bool contents_equal(const void* other, size_t length) const;
if(WITH_RADOSGW_DAOS)
list(APPEND librgw_common_srcs driver/motr/rgw_sal_daos.cc)
endif()
+
if(WITH_RADOSGW_POSIX)
#add_subdirectory(driver/posix)
find_package(LMDB REQUIRED)
add_compile_definitions(LMDB_SAFE_NO_CPP_UTILITIES)
list(APPEND librgw_common_srcs
- driver/posix/rgw_sal_posix.cc
driver/posix/lmdb-safe.cc
- driver/posix/posixDB.cc
- driver/posix/notify.cpp)
+ driver/posix/notify.cpp
+ driver/posix/posixDB.cc
+ driver/posix/rgw_sal_posix.cc)
endif()
+
if(WITH_JAEGER)
list(APPEND librgw_common_srcs rgw_tracer.cc)
endif()
+
if(WITH_RADOSGW_ARROW_FLIGHT)
# NOTE: eventually don't want this in common but just in radosgw daemon
# list(APPEND radosgw_srcs rgw_flight.cc rgw_flight_frontend.cc)
target_link_libraries(rgw_common PRIVATE global dbstore)
endif()
-if(WITH_RADOSGW_POSIX)
- target_link_libraries(rgw_common PRIVATE global dbstore)
-endif()
-
if(WITH_RADOSGW_MOTR)
find_package(motr REQUIRED)
target_link_libraries(rgw_common PRIVATE motr::motr)
rgw_main.cc)
add_executable(radosgw ${radosgw_srcs})
+
if(WITH_RADOSGW_ARROW_FLIGHT)
# target_compile_definitions(radosgw PUBLIC WITH_ARROW_FLIGHT)
target_compile_definitions(rgw_common PUBLIC WITH_ARROW_FLIGHT)
${rgw_libs}
${ALLOC_LIBS})
+if(WITH_RADOSGW_FDB)
+ message("FoundationDB support is active (experimental)")
+
+ add_library(rgw_fdb INTERFACE)
+
+ target_link_libraries(rgw_fdb INTERFACE
+ -lfdb_c
+ -lm
+ -lrt
+ )
+
+ target_compile_options(rgw_fdb INTERFACE
+ -pthread)
+
+#JFW: this is where the FDB client library packages install
+#fdb_c headers-- we need to find a smarter way to handle this
+#end of it before libfdb can be non-experimental:
+ target_include_directories(rgw_fdb INTERFACE
+ /usr/include/foundationdb)
+
+ target_include_directories(rgw_common PRIVATE
+ /usr/include/foundationdb
+ "${CMAKE_SOURCE_DIR}/src/rgw")
+
+ target_link_libraries(rgw_common PRIVATE
+ rgw_fdb)
+
+ target_include_directories(radosgw PRIVATE
+ /usr/include/foundationdb)
+
+ # Use the zpp_bits back-end for serialization:
+ add_library(zpp_bits INTERFACE)
+
+ # JFW: there must be a nicer way to refer to this path (or ask it to be installed somewhere?):
+ target_include_directories(zpp_bits INTERFACE "${CMAKE_BINARY_DIR}/_deps/zpp_bits-src/")
+
+ target_link_libraries(rgw_fdb INTERFACE zpp_bits)
+
+endif(WITH_RADOSGW_FDB)
+
install(TARGETS radosgw DESTINATION bin)
set(radosgw_admin_srcs
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025-2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+*/
+
+#ifndef CEPH_FDB_H
+ #define CEPH_FDB_H
+
+/* Welcome, brave adventurer! This where Ceph types are converted back and forth
+between FDB's types! If you have a user type to add, this is the place!
+*/
+
+#include "include/buffer.h"
+
+#include "fdb/fdb.h"
+
+#include <span>
+#include <vector>
+#include <cstdint>
+#include <string_view>
+
+/*** Conversions: */
+
+namespace ceph::libfdb::detail {
+
+auto as_fdb_span(ceph::buffer::list& bl)
+{
+ // c_str() makes the buffer::list contiguous. Use length(), not C-string
+ // rules, because buffer::list may contain arbitrary bytes:
+ auto p = bl.c_str();
+
+ return std::span<const std::uint8_t>(
+ reinterpret_cast<const std::uint8_t *>(p), bl.length());
+}
+
+} // namespace ceph::libfdb::detail
+
+namespace ceph::buffer {
+
+auto serialize(auto& archive, ceph::buffer::list& target)
+{
+ // This should be revisited after the library is separated from the larger
+ // surrounding project-- essentially, we can't write directly /into/ the buffer::list
+ // that I know of; yet, it would obviously be great to eliminate the copy
+ // here. I believe that somewhere in zpp::bits there's probably a way to get the
+ // archive to call a custom function-- but it is not clear to me at this time and
+ // I need to move on for now, unfortunately:
+ std::vector<std::uint8_t> out;
+
+ auto r = archive(out);
+
+ // JFW: this is really annoying, but again buffer::list is not something I find
+ // easy to wrangle-- I'm not sure there's a straightforward way to just assign
+ // a new value... so if you know what that is, please improve this:
+ target.clear();
+ target.append(out);
+
+ return r;
+}
+
+auto serialize(auto& archive, const ceph::buffer::list& src)
+{
+ std::vector<std::uint8_t> bytes;
+
+ auto i = std::cbegin(src);
+
+ // buffer::list copy already resizes the vector:
+ i.copy(src.length(), bytes);
+
+ return archive(std::span<const std::uint8_t> { bytes.data(), bytes.size() });
+}
+
+// This transliteration is one-way: we read from a buffer::ptr to an archive; writing
+// to a ceph::buffer::ptr as output seems pointless to me, as we already support spans
+// and buffer::list-- I feel in fact like we would be courting danger we don't need as it
+// is a non-owning structure.
+auto serialize(auto& archive, const ceph::buffer::ptr& src)
+{
+ std::span<std::uint8_t> src_span((std::uint8_t *)src.c_str(), src.length());
+
+ return archive(src_span);
+}
+
+} // namespace ceph::buffer
+
+#endif
--- /dev/null
+# libfdb Examples
+
+"Take a thousand days of practice for forging, and ten thousand days of practice for refining."
+ -- Miyamoto Musashi, Go Rin No Sho (~1645)
+
+Welcome, traveller! Grab your favorite walking stick, and let us journey into
+the realm of libfdb!
+
+While this is not proper documentation, hopefully this "cookbook-stye" set
+of mini-examples will help you on your libfdb path.
+
+Errata: Please report errata, or contact with examples you would like to see!
+
+These examples use a short namespace alias for readability and also to save
+typing with one's poor fingers! Yoikes!
+
+See examples/libfdb/ for some working, compilable simple examples.
+
+```cpp
+namespace lfdb = ceph::libfdb;
+using namespace std::string_literals;
+```
+
+## General Recipes
+
+```cpp
+/* Use a database_handle when you desire a single logical operation. Behind
+ * the scenes, libfdb will create and complete its own transaction for you.
+ * Database-handle operations may retry after recoverable FoundationDB errors,
+ * so callbacks and output iterators may be activated more than once. */
+lfdb::set(dbh, "person/barbara-moo/name", "Barbara Moo");
+```
+
+```cpp
+/* Pass a transaction handle when several operations must be grouped in the
+ * same transaction. Do not use the transaction after commit(). */
+auto txn = lfdb::make_transaction(dbh);
+
+lfdb::set(txn, "person/barbara-moo/name", "Barbara Moo");
+lfdb::set(txn, "person/barbara-moo/book", "Accelerated C++");
+
+if (!lfdb::commit(txn)) {
+ /* Retry the transaction body with a fresh or recovered transaction. */
+}
+```
+
+## Setup
+
+```cpp
+/* Open the default FoundationDB database. */
+auto dbh = lfdb::create_database();
+```
+
+```cpp
+/* Open a database with database and network options. Flag-only options use
+ * lfdb::option_flag because they have no value. Network options are applied
+ * only during the first FoundationDB network initialization; later calls to
+ * create_database() cannot change them. */
+lfdb::database_options dbopts{
+ { FDB_DB_OPTION_TRANSACTION_TIMEOUT, std::int64_t{5000} },
+};
+
+lfdb::network_options netopts{
+ { FDB_NET_OPTION_TRACE_ENABLE, lfdb::option_flag },
+};
+
+auto dbh = lfdb::create_database(dbopts, netopts);
+```
+
+```cpp
+/* Open a database with an explicit cluster file plus database/network options. */
+auto dbh = lfdb::create_database("/path/to/fdb.cluster", dbopts, netopts);
+```
+
+## Single-Key Operations
+
+```cpp
+/* Store and retrieve one value by key. */
+lfdb::set(dbh, "person/konrad-zuse/name", "Konrad Zuse");
+
+std::string name;
+if (lfdb::get(dbh, "person/konrad-zuse/name", name)) {
+ /* use name */
+}
+```
+
+```cpp
+/* Use a callback when the raw serialized bytes must be copied or decoded
+ * immediately. The span is only valid during the callback. */
+lfdb::get(dbh, "person/konrad-zuse/name",
+ [](std::span<const std::uint8_t> bytes) {
+ /* copy or decode bytes here */
+ });
+```
+
+## Key Existence And Erase
+
+```cpp
+/* Check for a key and erase it if it exists. */
+if (lfdb::key_exists(dbh, "person/jose-capablanca/title")) {
+ lfdb::erase(dbh, "person/jose-capablanca/title");
+}
+```
+
+## Multi-Key Writes
+
+```cpp
+/* Write key/value pairs from an STL associative container in one transaction. */
+std::map<std::string, std::string> people{
+ { "person/saladin/name", "Saladin" },
+ { "person/al-khwarizmi/name", "Al-Khwarizmi" },
+ { "person/albrecht-duerer/name", "Albrecht Duerer" },
+};
+
+lfdb::set(dbh, std::begin(people), std::end(people));
+```
+
+## Multi-Key Reads
+
+```cpp
+/* Read a key range into an STL associative container. */
+std::map<std::string, std::string> people;
+
+lfdb::get(dbh,
+ lfdb::select { "person/" },
+ std::inserter(people, std::end(people)));
+```
+
+## Key Ordering
+
+```cpp
+/* FoundationDB keys are ordered lexicographically by byte string. Choose key
+ * formats so lexical order matches the scan order you want. Numeric suffixes
+ * should usually be fixed-width and zero-padded. */
+lfdb::set(dbh, "person/000001/name", "Barbara Moo");
+lfdb::set(dbh, "person/000010/name", "Konrad Zuse");
+```
+
+## Prefix Selection
+
+"select" has two constructor forms. The one-argument form is usually the one
+you want: it selects every key with a shared prefix. This is a natural fit for
+FoundationDB key design, where related records are commonly grouped under a
+prefix such as `person/`, `bucket/index/`, or `object/metadata/`.
+
+```cpp
+/* Select all keys beginning with "person/". */
+auto people = lfdb::select { "person/" };
+```
+
+Using a key beginning with 0xFF will result in unpredictable behavior.
+
+## Explicit Key Ranges
+
+```cpp
+/* Select a half-open lexicographic key range: begin is included, end is
+ * excluded. */
+auto medieval_people = lfdb::select { "person/charlemagne", "person/saladin/" };
+```
+
+## Pair Generator
+
+```cpp
+/* Use pair_generator() for ordinary range scans where processing one key/value
+ * pair at a time is natural. */
+std::map<std::string, std::string> people;
+
+std::ranges::copy(lfdb::pair_generator(dbh, lfdb::select { "person/" }),
+ std::inserter(people, std::end(people)));
+```
+
+## Block Generator
+
+For very large prefix scans, use the same one-argument selector with
+`block_generator()`. This lets libfdb plan and read the range in blocks while
+the call site still names the logical key family directly.
+
+```cpp
+/* Use block_generator() for large range scans where split planning and
+ * block-at-a-time processing are useful. */
+for (auto&& block : lfdb::block_generator(dbh, lfdb::select { "object/metadata/" })) {
+ /* process block */
+}
+```
+
+## STL Containers As Values
+
+```cpp
+/* Store an STL container as one serialized value. */
+std::vector roles{ "compiler"s, "systems"s, "naval-officer"s };
+
+lfdb::set(dbh, "person/grace-hopper/roles", roles);
+
+std::vector<std::string> out_roles;
+lfdb::get(dbh, "person/grace-hopper/roles", out_roles);
+```
+
+## Associative Containers As Values
+
+```cpp
+/* Store an associative container as one serialized value. */
+std::map<std::string, std::string> profile{
+ { "name", "Maria Theresa" },
+ { "title", "Archduchess of Austria" },
+};
+
+lfdb::set(dbh, "person/maria-theresa/profile", profile);
+
+std::map<std::string, std::string> out_profile;
+lfdb::get(dbh, "person/maria-theresa/profile", out_profile);
+```
+
+## User Types As Values
+
+```cpp
+/* Store a user-defined type as one serialized value. */
+struct person_profile
+{
+ using serialize = zpp::bits::members<3>;
+
+ std::string name;
+ std::string field;
+ std::vector<std::string> tags;
+};
+
+auto profile = person_profile{
+ .name = "Edsger Dijkstra",
+ .field = "computer science",
+ .tags = std::vector{ "algorithms"s, "formal-methods"s },
+};
+
+lfdb::set(dbh, "person/edsger-dijkstra/profile", profile);
+
+person_profile out_profile;
+lfdb::get(dbh, "person/edsger-dijkstra/profile", out_profile);
+```
+
+## Manual Transactions
+
+```cpp
+/* Group multiple operations in one explicit transaction. */
+auto txn = lfdb::make_transaction(dbh);
+
+lfdb::set(txn, "person/matilda-of-tuscany/name", "Matilda of Tuscany");
+lfdb::set(txn, "person/matilda-of-tuscany/title", "Margravine");
+
+if (!lfdb::commit(txn)) {
+ /* Retry the transaction body. */
+}
+```
+
+## Manual Transactions With Options
+
+```cpp
+/* Create an explicit transaction with transaction options. */
+lfdb::transaction_options opts{
+ { FDB_TR_OPTION_READ_YOUR_WRITES_DISABLE, lfdb::option_flag },
+};
+
+auto txn = lfdb::make_transaction(dbh, opts);
+
+lfdb::set(txn, "person/hypatia/name", "Hypatia");
+
+if (!lfdb::commit(txn)) {
+ /* Retry the transaction body. */
+}
+```
+
+## Transactors: Replayable Transactions
+
+Transactors are function objects created with `make_transactor()`. Creating a
+transactor does not start a transaction; calling `operator()` creates the
+transaction, invokes the body, and commits it.
+
+The body may be called more than once after retryable FoundationDB errors. Keep
+it deterministic and free of non-idempotent external side effects. If recovery
+is not possible, or if user code throws, the exception escapes to the caller.
+
+```cpp
+/* Use a transactor when the transaction body should be replayed after retryable
+ * FoundationDB errors. */
+auto txr = lfdb::make_transactor(dbh);
+
+txr([](auto& txn) {
+ lfdb::set(txn, "person/eleanor-of-aquitaine/name", "Eleanor of Aquitaine");
+ lfdb::set(txn, "person/eleanor-of-aquitaine/title", "Duchess of Aquitaine");
+});
+```
+
+### Transactor options
+
+```cpp
+/* Options are applied to each transaction the transactor creates. */
+lfdb::transaction_options opts{
+ { FDB_TR_OPTION_READ_YOUR_WRITES_DISABLE, lfdb::option_flag },
+};
+
+auto txr = lfdb::make_transactor(dbh, opts);
+
+txr([](auto& txn) {
+ lfdb::set(txn, "person/zenobia/name", "Zenobia");
+});
+```
+
+```cpp
+/* Retryable FoundationDB errors are handled before control returns here. */
+auto txr = lfdb::make_transactor(dbh);
+
+try {
+ txr([](auto& txn) {
+ /* User exceptions propagate; the body is not committed. */
+ validate_profile_update();
+
+ lfdb::set(txn, "person/jose-capablanca/title",
+ std::vector{ "Original Grandmaster"s, "World Chess Champion"s });
+ });
+}
+catch (const lfdb::libfdb_exception& e) {
+ /* FoundationDB reported an error that libfdb could not recover from. */
+}
+catch (const std::exception& e) {
+ /* Application or system error from user code. */
+}
+```
--- /dev/null
+
+* If you're looking for some information about how to use the library, interface.h is the "user interface".
+
+----
+Network and Database Options:
+
+There are various database at network settings that can be adjusted; see the "options" test for some examples of using
+the network_options and database_options structures. But what may be tweaked, you ask? Excellent question-- and when
+you go to fdb_c's documentation, you will find little, because they are GENERATED! And, from whence? Well... because
+this file is VERY hard to find (in the FDB source), here you are:
+ fdbclient/vexillographer/fdb.options
+
+----
+Equivalence with FDBStreamingMode:
+
+FDB_STREAMING_MODE_ITERATOR
+
+The caller is implementing an iterator (most likely in a binding to a higher level language). The amount of data returned depends on the value of the iteration parameter to fdb_transaction_get_range().
+
+FDB_STREAMING_MODE_SMALL
+
+Data is returned in small batches (not much more expensive than reading individual key-value pairs).
+
+FDB_STREAMING_MODE_MEDIUM
+
+Data is returned in batches between _SMALL and _LARGE.
+
+FDB_STREAMING_MODE_LARGE
+
+Data is returned in batches large enough to be, in a high-concurrency environment, nearly as efficient as possible. If the caller does not need the entire range, some disk and network bandwidth may be wasted. The batch size may be still be too small to allow a single client to get high throughput from the database.
+
+FDB_STREAMING_MODE_SERIAL
+
+Data is returned in batches large enough that an individual client can get reasonable read bandwidth from the database. If the caller does not need the entire range, considerable disk and network bandwidth may be wasted.
+
+FDB_STREAMING_MODE_WANT_ALL
+
+The caller intends to consume the entire range and would like it all transferred as early as possible.
+
+FDB_STREAMING_MODE_EXACT
+
+The caller has passed a specific row limit and wants that many rows delivered in a single batch.
+
+enum struct streaming_mode_t : int {
+ iterator = FDB_STREAMING_MODE_ITERATOR,
+ small = FDB_STREAMING_MODE_SMALL,
+ medium = FDB_STREAMING_MODE_MEDIUM,
+ large = FDB_STREAMING_MODE_LARGE,
+ serial = FDB_STREAMING_MODE_SERIAL,
+ all = FDB_STREAMING_MODE_WANT_ALL,
+3F exact = FDB_STREAMING_MODE_EXACT
+};
+
+...these are not defined in terms of int or enum as far as I can tell, needs more exploring.
+
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- // vim: ts=8 sw=2 smarttab ft=cpp
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025-2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+*/
+
+#ifndef CEPH_FDB_BASE_H
+ #define CEPH_FDB_BASE_H
+
+// The API version we're writing against, which can (and probably does) differ
+// from the installed version. This must be defined before the FoundationDB header
+// is included (see fdb_c_apiversion.g.h):
+#define FDB_API_VERSION 730
+#include <foundationdb/fdb_c.h>
+
+// Ceph uses libfmt rather than <format>:
+#include <fmt/format.h>
+#include <fmt/ranges.h>
+
+#include <tuple>
+#include <mutex>
+#include <memory>
+#include <span>
+#include <ranges>
+#include <thread>
+#include <vector>
+#include <cstdint>
+#include <utility>
+#include <variant>
+#include <optional>
+#include <iterator>
+#include <concepts>
+#include <algorithm>
+#include <generator>
+#include <exception>
+#include <functional>
+#include <filesystem>
+#include <type_traits>
+
+#ifdef __cpp_lib_flat_map
+ #include <flat_map>
+ template <typename ...Args>
+ using flat_map = std::flat_map<Args...>;
+#else
+ #include <boost/container/flat_map.hpp>
+ template <typename ...Args>
+ using flat_map = boost::container::flat_map<Args...>;
+#endif
+
+// Wrangle some forward declarations:
+namespace ceph::libfdb {
+
+struct select;
+
+class database;
+class transaction;
+
+using database_handle = std::shared_ptr<database>;
+using transaction_handle = std::shared_ptr<transaction>;
+
+extern transaction_handle make_transaction(database_handle dbh);
+
+} // namespace ceph::libfdb
+
+// MOAR forward declarations-- "pay no attention to that man behind the curtain":
+namespace ceph::libfdb::detail {
+
+template <typename T, typename ...Ts>
+concept is_any_of = (std::is_same_v<T, Ts> || ...);
+
+struct future_value;
+
+template <typename ValueT = std::string>
+std::pair<std::string, ValueT> to_decoded_kv_pair(const FDBKeyValue& kv);
+
+inline fdb_error_t do_commit(transaction_handle& txn);
+
+inline void transaction_set_kv_bytes(const transaction_handle& txn,
+ std::span<const std::uint8_t> k,
+ std::span<const std::uint8_t> v);
+
+inline future_value await_ready_key_range_future(transaction_handle txn, const ceph::libfdb::select&, int);
+inline future_value await_ready_keyvalue_range_future(transaction_handle txn, const ceph::libfdb::select& key_range, int& iteration);
+
+// A generator that produces successive spans for a range:
+inline std::generator<std::span<const FDBKeyValue>> generate_FDB_pairs(ceph::libfdb::transaction& txn, ceph::libfdb::select key_range);
+
+// Stores a successively-generated of kv pair results to an iterator:
+template <typename OutIterT>
+requires std::output_iterator<OutIterT, std::pair<std::string, std::string>>
+inline bool get_value_range_from_transaction(ceph::libfdb::transaction& txn, const ceph::libfdb::select& key_range, OutIterT out_iter);
+
+} // namespace ceph::libfdb::detail
+
+namespace ceph::libfdb::concepts {
+
+// Note that "stringlikes" are not all "stringview-likes", such as when they can be
+// written to:
+template <typename StringViewLikeT>
+concept stringview_convertible = std::convertible_to<StringViewLikeT, std::string_view>;
+
+template <typename IteratorT>
+concept key_value_iterator =
+ std::input_iterator<IteratorT> and
+ requires(const std::iter_value_t<IteratorT>& kv) {
+ kv.first;
+ kv.second;
+ };
+
+template <typename FnT>
+concept value_callback =
+ std::invocable<FnT&, std::span<const std::uint8_t>>;
+
+template <typename T>
+concept value_output =
+ !value_callback<std::remove_reference_t<T>> &&
+ std::is_lvalue_reference_v<T>;
+
+template <typename T>
+concept storable_invocation_result =
+ !std::is_void_v<T> && !std::is_reference_v<T>;
+
+template <typename T>
+concept supported_invocation_result =
+ std::is_void_v<T> || storable_invocation_result<T>;
+
+// There's a high likelihood that we're going to get more sophisticated selectors,
+// so this is doing a more important job than it may appear to be:
+template <typename T>
+concept selector = ceph::libfdb::detail::is_any_of<T, ceph::libfdb::select>;
+
+} // namespace ceph::libfdb::concepts
+
+// libfdb_exception: How to deal, when Bad Things(TM) happen:
+namespace ceph::libfdb {
+
+// Should we commit after the (possibly) mutating operation?
+enum struct commit_after_op { commit, no_commit };
+
+struct libfdb_exception final : std::runtime_error
+{
+ using std::runtime_error::runtime_error;
+
+ fdb_error_t fdb_error_value = -1;
+
+ bool retryable() const noexcept
+ {
+ return 0 < fdb_error_value &&
+ fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE, fdb_error_value);
+ }
+
+ libfdb_exception(std::string_view msg)
+ : std::runtime_error(make_error_string(msg))
+ {}
+
+ explicit libfdb_exception(fdb_error_t fdb_error_value)
+ : std::runtime_error(make_fdb_error_string(fdb_error_value)),
+ fdb_error_value(fdb_error_value)
+ {}
+
+ static std::string make_error_string(const std::string_view msg)
+ {
+ return fmt::format("libfdb: {}", msg);
+ }
+
+ static std::string make_fdb_error_string(const fdb_error_t ec)
+ {
+ return make_error_string(fmt::format("FoundationDB error {}: {}", ec, fdb_get_error(ec)));
+ }
+};
+
+// A straightforward but pretty handy predicate-- ask an exception if it's something the
+// user can try replaying an operation to correct:
+inline bool retryable(const libfdb_exception& e) noexcept
+{
+ return e.retryable();
+}
+
+namespace detail {
+
+struct future_value final
+{
+ std::unique_ptr<FDBFuture, decltype(&fdb_future_destroy)> future_ptr;
+
+ public:
+ explicit future_value(FDBFuture *future_handle)
+ : future_ptr(future_handle, &fdb_future_destroy)
+ {}
+
+ public:
+ FDBFuture *raw_handle() const noexcept { return future_ptr.get(); }
+
+ private:
+ void destroy() noexcept { future_ptr.reset(nullptr); }
+
+ private:
+ friend class ceph::libfdb::transaction;
+};
+
+constexpr auto as_fdb_span(const char *s)
+{
+ return std::span<const std::uint8_t>((const std::uint8_t *)s, std::strlen(s));
+}
+
+constexpr auto as_fdb_span(std::string_view sv)
+{
+ return std::span<const std::uint8_t>((const std::uint8_t *)sv.data(), sv.size());
+}
+
+/* Prefix selectors assume user keys do not start with 0xFF. Appending 0xFF
+would exclude valid keys (beginning with prefix + 0xFF), so we build the
+next-in-lexicographic-order key instead. Callers can still specify an
+explicit full range if they need something special: */
+inline std::string make_range_end_key_for_prefix(std::string_view prefix)
+{
+ if (prefix.empty()) {
+ return "\xFF";
+ }
+
+ if (0xFF == static_cast<unsigned char>(prefix.front())) {
+ throw libfdb_exception("requested prefix has no (finite) end key");
+ }
+
+ auto i = prefix.find_last_not_of(static_cast<char>(0xFF));
+ auto end_key = std::string(prefix.substr(0, i + 1));
+
+ end_key.back() = static_cast<char>(static_cast<unsigned char>(end_key.back()) + 1);
+
+ return end_key;
+}
+
+} // namespace ceph::libfdb::detail
+
+/* lfdb::select is for handling batch queries-- there are two constructors to consider:
+ - single-key select creates the range containing all keys with that prefix;
+ - passing a start and end key creates a half-open lexicographic key range: [begin_key, end_key). */
+struct select final
+{
+ std::string begin_key, end_key;
+
+ public:
+ // We'll eventually need a way to get settings into the base library from the binding layer; there
+ // are a few ways we could do it, this is one I'm mulling over; do not use this right now.
+ mutable struct {
+ int stride = 0; // "unlimited"
+
+ // Some parts of the documentation claim FDB_STREAMING_MODE_ITERATOR is the default, other parts
+ // don't... examples tend to use FDB_STREAMING_MODE_WANT_ALL, but they operate on fairly small amounts
+ // of data. It's pretty hard to understand what the Right Thing(TM) to do is, the sure the answer may
+ // evolve as I learn more, but for now this setting at least means it's "plumbed through" (this
+ // particular mode starts with small batches and then grows to larger increments as more data is sent),
+ // even though this isn't really used at the moment:
+ FDBStreamingMode streaming_mode = FDB_STREAMING_MODE_ITERATOR;
+
+ } options;
+
+ public:
+ select(std::string_view begin_key_, std::string_view end_key_)
+ : begin_key(begin_key_), end_key(end_key_)
+ {}
+
+ select(std::string_view prefix)
+ : begin_key(prefix),
+ end_key(detail::make_range_end_key_for_prefix(prefix))
+ {}
+
+};
+
+// Flag-only options are indicated with an explicit option_flag, as they have no actual value:
+struct option_flag_t final {};
+inline constexpr option_flag_t option_flag;
+
+using option_value = std::variant<option_flag_t, std::int64_t, std::string, std::vector<std::uint8_t>>;
+
+// i.e. option /code/ to the value of the option itself (e.g. FDB_FOO, 42):
+template <typename OptionCodeT>
+using option_map = flat_map<OptionCodeT, option_value>;
+
+using network_options = option_map<FDBNetworkOption>;
+using database_options = option_map<FDBDatabaseOption>;
+using transaction_options = option_map<FDBTransactionOption>;
+
+namespace detail {
+
+// Note that these are specific to FDB's needs (see return type, casts):
+constexpr const std::uint8_t *data_of(const std::vector<std::uint8_t>& xs) { return (const std::uint8_t *)xs.data(); }
+constexpr const std::uint8_t *data_of(const std::string& xs) { return (const std::uint8_t *)xs.data(); }
+constexpr const std::uint8_t *data_of(const std::int64_t& x) { return reinterpret_cast<const std::uint8_t *>(&x); }
+constexpr const std::uint8_t *data_of(const option_flag_t&) { return nullptr; }
+
+// Note that these are specific to FDB's needs (see return type, casts):
+constexpr int size_of(const std::vector<std::uint8_t>& xs) { return static_cast<int>(xs.size()); }
+constexpr int size_of(const std::string& xs) { return static_cast<int>(xs.size()); }
+constexpr int size_of(const std::int64_t) { return 8; } // 8b = size required by FDB
+constexpr int size_of(const option_flag_t&) { return 0; }
+
+// ...also used:
+constexpr std::uint8_t *data_of(std::string_view xs) { return (std::uint8_t *)xs.data(); }
+constexpr int size_of(std::string_view xs) { return static_cast<int>(xs.size()); }
+
+inline auto as_fdb_option_args(const auto& option)
+{
+ auto [data, size] = std::visit([](const auto& x) {
+ return std::tuple { data_of(x), size_of(x) };
+ },
+ option.second);
+
+ return std::tuple { option.first, data, size };
+}
+
+inline void apply_options(const auto& option_map, auto&& set_option)
+{
+ std::ranges::for_each(option_map, [&set_option](const auto& option) {
+ auto args = as_fdb_option_args(option);
+
+ if (auto r = std::apply(set_option, args); 0 != r) {
+ throw libfdb_exception(fmt::format("while setting option {}; {}",
+ static_cast<int>(option.first),
+ libfdb_exception::make_fdb_error_string(r)));
+ }
+ });
+}
+
+// The global DB state and management thread:
+// JFW: more user hooks that go into FDB system possible here
+class database_system final
+{
+ database_system() = delete;
+
+ private:
+ static inline bool was_initialized = false;
+
+ static inline std::once_flag fdb_was_initialized;
+ static inline std::jthread fdb_network_thread;
+
+ static inline void initialize_fdb(const network_options& options)
+ {
+ // This must be called before ANY other API function:
+ if (fdb_error_t r = fdb_select_api_version(FDB_API_VERSION); 0 != r)
+ throw libfdb_exception(r);
+
+ // Zero or more calls to this may now be made:
+ apply_options(options, fdb_network_set_option);
+
+ // This must be called before any other API function (besides >= 0 calls to fdb_network_set_option()):
+ if (fdb_error_t r = fdb_setup_network(); 0 != r)
+ throw libfdb_exception(r);
+
+ // Launch network thread:
+ fdb_network_thread = std::jthread { &fdb_run_network };
+
+ // Okie-dokie, we're all set (distinct from fdb_was_initialized):
+ was_initialized = true;
+ }
+
+ private:
+ database_system(const network_options& network_opts)
+ {
+ std::call_once(ceph::libfdb::detail::database_system::fdb_was_initialized,
+ ceph::libfdb::detail::database_system::initialize_fdb,
+ network_opts);
+ }
+
+ public:
+ static bool& initialized() noexcept { return was_initialized; }
+
+ public:
+ static inline void shutdown_fdb()
+ {
+ using namespace std::chrono_literals;
+
+ if (not initialized()) {
+ return;
+ }
+
+ // shut down network and database:
+ if (int r = fdb_stop_network(); 0 != r)
+ {
+ // In this case, we likely don't want to throw from our dtor, but we may
+ // have something to log. As there's no higher-level hook for that, we
+ // have nothing to do right now.
+ // fmt::println("database::shutdown_fdb() error {}", r);
+ }
+
+ // This may not actually be needed, but it's a traditional courtesy:
+ std::this_thread::sleep_for(7ms);
+
+ if (fdb_network_thread.joinable()) {
+ fdb_network_thread.join();
+ }
+ }
+
+ private:
+ friend struct ceph::libfdb::database;
+};
+
+} // namespace ceph::libfdb::detail
+
+class database final
+{
+ detail::database_system db_system;
+
+ private:
+ std::shared_ptr<FDBDatabase> db_handle;
+
+ FDBDatabase *create_database_ptr(const std::filesystem::path cluster_file_path) {
+ FDBDatabase *fdbp = nullptr;
+
+ if (fdb_error_t r = fdb_create_database(cluster_file_path.c_str(), &fdbp); 0 != r) {
+ throw libfdb_exception(r);
+ }
+
+ return fdbp;
+ }
+
+ FDBTransaction *create_transaction() {
+ FDBTransaction *txn_p = nullptr;
+
+ if (fdb_error_t r = fdb_database_create_transaction(raw_handle(), &txn_p); 0 != r) {
+ throw libfdb_exception(r);
+ }
+
+ return txn_p;
+ }
+
+ public:
+ database(const std::filesystem::path cluster_file_path, const ceph::libfdb::database_options& db_opts, const network_options& network_opts)
+ : db_system(network_opts),
+ db_handle(create_database_ptr(cluster_file_path), &fdb_database_destroy)
+ {
+ detail::apply_options(db_opts,
+ [handle = raw_handle()](auto option_code, auto data, auto size) {
+ return fdb_database_set_option(handle, option_code, data, size);
+ });
+ }
+
+ database(const std::filesystem::path cluster_file_path, const ceph::libfdb::database_options& db_opts)
+ : database(cluster_file_path, db_opts, {})
+ {}
+
+ database(const ceph::libfdb::database_options& db_opts, const ceph::libfdb::network_options& net_opts)
+ : database("", db_opts, net_opts)
+ {}
+
+ database(const ceph::libfdb::database_options& db_opts)
+ : database("", db_opts, {})
+ {}
+
+ database(const std::filesystem::path cluster_file_path)
+ : database(cluster_file_path, {}, {})
+ {}
+
+ database()
+ : database(std::filesystem::path {}, {}, {})
+ {}
+
+ public:
+ explicit operator bool() const noexcept { return nullptr != raw_handle(); }
+
+ public:
+ FDBDatabase *raw_handle() const noexcept { return db_handle.get(); }
+
+ private:
+ friend transaction;
+
+};
+
+class transaction final
+{
+ database_handle dbh;
+
+ std::unique_ptr<FDBTransaction, decltype(&fdb_transaction_destroy)> txn_handle;
+
+ private:
+ bool get_single_value_from_transaction(const std::span<const std::uint8_t>& key, std::invocable<std::span<const std::uint8_t>> auto&& write_output);
+
+ public:
+ transaction(database_handle dbh_)
+ : dbh(dbh_),
+ txn_handle(dbh->create_transaction(), &fdb_transaction_destroy)
+ {}
+
+ transaction(database_handle dbh_, const transaction_options& opts)
+ : transaction(dbh_)
+ {
+ detail::apply_options(opts,
+ [handle = raw_handle()](auto code, auto data, auto size) {
+ return fdb_transaction_set_option(handle, code, data, size);
+ });
+ }
+
+ public:
+ explicit operator bool() const noexcept { return dbh && nullptr != raw_handle(); }
+
+ public:
+ FDBTransaction *raw_handle() const noexcept { return txn_handle.get(); }
+
+ private:
+ void set(std::span<const std::uint8_t> k, std::span<const std::uint8_t> v) {
+ fdb_transaction_set(raw_handle(),
+ (const uint8_t*)k.data(), k.size(),
+ (const uint8_t*)v.data(), v.size());
+ }
+
+ // JFW: it's not as easy to wedge an output_range into here as it appears, perhaps
+ // needs to be revisited; I'm binding it to what's actually used in practice for now:
+ template <typename OutIterT>
+ requires std::output_iterator<OutIterT, std::pair<std::string, std::string>>
+ bool get(const ceph::libfdb::select& key_range, OutIterT out_iter) {
+ return ceph::libfdb::detail::get_value_range_from_transaction(*this, key_range, out_iter);
+ }
+
+ bool get(const std::span<const std::uint8_t> k, concepts::value_callback auto&& val_collector) {
+ return get_single_value_from_transaction(k, val_collector);
+ }
+
+ void erase(std::span<const std::uint8_t> k) {
+ fdb_transaction_clear(raw_handle(),
+ (const std::uint8_t *)k.data(), k.size());
+ }
+
+ void erase(const ceph::libfdb::concepts::selector auto& key_range) {
+ fdb_transaction_clear_range(raw_handle(),
+ (const uint8_t *)key_range.begin_key.data(), key_range.begin_key.size(),
+ (const uint8_t *)key_range.end_key.data(), key_range.end_key.size());
+ }
+
+ bool key_exists(std::string_view k) {
+ return get_single_value_from_transaction(detail::as_fdb_span(k), [](auto) {});
+ }
+
+ bool commit(fdb_error_t *replay_error = nullptr);
+ void destroy() noexcept { txn_handle.reset(); }
+
+ // As you can see, friends are used extensively to implement the public interface; it would be nice to come up
+ // with a strategy for making these "lists" a bit more managable, but for now it's what we have and it allows me ot
+ // keep these handles mostly-opaque (this has proven to be a good idea as I've a few times shuffled internal details
+ // with no disruption to the user interface surface):
+ private:
+ friend inline void set(transaction_handle, const std::string_view, const auto&, const commit_after_op);
+ friend inline void set(database_handle, std::string_view, const auto&);
+ friend inline void set(transaction_handle, std::string_view, const ceph::libfdb::concepts::stringview_convertible auto&, const commit_after_op);
+ friend inline void set(database_handle, std::string_view, const ceph::libfdb::concepts::stringview_convertible auto&);
+
+ template <typename OutputTargetOrFnT>
+ requires concepts::value_callback<std::remove_reference_t<OutputTargetOrFnT>> ||
+ concepts::value_output<OutputTargetOrFnT&&>
+ friend inline bool get(ceph::libfdb::transaction_handle,
+ std::string_view,
+ OutputTargetOrFnT&&,
+ const commit_after_op);
+ friend inline bool get(ceph::libfdb::transaction_handle, const ceph::libfdb::select&, auto, const commit_after_op);
+
+ friend inline void erase(ceph::libfdb::transaction_handle, std::string_view, const commit_after_op);
+ friend inline void erase(ceph::libfdb::transaction_handle, const ceph::libfdb::select&, const commit_after_op);
+ friend inline void erase(ceph::libfdb::database_handle, std::string_view);
+ friend inline void erase(ceph::libfdb::database_handle, const ceph::libfdb::select&);
+
+ friend inline bool key_exists(transaction_handle txn, std::string_view k, const commit_after_op commit_after);
+
+ friend inline bool commit(transaction_handle& txn);
+ friend inline fdb_error_t ceph::libfdb::detail::do_commit(transaction_handle& txn);
+ friend inline void ceph::libfdb::detail::transaction_set_kv_bytes(const transaction_handle&,
+ std::span<const std::uint8_t>,
+ std::span<const std::uint8_t>);
+
+};
+
+namespace detail {
+
+// Since lambdas cannot be friend-functions, we use a named helper:
+inline void transaction_set_kv_bytes(const transaction_handle& txn,
+ std::span<const std::uint8_t> k,
+ std::span<const std::uint8_t> v)
+{
+ txn->set(k, v);
+}
+
+} // namespace detail
+
+inline bool ceph::libfdb::transaction::get_single_value_from_transaction(const std::span<const std::uint8_t>& key, std::invocable<std::span<const std::uint8_t>> auto&& write_output)
+{
+ const fdb_bool_t is_snapshot = false;
+
+ detail::future_value fv(fdb_transaction_get(
+ raw_handle(),
+ (const uint8_t *)key.data(), key.size(),
+ is_snapshot));
+
+ if (fdb_error_t r = fdb_future_block_until_ready(fv.raw_handle()); 0 != r) {
+ throw libfdb_exception(r);
+ }
+
+ fdb_bool_t key_was_found = false;
+ const uint8_t *out_buffer = nullptr;
+ int out_len = 0;
+
+ if (fdb_error_t r = fdb_future_get_value(fv.raw_handle(), &key_was_found, &out_buffer, &out_len); 0 != r) {
+ throw libfdb_exception(r);
+ }
+
+ // No errors, but no value was found:
+ if (0 == key_was_found) {
+ return false;
+ }
+
+ write_output(std::span<const std::uint8_t>(out_buffer, out_len));
+
+ // The happy path is the simple path:
+ return true;
+}
+
+[[nodiscard]] inline bool ceph::libfdb::transaction::commit(fdb_error_t *replay_error)
+{
+ if (nullptr != replay_error) {
+ *replay_error = 0;
+ }
+
+ // We don't want to try to vivify for an "empty" commit:
+ if (!*this)
+ return false;
+
+ detail::future_value fv(fdb_transaction_commit(raw_handle()));
+
+ if (fdb_error_t r = fdb_future_block_until_ready(fv.raw_handle()); 0 != r) {
+ throw libfdb_exception(r);
+ }
+
+ if (fdb_error_t r = fdb_future_get_error(fv.raw_handle()); 0 != r) {
+ detail::future_value ferror_result(fdb_transaction_on_error(raw_handle(), r));
+
+ if (0 != fdb_future_block_until_ready(ferror_result.raw_handle())) {
+ // In their example, they use the first error as the message source, so I will do that also:
+ throw libfdb_exception(r);
+ }
+
+ if (0 != fdb_future_get_error(ferror_result.raw_handle())) {
+ // Destroy the futures AND be sure to invalidate the transaction-- according to the documentation,
+ // this must happen in the specified order (which /should/ currently match the dtor order, but I am
+ // not going to touch this any more right now):
+ fv.destroy(), ferror_result.destroy(), destroy();
+
+ // Again, use the original error:
+ throw libfdb_exception(r);
+ }
+
+ // A false result means on_error() succeeded; the caller should replay the transaction:
+ if (nullptr != replay_error) {
+ *replay_error = r;
+ }
+
+ return false;
+ }
+
+ // Ok:
+ return true;
+}
+
+} // namespace ceph::libfdb
+
+// Future-wrangling and tricky retry-handling:
+namespace ceph::libfdb::detail {
+
+inline future_value block_until_ready(future_value&& fv)
+{
+ if (fdb_error_t r = fdb_future_block_until_ready(fv.raw_handle()); 0 != r) {
+ throw libfdb_exception(r);
+ }
+
+ // Note that fdb_future_block_until_ready() does not by itself check for errors
+ // with the value; so, we need to do this separately:
+ fdb_error_t r = fdb_future_get_error(fv.raw_handle());
+
+ if (r) {
+ throw libfdb_exception(r);
+ }
+
+ return fv;
+}
+
+inline future_value wait_until_ready(future_value&& fv)
+{
+ if (fdb_error_t r = fdb_future_block_until_ready(fv.raw_handle()); 0 != r) {
+ throw libfdb_exception(r);
+ }
+
+ return fv;
+}
+
+template <typename FnT, typename... XS>
+requires std::invocable<FnT, XS...>
+inline future_value await_future_of(FnT&& fn, XS&& ...params)
+{
+ return block_until_ready(
+ std::invoke(std::forward<FnT>(fn), std::forward<XS>(params)...));
+}
+
+template <typename OutIterT>
+requires std::output_iterator<OutIterT, std::pair<std::string, std::string>>
+inline bool get_value_range_from_transaction(transaction& txn, const select& key_range, OutIterT out_iter)
+{
+ auto flattened = detail::generate_FDB_pairs(txn, key_range) | std::views::join;
+ std::ranges::transform(flattened, out_iter, to_decoded_kv_pair<std::string>);
+
+ return true;
+}
+
+inline fdb_error_t value_from_db(future_value& fv,
+ std::invocable<future_value&> auto extract_fn)
+{
+ return std::invoke(extract_fn, fv);
+}
+
+inline bool retry_after_error(ceph::libfdb::transaction_handle& txn, const fdb_error_t r)
+{
+ if (0 == r) {
+ return false;
+ }
+
+ if (not fdb_error_predicate(FDB_ERROR_PREDICATE_RETRYABLE, r)) {
+ // Non-retryable errors cannot be repaired by on_error():
+ throw ceph::libfdb::libfdb_exception(r);
+ }
+
+ future_value on_error_future(fdb_transaction_on_error(txn->raw_handle(), r));
+
+ if (fdb_error_t on_error_r = fdb_future_block_until_ready(on_error_future.raw_handle()); 0 != on_error_r) {
+ throw ceph::libfdb::libfdb_exception(r);
+ }
+
+ if (fdb_error_t on_error_r = fdb_future_get_error(on_error_future.raw_handle()); 0 != on_error_r) {
+ throw ceph::libfdb::libfdb_exception(r);
+ }
+
+ return true;
+}
+
+// Convert FDBKey array into something useful:
+inline std::vector<ceph::libfdb::select> as_select_seq(const FDBKey* const xs, const int n)
+{
+ std::vector<ceph::libfdb::select> out;
+
+ if (1 < n) {
+ out.reserve(static_cast<std::size_t>(n - 1));
+ }
+
+ // Gather the flattened list into *overlapping* libfdb::select pairs:
+ for (const auto& dyad : std::span(xs, n) | std::views::slide(2)) {
+ const auto& fst = std::ranges::begin(dyad)[0];
+ const auto& snd = std::ranges::begin(dyad)[1];
+
+ out.push_back({
+ { (const char *)fst.key, static_cast<std::string::size_type>(fst.key_length) },
+ { (const char *)snd.key, static_cast<std::string::size_type>(snd.key_length) }
+ });
+ }
+
+ return out;
+}
+// Finding a clear example both in the samples and in the documentation is not very easy. The
+// statelessness of FDB requests bleeds into here with basically no hand-holding, so it's fairly subtle and
+// not easy to see what to do (I'm still not sure I have it right), but note for instance that the call
+// parameters have to change for subsequent reads.
+inline future_value get_range_future_from_transaction(ceph::libfdb::transaction& txn, const ceph::libfdb::select& selection, int iteration)
+{
+ const auto& begin_key = selection.begin_key;
+ const auto& end_key = selection.end_key;
+
+ // Hook for getting settings into here through the selector:
+ const auto& streaming_mode = selection.options.streaming_mode;
+
+ // The documentation makes this stuff about as clear as mud... read VERY carefully
+ // when you fiddle with these:
+ int begin_or_eq = (1 == iteration) ? 0 : 1;
+ const int begin_offset = 1;
+ const int end_or_eq = 0;
+ const int end_offset = 1;
+
+ // See validate_and_update_parameters() in fdb_c.cpp (FDB source) if greater clarity is needed on the meaning of some of these, it can be
+ // hard to deduce from the documentation:
+ // Returns an FDBKeyValueArray in the future:
+
+ return future_value(fdb_transaction_get_range(txn.raw_handle(),
+ (const uint8_t *)begin_key.data(), begin_key.size(), // the reference-point key
+ begin_or_eq, // begin or eq
+ begin_offset, // begin offset
+
+ (const uint8_t *)end_key.data(), end_key.size(), // the end selector key
+ end_or_eq, // end or eq
+ end_offset, // end offset (a shift AFTER end is matched)
+
+ // How should results be grouped/chunked:
+ 0, // limit (0 == unlimited)
+ 0, // target bytes (0 == unlimited)
+ streaming_mode, // streaming mode (e.g.: FDB_STREAMING_MODE_WANT_ALL)
+ iteration, // iteration # (produced side effect)
+
+ // Other options:
+ 0, // 0 unless this IS a snapshot read
+ 0 // reverse: should items come in reverse order?
+ ));
+}
+
+inline std::vector<ceph::libfdb::select> locate_split_points(
+ ceph::libfdb::database_handle dbh,
+ ceph::libfdb::select selector,
+ const std::int64_t remote_chunk_size)
+{
+ using ceph::libfdb::detail::wait_until_ready;
+
+ auto txn = ceph::libfdb::make_transaction(dbh);
+
+ // We can do this without side-effects by combining some lambdas, but for now I'm satisfied if it works:
+ FDBKey const *keys = nullptr;
+ int nkeys = 0;
+
+ for (bool should_retry = true; should_retry;) {
+
+ auto fv = wait_until_ready(future_value(fdb_transaction_get_range_split_points(
+ txn->raw_handle(),
+ (const std::uint8_t *)selector.begin_key.data(), static_cast<int>(selector.begin_key.length()),
+ (const std::uint8_t *)selector.end_key.data(), static_cast<int>(selector.end_key.length()),
+ remote_chunk_size)));
+
+ should_retry = retry_after_error(txn, value_from_db(fv,
+ [&keys, &nkeys](future_value& fv) {
+ return fdb_future_get_key_array(fv.raw_handle(), &keys, &nkeys);
+ }));
+
+ if (not should_retry) {
+ return as_select_seq(keys, nkeys);
+ }
+ }
+
+ return {};
+}
+
+// Generators:
+// The returned memory should be copied immediately as its lifetime will end once the Future is destroyed:
+// (This is pretty much why this is in the "detail" namespace for the moment.)
+inline std::generator<std::span<const FDBKeyValue>> generate_FDB_pairs(transaction& txn, select key_range)
+{
+ // As FDB is stateless, we need to track state somewhere:
+ int more_available = 1;
+ int out_count = 0;
+ int iteration = 1; // expected to be 1 when FDB is called in iterator mode
+
+ const FDBKeyValue *out_kvs = nullptr;
+
+ for (; more_available; iteration++) {
+
+ auto fv = await_future_of([&]() { return get_range_future_from_transaction(txn, key_range, iteration); });
+
+ if (fdb_error_t r = fdb_future_get_keyvalue_array(fv.raw_handle(), &out_kvs, &out_count, &more_available); 0 != r) {
+ throw libfdb_exception(r);
+ }
+
+ auto result = std::span<const FDBKeyValue>(out_kvs, out_count);
+
+ if (more_available) {
+ // Make the first part of the range for the new search equal to the last one from the old search:
+ const auto& last_key = result.back();
+ key_range.begin_key = std::string_view((const char *)last_key.key, last_key.key_length);
+ }
+
+ co_yield result;
+ }
+}
+
+} // namespace ceph::libfdb::detail
+
+
+#endif
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025-2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+*/
+
+#ifndef CEPH_FDB_CONVERSION_H
+ #define CEPH_FDB_CONVERSION_H
+
+#include "base.h"
+
+#include "zpp_bits.h"
+
+#include <span>
+#include <string>
+#include <vector>
+#include <cstdint>
+#include <concepts>
+#include <functional>
+#include <string_view>
+#include <type_traits>
+#include <system_error>
+
+/* This module is for converting internal types "owned" by FoundationDB. They've initially been implemented in
+a conversion namespace with overloads, which is the same way that user conversions work, but especially with Concepts
+this technique doesn't have to be used-- it is likely possible to avoid default construction and possible extra copies.
+Since I'm building a prototype right now, it's more important to me on this pass to make things understandable and
+clear, because past experience informs me that it's better to have a clear understanding of what the goals of "to"
+and "from" versions actually are in relationship to user-level types than to have every nanosecond of performance
+be available on day one.
+
+The target of "to" conversions is not a USER type, but rather the FUNCTIONS provided inside of libfdb-- users
+should NOT see the output of these or have to handle them outside of tests or edge-cases (and even then, I doubt it's
+needed, though I won't work hard to stop it).
+
+I'm hoping that later down the line I can sit and spend more time with this-- it would be nice, for example, if we could
+use memory provided by the caller.
+
+Additionally, this mechanism is mostly obviated by forwarding the work to zpp_bits, but keeping it here provides an additional
+hook "ahead" of that library, and indeed eliminates any actual dependency on it-- we use it to smooth out a few areas where
+zpp_bits is designed for serialization ONLY rather than also playing nice with some conversions, especially those where the
+input and output sizes of an array may not match.
+*/
+namespace ceph::libfdb::to {
+
+inline auto convert(const auto& from, std::vector<std::uint8_t>& out_data) -> std::span<const std::uint8_t>
+{
+ out_data.clear();
+
+ zpp::bits::out out(out_data);
+
+ // zpp::bits won't write a size if we start with a fixed size array:
+ // (see dynamic_extent):
+ if constexpr (std::is_array_v<decltype(from)>) {
+ out(std::span(from, std::size(from))).or_throw();
+
+ return out_data;
+ }
+
+ out(from).or_throw();
+
+ return out_data;
+}
+
+inline auto convert(const auto& from) -> std::vector<std::uint8_t>
+{
+ std::vector<std::uint8_t> out_data;
+ convert(from, out_data);
+
+ return out_data;
+}
+
+} // namespace ceph::libfdb::to
+
+/* Map from FDB inputs from FDB TYPE to CONCRETE (i.e. copyable) userland types. Do NOT add
+non-FDB input sources here (or any non-matching user output sources). Do NOT add
+non-owning targets, lest Antevorda be angered!: */
+namespace ceph::libfdb::from {
+
+inline void convert(const std::span<const std::uint8_t>& from, auto& to)
+{
+ zpp::bits::in zpp_in(from);
+ zpp_in(to).or_throw();
+}
+
+template <std::invocable<const char *, size_t> OutputFunction>
+inline void convert(const std::span<const std::uint8_t>& in, OutputFunction& fn)
+{
+ fn((const char *)in.data(), in.size());
+}
+
+} // namespace ceph::libfdb::from
+
+namespace ceph::libfdb::detail {
+
+template <typename ValueT>
+inline std::pair<std::string, ValueT> to_decoded_kv_pair(const FDBKeyValue& kv)
+{
+ std::pair<std::string, ValueT> r;
+
+ r.first.assign((const char *)kv.key, static_cast<std::string::size_type>(kv.key_length));
+
+ try
+ {
+ ceph::libfdb::from::convert(std::span<const std::uint8_t>(kv.value, kv.value_length), r.second);
+ }
+ catch (const std::system_error& e) {
+ // Translate from underlying (e.g. zpp_bits) conversion error into the right type:
+ // This is a bit bound to zpp_bits for the moment, but there's not a more direct way to distinguish this
+ // from a different system_error. We could do that, by using zpp_bits' non-throwing modes and throwing a
+ // special type, but this will do for now.
+ throw ceph::libfdb::libfdb_exception(e.what());
+ }
+
+ return r;
+}
+
+} // namespace ceph::libfdb::detail
+
+#endif
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
+// vim: ts=8 sw=2 smarttab ft=cpp
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025-2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+*/
+
+#ifndef CEPH_RGW_FDB_H
+ #define CEPH_RGW_FDB_H
+
+#include "base.h"
+#include "interface.h"
+#include "conversion.h"
+
+#endif
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- // vim: ts=8 sw=2 smarttab ft=cpp
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025-2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+*/
+
+#ifndef CEPH_FDB_BINDINGS_H
+ #define CEPH_FDB_BINDINGS_H
+
+#include "base.h"
+#include "conversion.h"
+
+#include <span>
+#include <ranges>
+#include <cstdint>
+#include <utility>
+#include <optional>
+#include <concepts>
+#include <iterator>
+#include <generator>
+#include <exception>
+#include <algorithm>
+#include <functional>
+#include <filesystem>
+#include <type_traits>
+
+namespace ceph::libfdb {
+
+/* This needs to be called once and only once during application lifetime,
+when no more FoundationDB calls will be made: */
+inline void shutdown_libfdb()
+{
+ ceph::libfdb::detail::database_system::shutdown_fdb();
+}
+
+// By default, libfdb will in turn use FDB's own defaults (see FDB docs):
+inline database_handle create_database()
+{
+ return std::make_shared<database>();
+}
+
+inline database_handle create_database(const std::filesystem::path dbfile)
+{
+ return std::make_shared<database>(dbfile);
+}
+
+inline database_handle create_database(const std::filesystem::path dbfile,
+ const database_options& dbopts,
+ const network_options& netopts)
+{
+ return std::make_shared<database>(dbfile, dbopts, netopts);
+}
+
+inline database_handle create_database(const database_options& dbopts,
+ const network_options& netopts)
+{
+ return std::make_shared<database>(dbopts, netopts);
+}
+
+inline database_handle create_database(const database_options& opts)
+{
+ return std::make_shared<database>(opts, network_options{});
+}
+
+inline database_handle create_database(const std::filesystem::path dbfile,
+ const database_options& dbopts)
+{
+ return std::make_shared<database>(dbfile, dbopts, network_options{});
+}
+
+inline transaction_handle make_transaction(database_handle dbh)
+{
+ return std::make_shared<transaction>(dbh);
+}
+
+inline transaction_handle make_transaction(database_handle dbh, const transaction_options& opts)
+{
+ return std::make_shared<transaction>(dbh, opts);
+}
+
+// Note: only rarely is a direct call to this needed. You can use transactors or pass database_handles
+// to get automagic.
+// Note: after a transaction is committed, it cannot be used again; but right now, that is NOT
+// an error with respect to the object. So, don't do operations on the object after you've committed
+// it or the behavior could be surprising.
+// On false, the client should retry the transaction:
+[[nodiscard]] inline bool commit(transaction_handle& txn)
+{
+ return txn->commit();
+}
+
+} // namespace ceph::libfdb
+
+namespace ceph::libfdb::detail {
+
+// Forward declarations:
+template <typename FnT>
+using transaction_invocation_result_t =
+ std::invoke_result_t<FnT&, transaction_handle&>;
+
+template <typename FnT>
+concept supported_transaction_invocation =
+ concepts::supported_invocation_result<transaction_invocation_result_t<FnT>>;
+
+template <typename FnT>
+using operation_result_t =
+ std::conditional_t<std::is_void_v<transaction_invocation_result_t<FnT>>,
+ void,
+ std::remove_cvref_t<transaction_invocation_result_t<FnT>>>;
+
+template <typename OutValuesT>
+struct value_collector_t final
+{
+ OutValuesT& out_values;
+
+ void operator()(std::span<const std::uint8_t> out_data) const;
+};
+
+template <typename OutValuesT>
+auto value_collector(OutValuesT& out_values) -> value_collector_t<OutValuesT>;
+
+template <supported_transaction_invocation FnT>
+auto maybe_retry(transaction_handle txn, FnT&& fn) -> operation_result_t<FnT>;
+
+template <supported_transaction_invocation FnT>
+auto maybe_commit(transaction_handle txn, const commit_after_op commit_after, FnT&& fn)
+ -> operation_result_t<FnT>;
+
+template <supported_transaction_invocation FnT>
+auto commit_noreplay(transaction_handle txn, const commit_after_op commit_after, FnT&& fn)
+ -> operation_result_t<FnT>;
+
+} // namespace ceph::libfdb::detail
+
+namespace ceph::libfdb {
+
+inline void set(transaction_handle txn,
+ std::string_view k, const auto& v,
+ const commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [key = detail::as_fdb_span(k), &v](const transaction_handle& txn) {
+ return txn->set(key, ceph::libfdb::to::convert(v));
+ });
+}
+
+// If someone gives us an explicit transaction handle, they almost certainly don't want to commit
+// it (though they can always specify otherwise):
+inline void set(transaction_handle txn, std::string_view k, const auto& v)
+{
+ return set(txn, k, v, commit_after_op::no_commit);
+}
+
+// ...conversely, with a database handle given, we can assume they DO want to auto-commit:
+inline void set(database_handle dbh,
+ std::string_view k, const auto& v)
+{
+ return detail::maybe_commit(make_transaction(dbh), commit_after_op::commit,
+ [key = detail::as_fdb_span(k), &v](const transaction_handle& txn) {
+ return txn->set(key, ceph::libfdb::to::convert(v));
+ });
+}
+
+template <template <typename ...> typename AssocT = flat_map,
+ concepts::key_value_iterator IteratorT>
+inline void set(transaction_handle txn,
+ IteratorT b, IteratorT e,
+ const commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [&b, &e](const transaction_handle& txn) {
+ std::vector<std::uint8_t> fixed_buffer;
+
+ std::ranges::for_each(std::ranges::subrange(b, e),
+ [&txn, &fixed_buffer](const auto& kv) {
+ detail::transaction_set_kv_bytes(txn,
+ detail::as_fdb_span(kv.first),
+ ceph::libfdb::to::convert(kv.second, fixed_buffer));
+ });
+ });
+}
+
+template <template <typename ...> typename AssocT = flat_map,
+ concepts::key_value_iterator IteratorT>
+inline void set(database_handle dbh, IteratorT b, IteratorT e)
+{
+ return detail::maybe_commit(make_transaction(dbh), commit_after_op::commit,
+ [b, e](const transaction_handle& txn) {
+ return set(txn, b, e, commit_after_op::no_commit);
+ });
+}
+
+// Note that we force things into a span so that byte streams get the proper encoding expected by zpp_bits:
+inline void set(transaction_handle txn,
+ std::string_view k, const ceph::libfdb::concepts::stringview_convertible auto& v,
+ const commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [key = detail::as_fdb_span(k), value = std::string_view(v)](const transaction_handle& txn) {
+ return txn->set(key, ceph::libfdb::to::convert(value));
+ });
+}
+
+inline void set(transaction_handle txn,
+ std::string_view k, const ceph::libfdb::concepts::stringview_convertible auto& v)
+{
+ return set(txn, k, v, commit_after_op::no_commit);
+}
+
+inline void set(database_handle dbh,
+ std::string_view k, const ceph::libfdb::concepts::stringview_convertible auto& v)
+{
+ return detail::maybe_commit(make_transaction(dbh), commit_after_op::commit,
+ [key = detail::as_fdb_span(k), value = std::string_view(v)](const transaction_handle& txn) {
+ return txn->set(key, ceph::libfdb::to::convert(value));
+ });
+}
+
+} // namespace ceph::libfdb
+
+namespace ceph::libfdb {
+
+// erase() in libfdb is clear() in FDB parlance:
+inline void erase(ceph::libfdb::transaction_handle txn,
+ const ceph::libfdb::select& key_range,
+ const commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [&key_range](const transaction_handle& txn) {
+ return txn->erase(key_range);
+ });
+}
+
+inline void erase(ceph::libfdb::transaction_handle txn, const ceph::libfdb::select& key_range)
+{
+ return erase(txn, key_range, commit_after_op::no_commit);
+}
+
+inline void erase(ceph::libfdb::database_handle dbh,
+ const ceph::libfdb::select& key_range)
+{
+ return detail::maybe_commit(ceph::libfdb::make_transaction(dbh), commit_after_op::commit,
+ [&key_range](const transaction_handle& txn) {
+ return txn->erase(key_range);
+ });
+}
+
+inline void erase(ceph::libfdb::transaction_handle txn,
+ std::string_view k,
+ const commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [key = detail::as_fdb_span(k)](const transaction_handle& txn) {
+ return txn->erase(key);
+ });
+}
+
+inline void erase(ceph::libfdb::transaction_handle txn, std::string_view k)
+{
+ return erase(txn, k, commit_after_op::no_commit);
+}
+
+inline void erase(ceph::libfdb::database_handle dbh, std::string_view k)
+{
+ return detail::maybe_commit(make_transaction(dbh), commit_after_op::commit,
+ [key = detail::as_fdb_span(k)](const transaction_handle& txn) {
+ return txn->erase(key);
+ });
+}
+
+} // namespace ceph::libfdb
+
+namespace ceph::libfdb {
+
+// get() with selector:
+// JFW: Satisfying output_iterator is not as straightforward as it appears, I need to look at this mechanism again; meanwhile, the template doesn't
+// /prevent/ future type narrowing, but I'm forcing it to std::string for now:
+inline bool get(ceph::libfdb::transaction_handle txn,
+ const ceph::libfdb::select& key_range, auto out_iter,
+ const ceph::libfdb::commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [&key_range, out_iter](const transaction_handle& txn) {
+ return txn->get(key_range, out_iter);
+ });
+}
+
+inline bool get(ceph::libfdb::transaction_handle txn,
+ const ceph::libfdb::select& key_range, auto out_iter)
+{
+ return get(txn, key_range, out_iter, commit_after_op::no_commit);
+}
+
+inline bool get(ceph::libfdb::database_handle dbh,
+ const ceph::libfdb::select& key_range, auto out_iter)
+{
+ return detail::maybe_retry(ceph::libfdb::make_transaction(dbh),
+ [&key_range, out_iter](transaction_handle& txn) {
+ return get(txn, key_range, out_iter, commit_after_op::no_commit);
+ });
+}
+
+template <typename OutputTargetOrFnT>
+requires concepts::value_callback<std::remove_reference_t<OutputTargetOrFnT>> ||
+ concepts::value_output<OutputTargetOrFnT&&>
+inline bool get(ceph::libfdb::transaction_handle txn,
+ std::string_view key,
+ OutputTargetOrFnT&& output_target_or_fn,
+ const commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [key = detail::as_fdb_span(key), &output_target_or_fn](const transaction_handle& txn) {
+ if constexpr (concepts::value_callback<std::remove_reference_t<OutputTargetOrFnT>>) {
+ return txn->get(key, output_target_or_fn);
+ }
+
+ if constexpr (concepts::value_output<OutputTargetOrFnT&&>) {
+ return txn->get(key,
+ detail::value_collector(output_target_or_fn));
+ }
+ });
+}
+
+template <typename OutputTargetOrFnT>
+requires concepts::value_callback<std::remove_reference_t<OutputTargetOrFnT>> ||
+ concepts::value_output<OutputTargetOrFnT&&>
+inline bool get(ceph::libfdb::transaction_handle txn,
+ std::string_view key, OutputTargetOrFnT&& output_target_or_fn)
+{
+ return get(txn, key, std::forward<OutputTargetOrFnT>(output_target_or_fn), commit_after_op::no_commit);
+}
+
+template <typename OutputTargetOrFnT>
+requires concepts::value_callback<std::remove_reference_t<OutputTargetOrFnT>> ||
+ concepts::value_output<OutputTargetOrFnT&&>
+inline bool get(ceph::libfdb::database_handle dbh,
+ std::string_view key, OutputTargetOrFnT&& output_target_or_fn)
+{
+ return detail::maybe_retry(ceph::libfdb::make_transaction(dbh),
+ [key, &output_target_or_fn](transaction_handle& txn) {
+ return get(txn, key, output_target_or_fn, commit_after_op::no_commit);
+ });
+}
+
+} // namespace ceph::libfdb
+
+namespace ceph::libfdb {
+
+// Does a key exist?
+inline bool key_exists(transaction_handle txn,
+ std::string_view k,
+ const commit_after_op commit_after)
+{
+ return detail::commit_noreplay(txn, commit_after,
+ [k](const transaction_handle& txn) {
+ return txn->key_exists(k);
+ });
+}
+
+inline bool key_exists(transaction_handle txn, std::string_view k)
+{
+ return key_exists(txn, k, commit_after_op::no_commit);
+}
+
+inline bool key_exists(database_handle dbh, std::string_view k)
+{
+ return detail::maybe_retry(ceph::libfdb::make_transaction(dbh),
+ [k](transaction_handle& txn) {
+ return key_exists(txn, k, commit_after_op::no_commit);
+ });
+}
+
+} // namespace ceph::libfdb
+
+namespace ceph::libfdb {
+
+/* A "transactor" is a function-like wrapper for running replayable transactions.
+ * It defers transaction creation until called, commits after the user function
+ * returns, retries when FoundationDB requests replay, and throws when recovery
+ * fails or retry attempts are exhausted. Plus, the name is pretty cool. */
+class transactor final
+{
+ database_handle dbh;
+
+ std::optional<transaction_options> opts;
+
+ private:
+ explicit transactor(database_handle dbh_)
+ : dbh(dbh_)
+ {}
+
+ transactor(database_handle dbh_, const transaction_options& opts_)
+ : dbh(dbh_),
+ opts(opts_)
+ {}
+
+ public:
+ template <typename FnT>
+ requires std::invocable<FnT&, transaction_handle&>
+ decltype(auto) operator()(FnT&& fn) const
+ {
+ auto txn = opts ? make_transaction(dbh, *opts)
+ : make_transaction(dbh);
+
+ return detail::maybe_retry(txn, std::forward<FnT>(fn));
+ }
+
+ private:
+ friend inline transactor make_transactor(database_handle dbh);
+ friend inline transactor make_transactor(database_handle dbh, const transaction_options& opts);
+};
+
+inline transactor make_transactor(database_handle dbh)
+{
+ return transactor(dbh);
+}
+
+inline transactor make_transactor(database_handle dbh, const transaction_options& opts)
+{
+ return transactor(dbh, opts);
+}
+
+} // namespace ceph::libfdb
+
+// Basic Generators:
+namespace ceph::libfdb {
+
+template <typename ValueT = std::string>
+inline auto pair_generator(ceph::libfdb::transaction_handle txn, ceph::libfdb::select key_range)
+ -> std::generator<std::pair<std::string, ValueT>>
+{
+ for (const std::span<const FDBKeyValue>& kvp_block : ceph::libfdb::detail::generate_FDB_pairs(*txn, key_range)) {
+ for (const auto& kvp : kvp_block) {
+ co_yield ceph::libfdb::detail::to_decoded_kv_pair<ValueT>(kvp);
+ }
+ }
+}
+
+template <typename ValueT = std::string>
+inline auto pair_generator(const ceph::libfdb::database_handle& dbh, ceph::libfdb::select key_range)
+{
+ return pair_generator<ValueT>(ceph::libfdb::make_transaction(dbh), key_range);
+}
+
+// Note: block_generator() uses split planning to tackle large sets; use pair_generator() for
+// direct scans. This is meant to be straightforward and easy-to-understand-- hence, there's not
+// a recover strategy or other things (you can replay the entire query)-- as new needs arise, this
+// can be made more flexible via selector options, dynamic range-splitting, etc., but so far there
+// has been no need:
+template <typename AssocT = flat_map<std::string, std::string>>
+auto block_generator(ceph::libfdb::database_handle dbh, ceph::libfdb::select selector)
+-> std::generator<AssocT>
+{
+ // JFW: Although this is tunable, in my measurement it isn't a large factor so far-- likely
+ // these can move into the select instance itself (this is hard to measure in tests-- N
+ // has to be pretty big; I've adjusted chunk_size to where I guesstimate it needs to be,
+ // we need real-world experience to tweak this further):
+ const auto chunk_size = 4*(1024*1024);
+
+ auto split_points = detail::locate_split_points(dbh, selector, chunk_size);
+
+ // This can be paralellized, but early benchmarks didn't show it helping:
+ for (const auto& sp : split_points) {
+ AssocT block;
+ for (auto&& p : pair_generator(dbh, sp)) {
+ block.emplace(std::move(p));
+ }
+
+ co_yield block;
+ }
+}
+
+} // namespace ceph::libfdb
+
+namespace ceph::libfdb::detail {
+
+// Helper implementations:
+inline fdb_error_t do_commit(transaction_handle& txn)
+{
+ if (fdb_error_t r = 0; !txn->commit(&r)) {
+ return r;
+ }
+
+ return 0;
+}
+
+template <typename OutValuesT>
+void value_collector_t<OutValuesT>::operator()(std::span<const std::uint8_t> out_data) const
+{
+ ceph::libfdb::from::convert(out_data, out_values);
+}
+
+template <typename OutValuesT>
+auto value_collector(OutValuesT& out_values) -> value_collector_t<OutValuesT>
+{
+ return { out_values };
+}
+
+enum struct invocation_failure_policy { no_retry, retry };
+
+struct no_invocation_result final {};
+
+template <typename ResultT>
+using stored_invocation_result_t =
+ std::conditional_t<std::is_void_v<ResultT>,
+ no_invocation_result,
+ std::remove_cvref_t<ResultT>>;
+
+template <typename ResultT, typename FnT>
+requires concepts::supported_invocation_result<ResultT>
+auto store_invocation_result(transaction_handle& txn, FnT&& fn)
+ -> stored_invocation_result_t<ResultT>
+{
+ if constexpr (std::is_void_v<ResultT>) {
+ return (std::invoke(fn, txn), stored_invocation_result_t<ResultT>{});
+ }
+
+ if constexpr (not std::is_void_v<ResultT>) {
+ return std::invoke(fn, txn);
+ }
+}
+
+template <typename ResultT, typename StoredT>
+requires std::is_void_v<ResultT>
+void invocation_value_from_result(std::optional<StoredT>&&)
+{}
+
+template <typename ResultT, typename StoredT>
+requires concepts::storable_invocation_result<ResultT>
+auto invocation_value_from_result(std::optional<StoredT>&& result)
+{
+ return *std::move(result);
+}
+
+template <invocation_failure_policy FailurePolicy,
+ typename FnT,
+ typename CommitFnT,
+ typename ResultT = std::invoke_result_t<FnT&, transaction_handle&>>
+requires concepts::supported_invocation_result<ResultT>
+auto attempt_invocation(transaction_handle& txn, FnT&& fn, CommitFnT&& commit_fn)
+ -> std::optional<stored_invocation_result_t<ResultT>>
+{
+ using stored_result_t = stored_invocation_result_t<ResultT>;
+
+ std::optional<stored_result_t> result;
+
+ try {
+ result.emplace(store_invocation_result<ResultT>(txn, fn));
+ }
+ catch (const libfdb_exception& e) {
+ // Figure out how to recover from invocation failure:
+
+ if constexpr (invocation_failure_policy::no_retry == FailurePolicy) {
+ throw;
+ }
+
+ if (not e.retryable()) {
+ throw;
+ }
+
+ retry_after_error(txn, e.fdb_error_value);
+ return std::nullopt;
+ }
+
+ if (!std::invoke(commit_fn, txn)) {
+ return std::nullopt;
+ }
+
+ return result;
+}
+
+template <invocation_failure_policy FailurePolicy,
+ typename FnT,
+ typename CommitFnT,
+ typename ResultT = std::invoke_result_t<FnT&, transaction_handle&>>
+requires concepts::supported_invocation_result<ResultT>
+decltype(auto) invoke_with_retry(transaction_handle& txn, FnT&& fn, CommitFnT&& commit_fn)
+{
+ for (auto tries = 10; tries; --tries) {
+ if (auto result = attempt_invocation<FailurePolicy>(txn, fn, commit_fn)) {
+ return invocation_value_from_result<ResultT>(std::move(result));
+ }
+ }
+
+ throw libfdb_exception("transaction retry limit exceeded");
+}
+
+template <supported_transaction_invocation FnT>
+auto maybe_retry(transaction_handle txn, FnT&& fn) -> operation_result_t<FnT>
+{
+ return invoke_with_retry<invocation_failure_policy::retry>(
+ txn, std::forward<FnT>(fn),
+ [](transaction_handle& txn) {
+ return ceph::libfdb::commit(txn);
+ });
+}
+
+// Sadly, a RAII wrapper can't safely capture all of the failure conditions that can occur during an
+// FDB commit without throwing from its dtor; but, we generally work around that by wrapping as much
+// into function objects, simplifying the situation about as much as I can think of.
+// Commit operation, possibly replay if FDB requests it:
+template <supported_transaction_invocation FnT>
+auto maybe_commit(transaction_handle txn, const commit_after_op commit_after, FnT&& fn)
+ -> operation_result_t<FnT>
+{
+ if (commit_after_op::commit != commit_after) {
+ return std::invoke(fn, txn);
+ }
+
+ return invoke_with_retry<invocation_failure_policy::no_retry>(
+ txn, std::forward<FnT>(fn),
+ [](transaction_handle& txn) {
+ return ceph::libfdb::commit(txn);
+ });
+}
+
+// Commit only once; the caller is responsible for transaction replay:
+template <supported_transaction_invocation FnT>
+auto commit_noreplay(transaction_handle txn, const commit_after_op commit_after, FnT&& fn)
+ -> operation_result_t<FnT>
+{
+ using result_t = transaction_invocation_result_t<FnT>;
+
+ auto commit_fn = [](transaction_handle& txn) {
+ if (fdb_error_t r = do_commit(txn); 0 != r) {
+ throw ceph::libfdb::libfdb_exception(r);
+ }
+
+ return true;
+ };
+
+ if (commit_after_op::commit != commit_after) {
+ return std::invoke(fn, txn);
+ }
+
+ if constexpr (std::is_void_v<result_t>) {
+ attempt_invocation<invocation_failure_policy::no_retry>(txn, fn, commit_fn);
+ return;
+ }
+
+ if constexpr (concepts::storable_invocation_result<result_t>) {
+ auto result = attempt_invocation<invocation_failure_policy::no_retry>(
+ txn, fn, commit_fn);
+ return std::remove_cvref_t<result_t>(std::move(*result));
+ }
+}
+
+} // namespace ceph::libfdb::detail
+
+#endif
EXPECT_EQ(0, ::memcmp("12C", bl.c_str(), 3));
}
+TEST(BufferList, size) {
+ buffer::list bl;
+ EXPECT_EQ(bl.size(), bl.length());
+
+ bl.append("12345");
+ EXPECT_EQ(bl.size(), bl.length());
+
+ bl.clear();
+ EXPECT_EQ(bl.size(), bl.length());
+}
+
TEST(BufferHash, all) {
{
bufferlist bl;
+#
+# Helper for adding RGW tests built with Catch2:
+# Note: to define a test that does not use Catch2::main(), use:
+# add_catch2_test_rgw(my_test_name NO_CATCH2_MAIN)
+# ...your test should be in a file called "test_<my_test_name>.cc (ex. "test_foo.cc")
+# and the output will be called "unittest_my_test_name".
+#
+function(add_catch2_test_rgw test_name)
+add_catch2_test(${test_name}
+ ${ARGV1}
+ EXTRA_LIBS rgw_common ${rgw_libs}
+ EXTRA_INCS ${rgw_incs}
+ EXTRA_INCS "SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/src/rgw")
+endfunction()
+
if(WITH_RADOSGW_AMQP_ENDPOINT)
# amqp mock library
set(amqp_mock_src
install(TARGETS ceph_test_datalog DESTINATION ${CMAKE_INSTALL_BINDIR})
endif()
-#
-# Helper for adding RGW tests built with Catch2:
-# Note: to define a test that does not use Catch2::main(), use:
-# add_catch2_test_rgw(my_test_name NO_CATCH2_MAIN)
-# ...your test should be in a file called "test_<my_test_name>.cc (ex. "test_foo.cc")
-# and the output will be called "unittest_my_test_name".
-#
-function(add_catch2_test_rgw test_name)
-add_catch2_test(${test_name}
- ${ARGV1}
- EXTRA_LIBS rgw_common ${rgw_libs}
- EXTRA_INCS "SYSTEM PRIVATE ${CMAKE_SOURCE_DIR}/src/rgw")
-endfunction()
-
-add_catch2_test_rgw(rgw_hex)
-
add_executable(unittest_rgw_async_utils test_rgw_async_utils.cc)
add_ceph_unittest(unittest_rgw_async_utils)
target_link_libraries(unittest_rgw_async_utils ${rgw_libs} ${UNITTEST_LIBS})
add_ceph_unittest(unittest_rgw_http_client)
target_link_libraries(unittest_rgw_http_client
rgw_common ${rgw_libs} ${UNITTEST_LIBS})
+
+if(WITH_RADOSGW_FDB)
+ list(APPEND rgw_incs "${CMAKE_BINARY_DIR}/_deps/zpp_bits-src/")
+ add_catch2_test_rgw(rgw_hex)
+ add_catch2_test_rgw(fdb NO_CATCH2_MAIN)
+ add_catch2_test_rgw(fdb_ceph NO_CATCH2_MAIN)
+endif()
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- // vim: ts=8 sw=2 smarttab ft=cpp
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025-2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+*/
+
+#include <catch2/catch_config.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/catch_template_test_macros.hpp>
+
+#include <catch2/generators/catch_generators.hpp>
+#include <catch2/generators/catch_generators_adapters.hpp>
+
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include "test/rgw/test_fdb_common.h"
+
+#include <fmt/format.h>
+#include <fmt/chrono.h>
+#include <fmt/ranges.h>
+
+#include "include/random.h"
+
+#include <boost/container/flat_map.hpp>
+
+#include <map>
+#include <list>
+#include <chrono>
+#include <vector>
+#include <ranges>
+#include <iterator>
+#include <algorithm>
+#include <unordered_map>
+
+using Catch::Matchers::AllMatch;
+
+using fmt::format;
+using fmt::println;
+
+using std::end;
+using std::begin;
+
+using std::string;
+using std::string_view;
+
+using std::to_string;
+
+using std::vector;
+
+using namespace std::literals;
+
+// Be nice to Catch2's template-test macros:
+using string_pair = std::pair<std::string, std::string>;
+
+// Collect values in selection to out_values:
+auto key_counter(auto txn, const auto& selector, auto& out_values) -> auto {
+ out_values.clear();
+
+ lfdb::get(txn, selector,
+ std::inserter(out_values, std::end(out_values)));
+
+ return out_values.size();
+};
+
+auto key_count(auto& dbh, const auto& selector) {
+ std::map<std::string, std::string> _;
+ return key_counter(lfdb::make_transaction(dbh), selector, _);
+}
+
+inline auto write_monotonic_kvs(lfdb::database_handle dbh, const int N, std::string_view prefix = "key")
+{
+ auto kvs = make_monotonic_kvs(N, prefix);
+
+ for(const auto& [k, v] : kvs)
+ lfdb::set(lfdb::make_transaction(dbh), k, v, lfdb::commit_after_op::commit);
+
+ return kvs;
+}
+
+// Basically, make sure we're actually linking with the library:
+TEST_CASE()
+{
+ REQUIRE_THROWS_AS([] { throw ceph::libfdb::libfdb_exception(0); }(),
+ ceph::libfdb::libfdb_exception);
+}
+
+TEST_CASE("fdb simple", "[rgw][fdb]") {
+ janitor j;
+
+ auto dbh = j.dbh();
+
+ const string_view k = "key";
+ const string v = fmt::format("value-{:%c}", std::chrono::system_clock::now());
+
+ SECTION("read missing key") {
+ const string_view missing_key = "missing_key";
+
+ SECTION("with transaction") {
+ std::string out_value;
+
+ auto txn_handle = lfdb::make_transaction(dbh);
+ REQUIRE(nullptr != txn_handle);
+
+ CAPTURE(missing_key);
+ CAPTURE(out_value);
+ REQUIRE_FALSE(lfdb::get(txn_handle, missing_key, out_value, lfdb::commit_after_op::no_commit));
+ CHECK(v != out_value);
+ }
+ }
+
+ SECTION("CRD single-key") {
+ std::string out_value;
+
+ // The key initially either exists, or we'll write it anew, either is fine:
+ CHECK_NOTHROW(lfdb::set(lfdb::make_transaction(dbh), k, v, lfdb::commit_after_op::commit));
+
+ // Make sure that it DOES exist:
+ CHECK(lfdb::get(lfdb::make_transaction(dbh), k, out_value, lfdb::commit_after_op::no_commit));
+ CHECK(v == out_value);
+
+ // "erase()" is known as "clear" in FDB parlance, deleting a record:
+ REQUIRE_NOTHROW(lfdb::erase(lfdb::make_transaction(dbh), k, lfdb::commit_after_op::commit));
+
+ // ...as this shouldn't be updated again, make sure there isn't an accidental match:
+ out_value.clear();
+
+ // ...and, POOF!-- it should be gone:
+ CHECK_FALSE(lfdb::get(lfdb::make_transaction(dbh), k, out_value, lfdb::commit_after_op::no_commit));
+ CHECK(v != out_value);
+ }
+
+ SECTION("read/write single key") {
+ REQUIRE(nullptr != dbh);
+
+ // First, be sure we have a valid value written to the database:
+ REQUIRE_NOTHROW(lfdb::set(lfdb::make_transaction(dbh), k, v, lfdb::commit_after_op::commit));
+
+ SECTION("read transaction") {
+ std::string out_value;
+
+ CHECK(lfdb::get(lfdb::make_transaction(dbh), k, out_value, lfdb::commit_after_op::no_commit));
+ CHECK(v == out_value);
+ }
+ }
+
+ SECTION("check for existence of key") {
+ REQUIRE(nullptr != dbh);
+
+ // Erase the key if it's already there:
+ lfdb::erase(lfdb::make_transaction(dbh), k, lfdb::commit_after_op::commit);
+
+ // Now, we shouldn't find anything:
+ CHECK_FALSE(lfdb::key_exists(lfdb::make_transaction(dbh), k, lfdb::commit_after_op::no_commit));
+
+ // Write the key:
+ lfdb::set(lfdb::make_transaction(dbh), k, v, lfdb::commit_after_op::commit);
+
+ // ...it should magically be there!
+ CHECK(lfdb::key_exists(lfdb::make_transaction(dbh), k, lfdb::commit_after_op::no_commit));
+
+ // ...and now it should be gone again:
+ lfdb::erase(lfdb::make_transaction(dbh), k, lfdb::commit_after_op::commit);
+ CHECK_FALSE(lfdb::key_exists(lfdb::make_transaction(dbh), k, lfdb::commit_after_op::no_commit));
+ }
+}
+
+TEST_CASE("delete keys in range", "[rgw][fdb]") {
+ janitor dbh;
+
+ // Exactly 20 keys, 0-19:
+ const auto selector = lfdb::select { make_key(0), make_key(20) };
+
+ // Make sure we're "empty":
+ REQUIRE(0 == key_count(dbh, selector));
+
+ // Make sure we have a matching number of keys in our selector range:
+ const auto kvs = write_monotonic_kvs(dbh, 20);
+ REQUIRE(20 == key_count(dbh, selector));
+
+ // Erase a single value:
+ lfdb::erase(dbh, make_key(5));
+ CHECK(19 == key_count(dbh, selector));
+
+ // Erase an edge of the range:
+ lfdb::erase(dbh, lfdb::select { make_key(0), make_key(1) });
+ CHECK(18 == key_count(dbh, selector));
+
+ // ...the other edge:
+ lfdb::erase(dbh, lfdb::select { make_key(19), make_key(20) });
+ CHECK(17 == key_count(dbh, selector));
+
+ // Erase the entire range:
+ lfdb::erase(dbh, selector);
+ CHECK(0 == key_count(dbh, selector));
+}
+
+TEMPLATE_PRODUCT_TEST_CASE("multi-key ops", "[rgw][fdb]",
+(std::vector, std::list), (string_pair))
+{
+ janitor j;
+
+ auto dbh = j.dbh();
+
+ // Write a sequence of keys so we have some data to work with:
+ const auto kvs = write_monotonic_kvs(dbh, 100);
+
+ SECTION("check multiple key write", "[fdb]") {
+ auto txn = lfdb::make_transaction(dbh);
+
+ std::string out_value;
+
+ CHECK((*(kvs.find(make_key(0)))).second == make_value(0));
+ CHECK(lfdb::get(txn, make_key(0), out_value, lfdb::commit_after_op::no_commit));
+ CHECK(make_value(0) == out_value);
+
+ out_value.clear();
+ CHECK((*(kvs.find(make_key(99)))).second == make_value(99));
+ CHECK(lfdb::get(txn, make_key(99), out_value, lfdb::commit_after_op::no_commit));
+ CHECK(make_value(99) == out_value);
+ }
+
+ SECTION("check multiple key selection", "[fdb]") {
+ TestType out_values;
+
+ auto txn = lfdb::make_transaction(dbh);
+
+ lfdb::get(txn, lfdb::select { make_key(0), make_key(100) }, std::back_inserter(out_values), lfdb::commit_after_op::no_commit);
+
+ CHECK(100 == out_values.size());
+
+ // Maybe not the world's most creative test, but the idea is to try getting some random keys:
+ for(auto i = ceph::util::generate_random_number(out_values.size() - 1); i; --i) {
+ CHECK(std::end(out_values) != std::ranges::find(out_values, string_pair { make_key(i), make_value(i) }));
+ }
+ }
+}
+
+TEST_CASE("check selectors", "[fdb][rgw]") {
+ janitor dbh;
+
+ const int nentries = 10;
+
+ const auto select_all = lfdb::select { make_key(0), make_key(nentries) };
+
+ CHECK("" == lfdb::select { "" }.begin_key);
+ CHECK("\xFF" == lfdb::select { "" }.end_key);
+ CHECK("abc" == lfdb::select { "abc" }.begin_key);
+ CHECK("abd" == lfdb::select { "abc" }.end_key);
+ CHECK("abd" == lfdb::select { std::string("abc\xFF", 4) }.end_key);
+
+ // Make sure that there's nothing in our test range:
+ dbh.drop_all();
+ REQUIRE(0 == key_count(dbh, select_all));
+
+ const auto kvs = write_monotonic_kvs(dbh, nentries);
+
+ // Make sure there's exactly as many entries as we added:
+ REQUIRE(nentries == key_count(dbh, select_all));
+
+ std::vector<std::pair<std::string, std::string>> out;
+ lfdb::get(dbh, select_all, std::back_inserter(out));
+
+ // These /are/ the droids you're looking for:
+ CHECK(nentries == out.size());
+ CHECK(make_key(0) == out.front().first);
+ CHECK(make_key(nentries - 1) == out.back().first);
+
+ lfdb::set(dbh, "keyx", "outside");
+ out.clear();
+ lfdb::get(dbh, lfdb::select { "key_" }, std::back_inserter(out));
+ CHECK(nentries == out.size());
+
+ // Get exactly no entries:
+ out.clear();
+ lfdb::get(dbh, lfdb::select { make_key(0), make_key(0) }, std::back_inserter(out));
+ CHECK(0 == out.size());
+
+ // Get exactly one entry:
+ out.clear();
+ lfdb::get(dbh, lfdb::select { make_key(1), make_key(2) }, std::back_inserter(out));
+ REQUIRE(1 == out.size());
+ CHECK(make_key(1) == out.front().first);
+}
+
+TEST_CASE("fdb conversions (built-in)", "[fdb][rgw]") {
+ // Manual tests of conversions to and from supported FDB built-in types.
+
+ SECTION("spanlike") {
+ // span<uint8_t> -> vector<uint8_t> -> vector<uint8_t>
+ const std::span<const std::uint8_t> n((const std::uint8_t *)msg, sizeof(msg));
+
+ std::vector<std::uint8_t> x;
+ x = ceph::libfdb::to::convert(n);
+
+ std::vector<std::uint8_t> o;
+ ceph::libfdb::from::convert(x, o);
+
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+ }
+
+ SECTION("NULL-as-data") {
+ // with NULL data-- const char* -> vector<uint8_t> -> vector<uint8_t>
+ const std::span<const std::uint8_t> n((const std::uint8_t *)msg_with_null, sizeof(msg_with_null));
+
+ std::vector<std::uint8_t> x;
+ x = ceph::libfdb::to::convert(n);
+
+ std::vector<std::uint8_t> o;
+ ceph::libfdb::from::convert(x, o);
+
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+ REQUIRE_THAT(msg_with_null, Catch::Matchers::RangeEquals(o));
+ }
+}
+
+TEST_CASE("fdb conversions (round-trip)", "[fdb][rgw]") {
+ janitor j;
+
+ auto dbh = j.dbh();
+
+ // string_view -> string
+ {
+ const std::string_view n = "Hello, World!";
+ std::string o;
+
+ lfdb::set(lfdb::make_transaction(dbh), "key", n, lfdb::commit_after_op::commit);
+ lfdb::get(lfdb::make_transaction(dbh), "key", o, lfdb::commit_after_op::no_commit);
+
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+ }
+
+ // vector<uint8_t> -> vector<uint8_t>
+ {
+ const std::vector<uint8_t> n = { 1, 2, 3, 4, 5 };
+ std::vector<uint8_t> o;
+
+ lfdb::set(lfdb::make_transaction(dbh), "key", n, lfdb::commit_after_op::commit);
+ lfdb::get(lfdb::make_transaction(dbh), "key", o, lfdb::commit_after_op::no_commit);
+
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+ }
+}
+
+TEST_CASE("fdb conversions (functions)", "[fdb][rgw]")
+{
+ SECTION("convert with a lambda function")
+ {
+ std::string_view n { pearl_msg };
+ std::string o;
+
+ std::vector<std::uint8_t> x = ceph::libfdb::to::convert(n);
+
+ auto fn = [&o](const char *data, std::size_t sz) -> void {
+ // Because we did /conversion/ on the inbound data, we're still obliged to
+ // reverse this (or else we'll see whatever artefacts the conversion produced)--
+ // the complication is a consequence of dealing with the underlying buffer directly:
+ std::span<const std::uint8_t> in_span((const std::uint8_t *)data, sz);
+
+ ceph::libfdb::from::convert(in_span, o);
+ };
+
+ ceph::libfdb::from::convert(x, fn);
+
+ CAPTURE(n);
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+ }
+
+ SECTION("get with a raw value callback")
+ {
+ janitor j;
+ auto dbh = j.dbh();
+
+ std::string_view n { pearl_msg };
+ std::string o;
+
+ lfdb::set(dbh, "key", n);
+
+ REQUIRE(lfdb::get(dbh, "key", [&o](std::span<const std::uint8_t> in) {
+ ceph::libfdb::from::convert(in, o);
+ }));
+
+ CAPTURE(n);
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+ }
+}
+
+TEST_CASE("basic generators", "[fdb]") {
+ janitor j;
+
+ const unsigned nkeys = GENERATE(0, 1, 2, 3, 10, 100, 1'000);
+
+ auto dbh = j.dbh();
+
+ const auto kvs_in = write_monotonic_kvs(dbh, nkeys);
+ REQUIRE(nkeys == kvs_in.size());
+
+ SECTION("pair_generator, kv pair return") {
+ std::map<std::string, std::string> out;
+
+ // pair_generator returns key-value pairs, keeping the specified transaction (or implicitly created one)
+ // alive until exhausted (note that this may cause the transaction to expire if approaching 5s or so):
+ for(auto&& kvp : lfdb::pair_generator(dbh, lfdb::select { make_key(0), make_key(nkeys) }))
+ out.emplace(kvp);
+
+ REQUIRE(nkeys == out.size());
+
+ // Be sure we captured the head and the tail:
+ if(0 < nkeys) {
+ CHECK(out.contains(make_key(0)));
+ CHECK(out.contains(make_key(nkeys - 1)));
+ }
+ }
+
+ SECTION("pair_generator owns its coroutine transaction") {
+ std::map<std::string, std::string> out;
+ const auto selector = lfdb::select { make_key(0), make_key(nkeys) };
+
+ // The transaction handle local to this lambda must remain alive inside the
+ // generator's coroutine frame after the lambda returns:
+ auto gen = [&dbh, selector] {
+ auto txn = lfdb::make_transaction(dbh);
+ return lfdb::pair_generator(txn, selector);
+ }();
+
+ std::ranges::copy(gen, std::inserter(out, std::end(out)));
+
+ REQUIRE(nkeys == out.size());
+
+ if(0 < nkeys) {
+ CHECK(out.contains(make_key(0)));
+ CHECK(out.contains(make_key(nkeys - 1)));
+ }
+ }
+}
+
+TEMPLATE_PRODUCT_TEST_CASE("associative data", "[fdb][rgw]",
+(std::map, std::unordered_map, boost::container::flat_map), ((std::string, std::string)))
+{
+ janitor j;
+
+ auto dbh = j.dbh();
+
+ TestType kvs{
+ { "hello", "world" },
+ { "lorem", "ipsum" },
+ { "perl", "camel" },
+ { "pearl", pearl_msg }
+ };
+
+ // From the "database" point of view, the structure is now that we have a single
+ // key pointing (p) to an associative array, e.g. map<p, map<k, v>>:
+ lfdb::set(lfdb::make_transaction(dbh), "key", kvs, lfdb::commit_after_op::commit);
+
+ TestType out_kvs;
+
+ lfdb::get(lfdb::make_transaction(dbh), "key", out_kvs, lfdb::commit_after_op::no_commit);
+
+ CHECK(pearl_msg == out_kvs["pearl"]);
+}
+
+SCENARIO("implicit transactions", "[fdb][rgw]")
+{
+ janitor j;
+
+ auto dbh = j.dbh();
+
+ std::string_view k = "hi", v = "there";
+
+ CAPTURE(k);
+ CAPTURE(v);
+
+ SECTION("implicitly create and complete transactions") {
+
+ REQUIRE_FALSE(lfdb::key_exists(dbh, k));
+ CHECK_NOTHROW(lfdb::set(dbh, k, v));
+ CHECK(lfdb::key_exists(dbh, k));
+
+ std::string ov;
+ CHECK(lfdb::get(dbh, k, ov));
+
+ CAPTURE(ov);
+
+ REQUIRE(v == ov);
+
+ CHECK_NOTHROW(lfdb::erase(dbh, k));
+ REQUIRE_FALSE(lfdb::key_exists(dbh, k));
+
+ REQUIRE_FALSE(lfdb::get(dbh, k, ov));
+ }
+
+ SECTION("implicitly create and complete transactions-- selection operations") {
+ // With an implicit transaction, mutating transactions should commit by default:
+ const auto selector = lfdb::select { make_key(0), make_key(20) };
+
+ const auto kvs = write_monotonic_kvs(dbh, 20);
+
+ lfdb::erase(dbh, lfdb::select { make_key(1), make_key(6) });
+
+ CHECK(15 == key_count(dbh, selector));
+
+ // Let's look around the edge cases of the selection:
+ CHECK_FALSE(lfdb::key_exists(dbh, make_key(1)));
+ CHECK_FALSE(lfdb::key_exists(dbh, make_key(5)));
+
+ CHECK(lfdb::key_exists(dbh, make_key(0)));
+ CHECK(lfdb::key_exists(dbh, make_key(6)));
+ }
+
+ SECTION("test behavior with shared transaction") {
+ SECTION("write in uncommitted transaction") {
+ using lfdb::commit_after_op;
+
+ auto txn = lfdb::make_transaction(dbh);
+
+ lfdb::set(txn, "Herman", "Hollerith", commit_after_op::no_commit);
+
+ // Key exists with respect to this transaction:
+ CHECK(lfdb::key_exists(txn, "Herman"));
+
+ lfdb::set(txn, "John", "Backus", commit_after_op::no_commit);
+
+ // Key exists with respect to this transaction:
+ CHECK(lfdb::key_exists(txn, "John", commit_after_op::no_commit));
+
+ // transaction is abandoned
+ }
+
+ // These were only set in the abandoned transaction:
+ CHECK_FALSE(lfdb::key_exists(dbh, "Herman"));
+ CHECK_FALSE(lfdb::key_exists(dbh, "John"));
+ }
+
+ SECTION("round trip") {
+ janitor j(dbh);
+
+ using namespace ceph::libfdb;
+
+ set(dbh, "key_0000", "value");
+ std::string out;
+ get(dbh, "key_0000", out);
+
+ CHECK("value" == out);
+ }
+
+ SECTION("round trip with raw string") {
+ // The underlying serializer can produce some surprising behavior; libfdb
+ // works around this so that the "right" thing to do is what gets done, with
+ // performance-maximzation left as an available, but explicit operation.
+
+ janitor j(dbh);
+
+ using namespace ceph::libfdb;
+
+ // Notice the raw literal going in here:
+ set(dbh, "key_0000", "value");
+
+ std::string out;
+ CHECK_NOTHROW(get(dbh, "key_0000", out));
+
+ CHECK(std::string_view("value") == std::string_view(out));
+
+ // Explicit raw buffers:
+ char out_buffer[9] = {};
+ CHECK_NOTHROW(get(dbh, "key_0000", out_buffer));
+
+ CHECK(std::string_view("value") == std::string_view(out));
+ }
+}
+
+SCENARIO("transactor", "[fdb]")
+{
+ janitor j;
+
+ auto dbh = j.dbh();
+
+ SECTION("transaction function returns nothing") {
+ auto txr = lfdb::make_transactor(dbh);
+
+ txr([](auto txn) {
+ lfdb::set(txn, "key", "value");
+ });
+
+ std::string out;
+ CHECK(lfdb::get(dbh, "key", out));
+ CHECK("value" == out);
+ }
+
+ SECTION("transaction function returns value") {
+ auto txr = lfdb::make_transactor(dbh);
+
+ auto [found, out] = txr([](auto txn) {
+ lfdb::set(txn, "key", "value");
+
+ std::string out;
+ auto found = lfdb::get(txn, "key", out);
+
+ return std::pair(found, out);
+ });
+
+ CHECK(found);
+ CHECK("value" == out);
+ }
+
+ SECTION("construct with transaction options") {
+ lfdb::transaction_options opts {
+ { FDB_TR_OPTION_READ_YOUR_WRITES_DISABLE, lfdb::option_flag }
+ };
+
+ auto txr = lfdb::make_transactor(dbh, opts);
+
+ txr([](auto txn) {
+ lfdb::set(txn, "key", "value");
+ });
+
+ std::string out;
+ CHECK(lfdb::get(dbh, "key", out));
+ CHECK("value" == out);
+ }
+
+ SECTION("transactor replays after conflict") {
+ auto txr = lfdb::make_transactor(dbh);
+
+ lfdb::set(dbh, "key", "initial");
+
+ txr([dbh](auto txn) {
+ std::string out;
+ if(not lfdb::get(txn, "key", out)) {
+ throw std::runtime_error("expected key does not exist");
+ }
+
+ // Force a conflict, making the transactor replay the body:
+ if("initial" == out) {
+ lfdb::set(dbh, "key", "conflict");
+ }
+
+ lfdb::set(txn, "key", "final");
+ });
+
+ std::string out;
+ CHECK(lfdb::get(dbh, "key", out));
+ CHECK("final" == out);
+ }
+
+ SECTION("transactor propagates transaction body exceptions") {
+ auto txr = lfdb::make_transactor(dbh);
+
+ CHECK_THROWS_WITH(txr([](auto) {
+ throw std::runtime_error("transaction body failed");
+ }), "transaction body failed");
+ }
+}
+
+SCENARIO("options", "[fdb]")
+{
+ // For information about options, consult the FoundationDB's source tree's
+ // documentation: fdbclient/vexillographer/fdb.options
+ SECTION("option types") {
+
+ // check that the types supported for FDB options are supported by
+ // the library:
+ lfdb::option_value ov;
+ ov = lfdb::option_flag; // flag
+ ov = 42; // integer
+ ov = std::string("hi"); // string
+ ov = std::vector<std::uint8_t>( // data
+ (const std::uint8_t *)pearl_msg,
+ (const std::uint8_t *)(pearl_msg + sizeof(pearl_msg)));
+ }
+
+ auto dbh0 = lfdb::create_database(
+ { { FDB_DB_OPTION_LOCATION_CACHE_SIZE, 200'000 } },
+ { { FDB_NET_OPTION_TRACE_ENABLE, lfdb::option_flag } });
+
+ auto dbh1 = lfdb::create_database("fishing for databass!", // name
+ { { FDB_DB_OPTION_LOCATION_CACHE_SIZE, 200'000 } }, // database options
+ { { FDB_NET_OPTION_TRACE_ENABLE, lfdb::option_flag } }); // network options
+
+ auto txn = lfdb::make_transaction(dbh0,
+ { { FDB_TR_OPTION_READ_YOUR_WRITES_DISABLE, lfdb::option_flag } });
+
+ SECTION("create_database()") {
+ lfdb::create_database();
+ lfdb::create_database("");
+ lfdb::create_database("", {}, {});
+ lfdb::create_database(lfdb::database_options {}, lfdb::network_options {});
+ }
+
+ SECTION("piecemeal construction") {
+ lfdb::network_options netopts;
+
+ // Note that, according to FDB's documentation, this setting's actually deprecated:
+ netopts[FDB_NET_OPTION_LOCAL_ADDRESS] = "127.0.0.1:2323";
+
+ // The cluster file is in "/etc/foundationdb.fdb.cluster" normally, but we'll point to
+ // nowhere just for fun. The cluster file is the "approved" way to establish a list of
+ // addressess, AFAIK, rather than setting the option:
+ lfdb::create_database("/dev/null", {}, netopts);
+ }
+}
+
+TEST_CASE("mini-demo", "[fdb]") {
+ janitor j;
+
+ using std::map;
+ using std::string;
+
+ map<string, string> bucket_entries = {
+ { "objName", "obj" },
+ { "bucketName", "bucket" },
+ { "creationTime", "2025-11-12T10:00:00" },
+ { "dirty", "0" },
+ { "hosts", "192.168.1.1:8000_192.168.1.2:8000" },
+ { "etag", "abc123def" },
+ { "objSize", "1048576" },
+ { "userId", "user123" },
+ { "displayName", "John Doe" }
+ };
+
+ // In a "real" program, it would be "auto dbh = lfdb::database_handle(...)":
+ auto dbh = j.dbh();
+
+ // This write will make and commit its own transaction:
+ lfdb::set(dbh, "bucket_obj", bucket_entries);
+
+ map<string, string> out;
+ lfdb::get(dbh, "bucket_obj", out);
+
+ // For "demo" purposes, you can ignore everything below here:
+ CAPTURE(out["userId"]);
+ REQUIRE(bucket_entries == out);
+
+ j.drop_all();
+}
+
+// Adapted from Catch2 documentation:
+#include <catch2/catch_session.hpp>
+
+int main(int argc, char **argv)
+{
+ int result = Catch::Session().run(argc, argv);
+
+ // Make sure that FoundationDB is shut down once and only once:
+ ceph::libfdb::shutdown_libfdb();
+
+ return result;
+}
--- /dev/null
+// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- // vim: ts=8 sw=2 smarttab ft=cpp
+
+/*
+ * Ceph - scalable distributed file system
+ *
+ * Copyright (C) 2025-2026 International Business Machines Corp. (IBM)
+ *
+ * This is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License version 2.1, as published by the Free Software
+ * Foundation. See file COPYING.
+ *
+*/
+
+#include <catch2/catch_config.hpp>
+
+#include <catch2/catch_test_macros.hpp>
+#include <catch2/catch_template_test_macros.hpp>
+
+#include <catch2/generators/catch_generators.hpp>
+#include <catch2/generators/catch_generators_adapters.hpp>
+
+#include <catch2/matchers/catch_matchers_all.hpp>
+
+#include <catch2/benchmark/catch_benchmark.hpp>
+
+#include "test/rgw/test_fdb_common.h"
+#include "rgw/ceph_fdb.h"
+
+#include <fmt/format.h>
+#include <fmt/chrono.h>
+#include <fmt/ranges.h>
+
+#include "include/random.h"
+
+#include <boost/container/flat_map.hpp>
+
+#include <chrono>
+#include <vector>
+#include <ranges>
+#include <algorithm>
+#include <execution>
+
+using Catch::Matchers::AllMatch;
+
+using fmt::format;
+using fmt::println;
+
+using std::end;
+using std::begin;
+
+using std::string;
+using std::string_view;
+
+using std::to_string;
+
+using std::vector;
+
+using namespace std::literals::string_literals;
+
+TEST_CASE("test the tester") {
+ // Quis custodiet ipsos custodes?
+
+ SECTION("check write and cleanup") {
+ janitor j;
+
+ j.drop_all_keys();
+
+ // Check that there are no keys:
+ std::string s;
+ lfdb::get(j, make_key(2, "VEAL"), s);
+ REQUIRE(s.empty());
+
+ populate_monotonic(j, 5, "VEAL");
+
+ // Check that we have some keys!
+ lfdb::get(j, make_key(2, "VEAL"), s);
+ REQUIRE(make_value(2) == s);
+
+ j.drop_all_keys();
+ }
+}
+
+TEST_CASE("don't use non-owning container types for output", "[example]") {
+ SKIP("A cautionary tale! Never use non-owning container types as output.");
+
+ // TL;DR: never use non-owning container types as output!
+ //
+ // It's possible to write an innocent-looking but actually
+ // bad conversion (we may be able to fix this through some
+ // resdesign, but it's how it is for now, and not user-facing). Anyway
+ // DON'T DO THIS! :-) Non-owning conversions are bad.
+ // ...since it won't throw consistently, I'll leave it as a cautionary
+ // tale:
+ const char *msg = "Greetings!";
+
+ ceph::buffer::list n;
+ n.append(msg);
+
+ // This looks innocuous... however...
+ std::span<const std::uint8_t> x;
+
+ // BAD: span<> is non-owning!
+ x = ceph::libfdb::to::convert(n);
+
+ ceph::buffer::list o;
+
+ // "x" potentially points at nothing; may throw or explode or work...:
+ ceph::libfdb::from::convert(x, o);
+}
+
+TEST_CASE("fdb conversions (ceph)", "[fdb][rgw]") {
+
+ const char *msg = "Hello, World!";
+
+ auto dbh = lfdb::create_database();
+
+ // ceph::buffer::list -> span<uint8_t> -> std::string
+ {
+ ceph::buffer::list n;
+ n.append(msg);
+
+ std::vector<std::uint8_t> x;
+ x = ceph::libfdb::to::convert(n);
+
+ std::string o;
+ ceph::libfdb::from::convert(x, o);
+
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+ }
+
+ // buffer::list -> span<uint8_t> -> buffer::list
+ {
+ ceph::buffer::list n;
+ n.append(msg);
+
+ std::vector<std::uint8_t> x;
+ x = ceph::libfdb::to::convert(n);
+
+ ceph::buffer::list o;
+ ceph::libfdb::from::convert(x, o);
+
+ lfdb::set(lfdb::make_transaction(dbh), "key", n, lfdb::commit_after_op::commit);
+ lfdb::get(lfdb::make_transaction(dbh), "key", o);
+
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+
+ lfdb::erase(dbh, "key");
+ }
+
+ SECTION("buffer::list direct and const conversion paths match")
+ {
+ ceph::buffer::list n;
+ n.append("direct conversion should match the normal const path");
+
+ auto direct = ceph::libfdb::to::convert(n);
+
+ const auto& cn = n;
+ auto via_serialize = ceph::libfdb::to::convert(cn);
+
+ CHECK(direct == via_serialize);
+ }
+
+ SECTION("empty buffer::list direct and const conversion paths match")
+ {
+ ceph::buffer::list n;
+
+ auto direct = ceph::libfdb::to::convert(n);
+
+ const auto& cn = n;
+ auto via_serialize = ceph::libfdb::to::convert(cn);
+
+ CHECK(direct == via_serialize);
+ }
+
+ SECTION("buffer::list with embedded nulls uses the same conversion path")
+ {
+ constexpr char data[] = { '\0', 'H', 'i', '\0', '!', '\0' };
+
+ ceph::buffer::list n;
+ n.append(data, sizeof(data));
+
+ auto direct = ceph::libfdb::to::convert(n);
+
+ const auto& cn = n;
+ auto via_serialize = ceph::libfdb::to::convert(cn);
+
+ CHECK(direct == via_serialize);
+ }
+
+ SECTION("buffer::list key span preserves logical key bytes")
+ {
+ ceph::buffer::list key;
+ key.append("buffer-list-key");
+
+ auto key_span = ceph::libfdb::detail::as_fdb_span(key);
+
+ CHECK(key.length() == key_span.size());
+ CHECK(std::ranges::equal(std::span((const char *)key_span.data(), key_span.size()),
+ std::string_view("buffer-list-key")));
+ }
+
+ SECTION("empty buffer::list key span is empty")
+ {
+ ceph::buffer::list key;
+
+ auto key_span = ceph::libfdb::detail::as_fdb_span(key);
+
+ CHECK(0 == key_span.size());
+ }
+
+ SECTION("buffer::list key span preserves embedded nulls")
+ {
+ constexpr char data[] = { '\0', 'k', 'e', 'y', '\0' };
+
+ ceph::buffer::list key;
+ key.append(data, sizeof(data));
+
+ auto key_span = ceph::libfdb::detail::as_fdb_span(key);
+
+ CHECK(key.length() == key_span.size());
+ CHECK(std::ranges::equal(std::span((const char *)key_span.data(), key_span.size()),
+ std::string_view(data, sizeof(data))));
+ }
+
+ SECTION("empty buffer::list round trip through public set/get")
+ {
+ ceph::buffer::list in;
+
+ lfdb::set(dbh, "empty-buffer-list", in);
+
+ ceph::buffer::list out;
+ REQUIRE(lfdb::get(dbh, "empty-buffer-list", out));
+ CHECK(0 == out.length());
+
+ lfdb::erase(dbh, "empty-buffer-list");
+ }
+
+ SECTION("buffer::list with embedded nulls round trips through public set/get")
+ {
+ constexpr char data[] = { '\0', 'A', '\0', 'B', '\0' };
+
+ ceph::buffer::list in;
+ in.append(data, sizeof(data));
+
+ lfdb::set(dbh, "embedded-null-buffer-list", in);
+
+ ceph::buffer::list out;
+ REQUIRE(lfdb::get(dbh, "embedded-null-buffer-list", out));
+ REQUIRE_THAT(in, Catch::Matchers::RangeEquals(out));
+
+ lfdb::erase(dbh, "embedded-null-buffer-list");
+ }
+
+ SECTION("buffer::list get replaces existing output")
+ {
+ ceph::buffer::list in;
+ in.append("new-value");
+
+ ceph::buffer::list out;
+ out.append("old-value");
+
+ lfdb::set(dbh, "replace-buffer-list-output", in);
+
+ REQUIRE(lfdb::get(dbh, "replace-buffer-list-output", out));
+ REQUIRE_THAT(in, Catch::Matchers::RangeEquals(out));
+
+ lfdb::erase(dbh, "replace-buffer-list-output");
+ }
+
+ SECTION("buffer::list (and buffer::list key) -> buffer::list")
+ {
+ ceph::buffer::list n;
+ n.append("Hello, World!");
+
+ ceph::buffer::list o;
+ o.append(n);
+
+ lfdb::set(lfdb::make_transaction(dbh), "key", n, lfdb::commit_after_op::commit);
+ lfdb::get(lfdb::make_transaction(dbh), "key", o);
+
+ REQUIRE_THAT(n, Catch::Matchers::RangeEquals(o));
+
+ lfdb::erase(dbh, "key");
+ }
+
+ SECTION("buffer::ptr")
+ {
+ string_view in("Hello, World!");
+ ceph::buffer::ptr p(ceph::buffer::claim_char(in.length(), const_cast<char *>(in.data())));
+ CHECK(in == string_view(p.c_str(), p.length()));
+
+ lfdb::set(dbh, "key", p);
+
+ std::string o;
+ lfdb::get(dbh, "key", o);
+
+ CHECK(in == o);
+ }
+}
+
+std::iostream& operator<<(ceph::libfdb::select& obj, std::iostream& os)
+{
+ os << obj.begin_key;
+ os << obj.end_key;
+ return os;
+}
+namespace ceph::libfdb {
+
+/* Not very coroutine-friendly, sadly; but will be interesting to benchmark, so I'm leaving
+ * it be for now:
+template <typename AssocT = boost::container::flat_map<std::string, std::string>>
+auto tier_generator(ceph::libfdb::database_handle dbh, ceph::libfdb::select selector)
+-> std::generator<AssocT>
+{
+ auto split_points = locate_split_points(dbh, selector);
+
+ const unsigned local_max_block = 2*2024; // vis-a-vis remote request max
+
+ std::transform_reduce(std::begin(split_points), std::end(split_points),
+ AssocT(),
+
+ [](auto&& lhs, auto&& rhs) {
+ return lhs.merge(rhs), lhs;
+ },
+
+ [dbh](const ceph::libfdb::select& selector) mutable {
+ AssocT out;
+
+ // ...my libstdc++ lacks std::from_range overloads; the Hard Way(TM) it is:
+ for(auto&& kvp : ceph::libfdb::pair_generator(dbh, selector)) {
+ out.emplace(kvp);
+ }
+
+ return out;
+ });
+}
+*/
+
+} // namespace ceph::libfdb
+
+
+TEST_CASE("generators", "[fdb][rgw]") {
+
+/* This is a generator for large queries that are pretty much expected to
+exceed the FoundationDB five-second transaction limit: */
+ SECTION("block generator") {
+ janitor j;
+
+ const size_t nkeys = GENERATE(0, 1, 1000); // , 100'000, 1'000'000);
+
+ populate_monotonic(j, nkeys);
+
+ SECTION("retrieve all") {
+ auto selector = lfdb::select { "key_" };
+
+ size_t total = 0;
+ for(const auto& block : lfdb::block_generator(j, selector)) {
+ total += block.size();
+ }
+
+ CHECK(nkeys == total);
+ }
+ }
+}
+
+TEST_CASE("generators", "[benchmark]") {
+ const size_t nkeys = GENERATE(0, 1, 1'000); // 5'000, 10'000, 50'000, 250'000, 1'000'000);
+
+ fmt::println("generator benchmark, nkeys = {}", nkeys);
+
+ BENCHMARK_ADVANCED("block generator read all")(Catch::Benchmark::Chronometer meter) {
+ janitor j;
+
+ populate_monotonic(j, nkeys);
+
+ auto selector = lfdb::select { "key_" };
+
+ size_t total = 0;
+ meter.measure([&j, &selector, &total] {
+ total = 0;
+ for(const auto& block : lfdb::block_generator(j, selector)) {
+ total += block.size();
+ }
+ });
+
+ REQUIRE(nkeys == total);
+ };
+}
+
+// Note that these are disabled for regular test runs. Use
+// unittest_fdb_ceph "simple benchmarks"
+// ...to run:
+TEST_CASE("simple benchmarks", "[benchmark]") {
+
+using namespace std::ranges;
+using std::for_each;
+
+// Remember that these will create exponential growth; you might
+// not want to run ALL combinations if you're short on time:
+auto N = GENERATE(0, 1, 256, 4'096); // 250'000, 1'000'000);
+
+unsigned stride = 512;
+
+fmt::println("N = {}", N);
+fmt::println("stride = {}", stride);
+
+// For casual use, these simply get intolerably slow at higher values:
+const bool run_slow_baselines = (N <= 256);
+
+std::vector<std::pair<std::string, std::string>> inputs;
+inputs.reserve(N);
+
+// Sadly, my version of libstdc++ lacks std::from_range_t:
+for(auto n : views::iota(0, N)) {
+ inputs.emplace_back(std::make_pair<std::string, std::string>(make_key(n), make_value(n)));
+}
+
+if(run_slow_baselines) {
+ BENCHMARK_ADVANCED("write simple records-- implicit transaction per key")(Catch::Benchmark::Chronometer meter) {
+ janitor j;
+
+ meter.measure([&j, &inputs, stride] {
+ // Notice that here we are passing around the database handle, winding up making a new one per-operation:
+ for(auto block : inputs | views::chunk(stride)) {
+ std::for_each(std::begin(block), std::end(block), [&j](const auto& kv) mutable {
+ lfdb::set(j, kv.first, kv.second);
+ });
+ }
+ });
+ };
+}
+
+BENCHMARK_ADVANCED("write simple records-- one transaction per block")(Catch::Benchmark::Chronometer meter) {
+ janitor j;
+
+ meter.measure([&j, &inputs, stride] {
+ // Shared-transaction:
+ for(auto block : inputs | views::chunk(stride)) {
+ auto txn = lfdb::make_transaction(j);
+ std::for_each(std::begin(block), std::end(block), [&txn](const auto& kv) mutable {
+ lfdb::set(txn, kv.first, kv.second);
+ });
+ if(false == lfdb::commit(txn)) {
+ throw std::runtime_error("unable to commit transaction");
+ }
+ }
+ });
+};
+
+BENCHMARK_ADVANCED("write simple records-- range set, one transaction per block")(Catch::Benchmark::Chronometer meter) {
+ janitor j;
+
+ meter.measure([&j, &inputs, stride] {
+ for(auto block : inputs | views::chunk(stride)) {
+ lfdb::set(j, std::begin(block), std::end(block));
+ }
+ });
+};
+
+// On a local FDB setup, block parallelism has not outperformed serial block
+// writes, but it is included as a safe comparison point; remote FDB not yet considered:
+BENCHMARK_ADVANCED("write simple records-- parallel, one transaction per block")(Catch::Benchmark::Chronometer meter) {
+ janitor j;
+
+ meter.measure([&j, &inputs, stride] {
+ auto blocks = inputs | views::chunk(stride);
+
+ std::for_each(std::execution::par, std::begin(blocks), std::end(blocks),
+ [&j](const auto& block) mutable {
+ auto txn = lfdb::make_transaction(j);
+
+ std::ranges::for_each(block, [&txn](const auto& kv) mutable {
+ lfdb::set(txn, kv.first, kv.second);
+ });
+
+ if(false == lfdb::commit(txn)) {
+ throw std::runtime_error("unable to commit transaction");
+ }
+ });
+ });
+};
+
+}
+
+#include <catch2/catch_session.hpp>
+
+int main(int argc, char **argv)
+{
+ int result = Catch::Session().run(argc, argv);
+
+ // Make sure that FoundationDB is shut down once and only once:
+ ceph::libfdb::shutdown_libfdb();
+
+ return result;
+}
--- /dev/null
+#ifndef TEST_FDB_COMMON_H
+ #define TEST_FDB_COMMON_H
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <fmt/format.h>
+
+#include "rgw/fdb/fdb.h"
+
+#include <algorithm>
+#include <map>
+#include <ranges>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+
+namespace lfdb = ceph::libfdb;
+
+constexpr const char* const msg = "Hello, World!";
+
+constexpr const char msg_with_null[] = { '\0', 'H', 'i', '\0', ' ', 't', 'h', 'e', 'r', 'e', '!', '\0'};
+
+constexpr const char pearl_msg[] =
+R"(Perle, plesaunte to prynces paye
+To clanly clos in golde so clere;
+Oute of oryent, I hardyly saye.
+Ne proved I never her precios pere.
+)";
+
+// It's a little moribund, but for tests we use this highly-creative (*cough!*) prefix
+// to make it easy to not only remove stale keys but diagnose them when there's a problem:
+inline std::string make_key(const int n, std::string_view prefix = "key")
+{
+ return fmt::format("{}_{:010d}", prefix, n);
+}
+
+inline std::string make_value(const int n)
+{
+ return fmt::format("value_{:010d}", n);
+}
+
+inline std::map<std::string, std::string> make_monotonic_kvs(const unsigned N, std::string_view prefix = "key")
+{
+ std::map<std::string, std::string> kvs;
+
+ for(const auto i : std::ranges::iota_view(0u, N)) {
+ kvs.insert({ make_key(i, prefix), make_value(i) });
+ }
+
+ return kvs;
+}
+
+// Gadget to help clean up test keys-- both removing stale ones when we enter and also tidying
+// up after ourselves when we leave scope:
+struct janitor final
+{
+ ceph::libfdb::database_handle dbh_;
+
+ // flip this off if you need artifacts after debugging:
+ bool drop_after_scope = true;
+
+ janitor(ceph::libfdb::database_handle dbh_)
+ : dbh_(dbh_)
+ {
+ drop_all();
+ }
+
+ janitor()
+ : janitor(ceph::libfdb::create_database())
+ {}
+
+ ~janitor()
+ {
+ if(drop_after_scope)
+ drop_all();
+ }
+
+ public:
+ ceph::libfdb::database_handle dbh() { return dbh_; }
+
+ public:
+ // This type coersion turns out to be very useful:
+ operator ceph::libfdb::database_handle() { return dbh(); }
+
+ public:
+ void drop(const lfdb::select& range)
+ {
+ lfdb::erase(ceph::libfdb::make_transaction(dbh()),
+ range,
+ lfdb::commit_after_op::commit);
+ }
+
+ // Drop everything in the database (except system keys):
+ void drop_all()
+ {
+ // Note: technically, [0x00, 0xFF) is needed to include the system keys (if the transaction's allowed to
+ // access these). However, special permissions are needed to access these magical "system keys" and we
+ // probably don't actually want to delete them erroneously. So, we stick with the normal user-key range...
+ return drop(lfdb::select { "" });
+ }
+
+ // Drop keys made with our key convention:
+ void drop_all_keys()
+ {
+ return drop(lfdb::select { "key_" });
+ }
+};
+
+// This is both a utility for the test suite AND an example of one way to break a long series of writes up
+// into blocks to avoid transaction timeout. Note that it runs serially; a parallel implementation is possible
+// but probably would be slower for the scales used in the test and would involve mechanism a fair amount of
+// extra mechanism, as FDB transactions themselves are not thread-safe:
+inline void populate_monotonic(lfdb::database_handle dbh, const int N, std::string_view prefix = "key")
+{
+ using namespace std::ranges;
+
+ const unsigned stride = 8*1024;
+ const auto total = static_cast<unsigned>(N);
+ const auto nblocks = (total + stride - 1) / stride;
+
+ for(auto block_index : views::iota(0u, nblocks)) {
+ const auto first = block_index * stride;
+ const auto last = std::min(first + stride, total);
+
+ // We make a new transaction each block to avoid session timeout:
+ auto txn = make_transaction(dbh);
+
+ for(auto n : views::iota(first, last)) {
+ lfdb::set(txn, make_key(n, prefix), make_value(n));
+ }
+
+ if(false == lfdb::commit(txn)) {
+ throw std::runtime_error(fmt::format("unable to commit transaction; {} entries, {} stride", total, stride));
+ }
+ }
+}
+
+#endif