diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5045253d..d2547461 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -469,6 +469,9 @@ jobs: df -h shell: bash + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Build transfer engine only run: | cd mooncake-transfer-engine @@ -503,9 +506,8 @@ jobs: - name: Configure project with unit tests and examples run: | cd build - cmake -G Ninja .. -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DENABLE_SCCACHE=ON + cmake -G Ninja .. -DBUILD_UNIT_TESTS=ON -DBUILD_EXAMPLES=ON -DWITH_STORE_RUST=ON -DENABLE_SCCACHE=ON shell: bash - # TODO: lack WITH_RUST_EXAMPLE - name: Build project with unit tests and examples run: | @@ -516,6 +518,14 @@ jobs: sudo cmake --install . shell: bash + - name: Check Mooncake Store Rust bindings and example + run: | + cd mooncake-store/rust + MOONCAKE_STORE_LIB_DIR=$GITHUB_WORKSPACE/build/mooncake-store/src \ + MOONCAKE_STORE_INCLUDE_DIR=$GITHUB_WORKSPACE/mooncake-store/include \ + cargo check --example basic_usage --tests + shell: bash + - name: Configure project run: | cd build diff --git a/mooncake-store/rust/CMakeLists.txt b/mooncake-store/rust/CMakeLists.txt index 0aa0f887..54908527 100644 --- a/mooncake-store/rust/CMakeLists.txt +++ b/mooncake-store/rust/CMakeLists.txt @@ -21,3 +21,19 @@ add_custom_command( COMMENT "Building mooncake_store Rust crate" VERBATIM ) + +# Build the basic_usage example so that CI can verify it compiles. +add_custom_target(build_mooncake_store_rust_example DEPENDS mooncake_store) + +add_custom_command( + TARGET build_mooncake_store_rust_example + COMMAND + ${CMAKE_COMMAND} -E env + CARGO_TARGET_DIR=${CMAKE_CURRENT_BINARY_DIR} + MOONCAKE_STORE_LIB_DIR=${CMAKE_CURRENT_BINARY_DIR}/../src + MOONCAKE_STORE_INCLUDE_DIR=${CMAKE_CURRENT_SOURCE_DIR}/../include + cargo build --example basic_usage --release + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMENT "Building mooncake_store Rust basic_usage example" + VERBATIM +) diff --git a/mooncake-store/rust/Cargo.toml b/mooncake-store/rust/Cargo.toml index 0a0109ae..eefd85a0 100644 --- a/mooncake-store/rust/Cargo.toml +++ b/mooncake-store/rust/Cargo.toml @@ -10,6 +10,10 @@ name = "mooncake_store" # rlib for use as a Rust dependency, cdylib if a shared-library artifact is needed crate-type = ["rlib"] +[[example]] +name = "basic_usage" +path = "examples/basic_usage.rs" + [dependencies] thiserror = "2.0" libc = "0.2" diff --git a/mooncake-store/rust/examples/basic_usage.rs b/mooncake-store/rust/examples/basic_usage.rs new file mode 100644 index 00000000..2787bb5a --- /dev/null +++ b/mooncake-store/rust/examples/basic_usage.rs @@ -0,0 +1,159 @@ +// Copyright 2024 KVCache.AI +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Basic usage example for the Mooncake Store Rust bindings. +//! +//! This example demonstrates the API surface of `mooncake_store`: +//! - Creating a store handle +//! - Setting up the client (requires a running metadata server) +//! - Storing and retrieving values +//! - Checking existence and sizes +//! - Removing keys +//! +//! # Running +//! +//! The example needs a running Mooncake metadata server. Start one with: +//! +//! ```bash +//! cd mooncake-transfer-engine/example/http-metadata-server-python +//! pip install aiohttp +//! python bootstrap_server.py & +//! ``` +//! +//! Then build and run via CMake (which sets the right library paths): +//! +//! ```bash +//! cd build +//! cmake -G Ninja .. -DWITH_STORE_RUST=ON +//! cmake --build . --target build_mooncake_store_rust +//! ``` +//! +//! Or directly with cargo (after a CMake install): +//! +//! ```bash +//! cargo run --example basic_usage +//! ``` + +use mooncake_store::{MooncakeStore, ReplicateConfig, StoreError}; + +fn main() { + println!("=== Mooncake Store Rust Bindings — Basic Usage ===\n"); + + // Step 1: Create a store handle. + let store = match MooncakeStore::new() { + Ok(s) => { + println!("[OK] Store handle created."); + s + } + Err(e) => { + eprintln!("[FAIL] Could not create store handle: {e}"); + std::process::exit(1); + } + }; + + // Step 2: Connect to the metadata server and transport layer. + // + // In CI or environments without a running metadata server the setup + // call is expected to fail – this is not an error in the bindings + // themselves. + let metadata_server = + std::env::var("MC_METADATA_SERVER").unwrap_or_else(|_| "http://127.0.0.1:8080/metadata".to_string()); + + println!("Connecting to metadata server: {metadata_server}"); + + if let Err(e) = store.setup( + "localhost", + &metadata_server, + 512 << 20, // global_segment_size = 512 MiB + 128 << 20, // local_buffer_size = 128 MiB + "tcp", + "", // device_name (auto-select) + "127.0.0.1:50051", + ) { + eprintln!( + "[SKIP] setup() returned an error (expected when no server is running): {e}\n\ + The API surface has been validated successfully." + ); + std::process::exit(0); + } + + println!("[OK] Store client set up.\n"); + + // Step 3: Put a key/value pair (copies data into the store). + let key = "example-key"; + let value = b"hello, mooncake!"; + + if let Err(e) = store.put(key, value, None) { + eprintln!("[FAIL] put() failed: {e}"); + std::process::exit(1); + } + println!("[OK] put(\"{key}\", {:?})", std::str::from_utf8(value).unwrap()); + + // Step 4: Check existence. + match store.is_exist(key) { + Ok(true) => println!("[OK] is_exist(\"{key}\") = true"), + Ok(false) => println!("[WARN] is_exist(\"{key}\") = false (unexpected)"), + Err(e) => eprintln!("[FAIL] is_exist() failed: {e}"), + } + + // Step 5: Get size. + match store.get_size(key) { + Ok(sz) => println!("[OK] get_size(\"{key}\") = {sz} bytes"), + Err(e) => eprintln!("[FAIL] get_size() failed: {e}"), + } + + // Step 6: Retrieve the value. + match store.get(key) { + Ok(data) => { + println!("[OK] get(\"{key}\") = {:?}", String::from_utf8_lossy(&data)); + assert_eq!(data, value, "round-trip mismatch!"); + } + Err(e) => eprintln!("[FAIL] get() failed: {e}"), + } + + // Step 7: Put with explicit replication config. + let config = ReplicateConfig { + replica_num: 2, + with_soft_pin: true, + preferred_segments: vec!["seg-0".to_string()], + }; + + if let Err(e) = store.put("replicated-key", b"replicated value", Some(&config)) { + eprintln!("[FAIL] put() with ReplicateConfig failed: {e}"); + } else { + println!("[OK] put(\"replicated-key\") with replica_num=2, soft_pin=true"); + } + + // Step 8: Remove the keys. + match store.remove(key, false) { + Ok(()) => println!("[OK] remove(\"{key}\")"), + Err(e) => eprintln!("[FAIL] remove() failed: {e}"), + } + + match store.remove("replicated-key", false) { + Ok(()) => println!("[OK] remove(\"replicated-key\")"), + Err(e) => eprintln!("[FAIL] remove() failed: {e}"), + } + + // Step 9: Demonstrate error handling for a missing key. + match store.get_size("nonexistent") { + Err(StoreError::OperationFailed(code)) => { + println!("[OK] get_size(\"nonexistent\") returned OperationFailed({code}) as expected"); + } + Ok(sz) => println!("[WARN] get_size(\"nonexistent\") unexpectedly returned {sz}"), + Err(e) => println!("[OK] get_size(\"nonexistent\") returned error: {e}"), + } + + println!("\n=== Example completed successfully ==="); +}