Rust external workload modifications (#11805)

* Rust external workload modifications

- add readme
- add simulation configuration file
- minor Rust bindings changes
- fix FDBPerfMetric::format_code default value in the C++ bindings
- Add CWorkload.c to CMake
- Fix cpp_workload test file

---------

Signed-off-by: Eloi DEMOLIS <eloi.demolis@clever-cloud.com>
This commit is contained in:
Eloi Démolis
2024-12-05 00:43:20 +01:00
committed by GitHub
parent 36829092b3
commit 4848aeba2b
11 changed files with 122 additions and 53 deletions

View File

@@ -382,21 +382,30 @@ if(NOT WIN32)
endif()
set(c_workloads_srcs
set(cpp_workloads_srcs
test/workloads/workloads.cpp
test/workloads/workloads.h
test/workloads/SimpleWorkload.cpp)
set(c_workloads_srcs
test/workloads/CWorkload.c)
if(OPEN_FOR_IDE)
add_library(cpp_workloads OBJECT ${cpp_workloads_srcs})
add_library(c_workloads OBJECT ${c_workloads_srcs})
else()
add_library(cpp_workloads SHARED ${cpp_workloads_srcs})
add_library(c_workloads SHARED ${c_workloads_srcs})
endif()
set_target_properties(cpp_workloads PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/share/foundationdb")
set_target_properties(c_workloads PROPERTIES
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/share/foundationdb")
target_link_libraries(c_workloads PUBLIC fdb_c)
target_link_libraries(cpp_workloads PUBLIC fdb_c)
target_include_directories(c_workloads PUBLIC ${CMAKE_SOURCE_DIR}/bindings/c)
if(NOT WIN32 AND NOT APPLE AND NOT OPEN_FOR_IDE)
target_link_options(cpp_workloads PRIVATE "LINKER:--version-script=${CMAKE_CURRENT_SOURCE_DIR}/external_workload.map,-z,nodelete")
target_link_options(c_workloads PRIVATE "LINKER:--version-script=${CMAKE_CURRENT_SOURCE_DIR}/external_workload.map,-z,nodelete")
endif()

View File

@@ -1,6 +1,7 @@
{
global:
workloadFactory;
workloadCFactory;
local:
*;
};

View File

@@ -85,7 +85,7 @@ struct FDBPerfMetric {
std::string name;
double value;
bool averaged;
std::string format_code = "0.3g";
std::string format_code = "%.3g";
};
class DLLEXPORT FDBWorkload {

View File

@@ -31,6 +31,7 @@ typedef struct CWorkload {
#define BIND(W) CWorkload* this = (CWorkload*)W
#define WITH(C, M, ...) (C).M((C).inner, ##__VA_ARGS__)
#define EXPORT extern __attribute__((visibility("default")))
static void workload_setup(OpaqueWorkload* raw_workload, FDBDatabase* db, FDBPromise done) {
BIND(raw_workload);
@@ -83,7 +84,7 @@ static void workload_free(OpaqueWorkload* raw_workload) {
free(this);
}
extern FDBWorkload workloadCFactory(const char* borrow_name, FDBWorkloadContext context) {
EXPORT FDBWorkload workloadCFactory(const char* borrow_name, FDBWorkloadContext context) {
int len = strlen(borrow_name) + 1;
char* name = (char*)malloc(len);
memcpy(name, borrow_name, len);

View File

@@ -0,0 +1,31 @@
# RustWorkload
This repository showcases an example workload written in Rust, designed to
interact with the FoundationDB simulation environment via the C API. The
workload provided here is not integrated into the CMake build system and serves
purely as a reference implementation.
## Building the Workload
You can build this crate in any standard Rust environment using the following command:
```shell
cargo build --release
```
This will generate a shared library that can be used as an `ExternalWorkload`
within the FoundationDB simulation. To inject this library into the simulation,
use the command:
```shell
fdbserver -r simulation -f test_file.toml
```
Its behavior should be equivalent to the `CWorkload.c` test workload.
## Limitations and Further Examples
This example workload does not include functionality for interacting with the
database. For examples with working database bindings, refer to the
[foundationdb-rs](https://github.com/foundationdb-rs/foundationdb-rs/tree/main/foundationdb-simulation)
project.

View File

@@ -1,4 +1,5 @@
use std::{ffi, str::FromStr};
use std::ffi::{CStr, CString};
use std::str::FromStr;
mod raw_bindings {
#![allow(non_camel_case_types)]
@@ -21,33 +22,37 @@ use raw_bindings::{
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn str_from_c(c_buf: *const i8) -> String {
let c_str = unsafe { ffi::CStr::from_ptr(c_buf) };
let c_str = unsafe { CStr::from_ptr(c_buf) };
c_str.to_str().unwrap().to_string()
}
pub fn str_for_c<T>(buf: T) -> ffi::CString
pub fn str_for_c<T>(buf: T) -> CString
where
T: Into<Vec<u8>>,
{
ffi::CString::new(buf).unwrap()
CString::new(buf).unwrap()
}
// -----------------------------------------------------------------------------
// Rust Types
/// Wrapper around a FoundationDB simulation context
pub struct WorkloadContext(FDBWorkloadContext);
/// Wrapper around a FoundationDB promise
pub struct Promise(FDBPromise);
/// Wrapper around a FoundationDB metric sink
pub struct Metrics(FDBMetrics);
/// A single metric entry
pub struct Metric {
#[derive(Clone, Copy)]
pub struct Metric<'a> {
/// The name of the metric
pub key: String,
pub key: &'a str,
/// The value of the metric
pub val: f64,
/// Indicates if the value represents an average or not
/// Specifies whether the metric value should be aggregated as a sum or an average
pub avg: bool,
/// C++ string formatter of the metric
pub fmt: Option<String>,
pub fmt: Option<&'a str>,
}
/// Indicates the severity of a FoundationDB log entry
@@ -70,10 +75,9 @@ pub enum Severity {
// Implementations
impl WorkloadContext {
pub(crate) fn new(raw: FDBWorkloadContext) -> Self {
pub fn new(raw: FDBWorkloadContext) -> Self {
Self(raw)
}
/// Add a log entry in the FoundationDB logs
pub fn trace<S>(&self, severity: Severity, name: S, details: &[(&str, &str)])
where
@@ -113,7 +117,7 @@ impl WorkloadContext {
pub fn set_process_id(&self, id: u64) {
unsafe { self.0.setProcessID.unwrap_unchecked()(self.0.inner, id) }
}
/// Get the current time
/// Get the current simulated time in seconds (starts at zero)
pub fn now(&self) -> f64 {
unsafe { self.0.now.unwrap_unchecked()(self.0.inner) }
}
@@ -150,7 +154,7 @@ impl WorkloadContext {
pub fn client_id(&self) -> i32 {
unsafe { self.0.clientId.unwrap_unchecked()(self.0.inner) }
}
/// Get the client id of the workload
/// Get the number of clients of the workload
pub fn client_count(&self) -> i32 {
unsafe { self.0.clientCount.unwrap_unchecked()(self.0.inner) }
}
@@ -182,9 +186,11 @@ impl Metrics {
pub(crate) fn new(raw: FDBMetrics) -> Self {
Self(raw)
}
/// Call `reserve` on the underlying C++ vector of metrics
pub fn reserve(&mut self, n: usize) {
unsafe { self.0.reserve.unwrap_unchecked()(self.0.inner, n as i32) }
}
/// Add a single metric to the sink
pub fn push(&mut self, metric: Metric) {
let key_storage = str_for_c(metric.key);
let fmt_storage = str_for_c(metric.fmt.as_deref().unwrap_or("%.3g"));
@@ -200,30 +206,35 @@ impl Metrics {
)
}
}
/// Add multiple metrics, ensuring the sink reallocates at most once
pub fn extend(&mut self, metrics: &[Metric]) {
self.reserve(metrics.len());
for metric in metrics {
self.push(*metric);
}
}
}
impl Metric {
/// Create a metric value entry
pub fn val<S, V>(key: S, val: V) -> Self
impl<'a> Metric<'a> {
/// Create a summed metric entry
pub fn val<V>(key: &'a str, val: V) -> Self
where
S: Into<String>,
f64: From<V>,
V: TryInto<f64>,
{
Self {
key: key.into(),
val: val.into(),
key,
val: val.try_into().ok().expect("convertion failed"),
avg: false,
fmt: None,
}
}
/// Create a metric average entry
pub fn avg<S, V>(key: S, val: V) -> Self
/// Create an averaged metric entry
pub fn avg<V>(key: &'a str, val: V) -> Self
where
S: Into<String>,
V: TryInto<f64>,
{
Self {
key: key.into(),
key,
val: val.try_into().ok().expect("convertion failed"),
avg: true,
fmt: None,

View File

@@ -4,13 +4,14 @@ mod bindings;
mod mock;
use bindings::{
str_from_c, FDBMetrics, FDBPromise, FDBWorkloadContext, OpaqueWorkload,
str_from_c, FDBDatabase, FDBMetrics, FDBPromise, FDBWorkload, FDBWorkloadContext, Metric,
Metrics, OpaqueWorkload, Promise, Severity, WorkloadContext,
};
pub use bindings::{FDBDatabase, FDBWorkload, Metric, Metrics, Promise, Severity, WorkloadContext};
// Should be replaced by a Rust wrapper over the FDBDatabase bindings, like the one provided by
// foundationdb-rs
/// Should be replaced by a Rust wrapper over the `FDBDatabase` bindings, like the one provided by foundationdb-rs
pub type MockDatabase = NonNull<FDBDatabase>;
/// FFI-safe wrapprer around a specific `RustWorkload` implementation
pub type WrappedWorkload = FDBWorkload;
/// Equivalent to the C++ abstract class `FDBWorkload`
pub trait RustWorkload {
@@ -48,7 +49,7 @@ pub trait RustWorkload {
/// * `out` - A metric sink
fn get_metrics(&self, out: Metrics);
/// Set the check timeout for this workload.
/// Set the check timeout in simulated seconds for this workload.
fn get_check_timeout(&self) -> f64;
}
@@ -57,7 +58,7 @@ pub trait RustWorkloadFactory {
/// If the test file contains a key-value pair workloadName the value will be passed to this method (empty string otherwise).
/// This way, a library author can implement many workloads in one library and use the test file to chose which one to run
/// (or run multiple workloads either concurrently or serially).
fn create(name: String, context: WorkloadContext) -> FDBWorkload;
fn create(name: String, context: WorkloadContext) -> WrappedWorkload;
}
unsafe extern "C" fn workload_setup<W: RustWorkload + 'static>(
@@ -108,19 +109,23 @@ unsafe extern "C" fn workload_drop<W: RustWorkload>(raw_workload: *mut OpaqueWor
unsafe { drop(Box::from_raw(raw_workload as *mut W)) };
}
pub fn wrap<W: RustWorkload + 'static>(workload: W) -> FDBWorkload {
let workload = Box::into_raw(Box::new(workload));
FDBWorkload {
inner: workload as *mut _,
setup: Some(workload_setup::<W>),
start: Some(workload_start::<W>),
check: Some(workload_check::<W>),
getMetrics: Some(workload_get_metrics::<W>),
getCheckTimeout: Some(workload_get_check_timeout::<W>),
free: Some(workload_drop::<W>),
impl WrappedWorkload {
pub fn new<W: RustWorkload + 'static>(workload: W) -> Self {
let workload = Box::into_raw(Box::new(workload));
WrappedWorkload {
inner: workload as *mut _,
setup: Some(workload_setup::<W>),
start: Some(workload_start::<W>),
check: Some(workload_check::<W>),
getMetrics: Some(workload_get_metrics::<W>),
getCheckTimeout: Some(workload_get_check_timeout::<W>),
free: Some(workload_drop::<W>),
}
}
}
/// Register a `RustWorkloadFactory` in the FoundationDB simulation.
/// This macro must be invoked exactly once in a program.
#[macro_export]
macro_rules! register_factory {
($name:ident) => {
@@ -128,7 +133,7 @@ macro_rules! register_factory {
extern "C" fn workloadCFactory(
raw_name: *const i8,
raw_context: $crate::FDBWorkloadContext,
) -> $crate::FDBWorkload {
) -> $crate::WrappedWorkload {
let name = $crate::str_from_c(raw_name);
let context = $crate::WorkloadContext::new(raw_context);
<$name as $crate::RustWorkloadFactory>::create(name, context)

View File

@@ -1,6 +1,6 @@
use crate::{
register_factory, wrap, FDBWorkload, Metric, Metrics, MockDatabase, Promise, RustWorkload,
RustWorkloadFactory, Severity, WorkloadContext,
register_factory, Metric, Metrics, MockDatabase, Promise, RustWorkload, RustWorkloadFactory,
Severity, WorkloadContext, WrappedWorkload,
};
struct MockWorkload {
@@ -64,20 +64,20 @@ impl Drop for MockWorkload {
struct MockFactory;
impl RustWorkloadFactory for MockFactory {
fn create(name: String, context: WorkloadContext) -> FDBWorkload {
fn create(name: String, context: WorkloadContext) -> WrappedWorkload {
let client_id = context.client_id();
let client_count = context.client_count();
println!("RustWorkloadFactory::create({name})[{client_id}/{client_count}]");
println!(
"my_c_option: {:?}",
context.get_option::<String>("my_c_option")
context.get_option::<String>("my_rust_option")
);
println!(
"my_c_option: {:?}",
context.get_option::<String>("my_c_option")
context.get_option::<String>("my_rust_option")
);
match name.as_str() {
"MockWorkload" => wrap(MockWorkload::new(name, client_id, context)),
"MockWorkload" => WrappedWorkload::new(MockWorkload::new(name, client_id, context)),
_ => panic!("Unknown workload name: {name}"),
}
}

View File

@@ -0,0 +1,11 @@
[[test]]
testTitle = 'Rust_CAPI_Test'
[[test.workload]]
testName = 'External'
useCAPI = true
libraryPath = './target/release'
libraryName = 'rust_workload'
workloadName = 'MockWorkload'
my_rust_option = 'my_value'

View File

@@ -27,11 +27,11 @@ public class PerfMetric {
private String formatCode;
public PerfMetric(String name, double value) {
this(name, value, true, "0.3g");
this(name, value, true, "%.3g");
}
public PerfMetric(String name, double value, boolean averaged) {
this(name, value, averaged, "0.3g");
this(name, value, averaged, "%.3g");
}
public PerfMetric(String name, double value, boolean averaged, String formatCode) {

View File

@@ -1,4 +1,4 @@
testTitle=SimpleExternalTest
testName=External
libraryName=c_workloads
libraryName=cpp_workloads
workloadName=SimpleWorkload