Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions metastore/src/meta_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub struct MetaStore {

/// Default tree names used by the MetaStore
/// These constants define the names of the special trees used internally
const DEFAULT_BUCKET_TREE: &str = "_BUCKETS";
const BUCKET_LIST_TREE: &str = "_BUCKETS";
const DEFAULT_BLOCK_TREE: &str = "_BLOCKS";
const DEFAULT_PATH_TREE: &str = "_PATHS";

Expand Down Expand Up @@ -55,15 +55,15 @@ impl MetaStore {
self.inlined_metadata_size - Object::minimum_inline_metadata_size()
}

/// Returns the tree which contains all the buckets.
/// Returns the tree which contains list of all the buckets.
///
/// This tree is used to store the bucket lists and provide
/// the CRUD operations for the bucket list.
///
/// # Returns
/// A tree with extended functionality for bucket operations or an error
pub fn get_allbuckets_tree(&self) -> Result<Box<dyn MetaTreeExt + Send + Sync>, MetaError> {
self.store.tree_ext_open(DEFAULT_BUCKET_TREE)
pub fn get_bucketlist_tree(&self) -> Result<Box<dyn MetaTreeExt + Send + Sync>, MetaError> {
self.store.tree_ext_open(BUCKET_LIST_TREE)
}

/// Returns the tree for a specific bucket with extended methods.
Expand Down Expand Up @@ -159,7 +159,7 @@ impl MetaStore {
/// Success or an error if the insertion fails
pub fn insert_bucket(&self, bucket_name: &str, raw_bucket: Vec<u8>) -> Result<(), MetaError> {
// Insert the bucket metadata into the buckets tree
let buckets = self.store.tree_open(DEFAULT_BUCKET_TREE)?;
let buckets = self.store.tree_open(BUCKET_LIST_TREE)?;
buckets.insert(bucket_name.as_bytes(), raw_bucket)?;

// Create the bucket tree if it doesn't exist
Expand All @@ -177,8 +177,8 @@ impl MetaStore {
/// This method currently loads all buckets into memory at once.
/// TODO: This should be paginated and return a stream for better scalability.
pub fn list_buckets(&self) -> Result<Vec<BucketMeta>, MetaError> {
let bucket = self.get_allbuckets_tree()?;
let buckets = bucket
let bucketlist_tree = self.get_bucketlist_tree()?;
let buckets = bucketlist_tree
.iter_all()
.filter_map(|result| {
let (_, value) = match result {
Expand Down Expand Up @@ -308,7 +308,7 @@ impl MetaStore {
/// # Returns
/// The number of keys in the bucket tree
pub fn num_keys(&self) -> usize {
self.store.num_keys(DEFAULT_BUCKET_TREE).unwrap()
self.store.num_keys(BUCKET_LIST_TREE).unwrap()
}

/// Returns the total disk space used by the metadata store.
Expand All @@ -324,7 +324,7 @@ impl Debug for MetaStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MetaStore")
.field("store", &"<Store>")
.field("bucket_tree_name", &DEFAULT_BUCKET_TREE)
.field("bucket_tree_name", &BUCKET_LIST_TREE)
.field("block_tree_name", &DEFAULT_BLOCK_TREE)
.field("path_tree_name", &DEFAULT_PATH_TREE)
.field("inlined_metadata_size", &self.inlined_metadata_size)
Expand Down
10 changes: 7 additions & 3 deletions respd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ By default, respd listens on `127.0.0.1:6379` and can be accessed using any Redi
| PING [message] | Ping the server (optionally with message)| `PING` or `PING hello` |
| CHECK <key> | Verify data integrity for a key | `CHECK mykey` |
| SELECT <namespace>| Switch to a different namespace | `SELECT mynamespace` |
| NSNEW <name> | Create a new namespace | `NSNEW mynamespace` |
| NSINFO <name> | Show info about a namespace | `NSINFO mynamespace` |
| NSNEW <n> | Create a new namespace | `NSNEW mynamespace` |
| NSINFO <n> | Show info about a namespace | `NSINFO mynamespace` |
| NSLIST | List all available namespaces | `NSLIST` |

- All commands are case-insensitive.
- Namespace commands (`SELECT`, `NSNEW`, `NSINFO`) allow multi-tenant data separation.
- Namespace commands (`SELECT`, `NSNEW`, `NSINFO`, `NSLIST`) allow multi-tenant data separation.

## Features
- Redis protocol compatibility (subset)
- Namespace support
- Data integrity checking
- Simple to run and integrate

## Known Limitations
- The `NSLIST` command currently collects all namespace names before sending the response. Future improvements will implement streaming responses to handle large numbers of namespaces more efficiently.

---

For project overview and build instructions, see the [main README](../README.md).
50 changes: 47 additions & 3 deletions respd/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub enum Command {
Select { namespace: String },
NSNew { name: String },
NSInfo { name: String },
NSList,
// Add more commands as needed
}

Expand Down Expand Up @@ -130,6 +131,13 @@ impl Command {

Ok(Command::NSInfo { name })
}
"NSLIST" => {
if array.len() != 1 {
return Err(CommandError::WrongNumberOfArguments("NSLIST".to_string()));
}

Ok(Command::NSList)
}
"DEL" => {
if array.len() != 2 {
return Err(CommandError::WrongNumberOfArguments("DEL".to_string()));
Expand Down Expand Up @@ -268,9 +276,11 @@ impl CommandHandler {
Command::Check { key } => self.handle_check(key).await,
Command::NSNew { name } => self.handle_nsnew(name).await,
Command::NSInfo { name } => self.handle_nsinfo(name).await,
// SELECT command is handled specially in the server.rs file
// This is just a placeholder to satisfy the compiler
Command::Select { namespace: _ } => Frame::SimpleString("OK".into()),
Command::NSList => self.handle_nslist().await,
Command::Select { .. } => {
// SELECT is handled at a higher level in the connection handler
Frame::Error("ERR SELECT should be handled at connection level".into())
}
}
}

Expand Down Expand Up @@ -407,4 +417,38 @@ impl CommandHandler {
}
}
}

/// Handle NSLIST command - list all namespaces
async fn handle_nslist(&self) -> Frame {
debug!("Handling NSLIST command");

// Use iter_namespace to get a stream of namespaces
match self.storage.iter_namespace() {
Ok(namespace_iter) => {
// Create an array to hold the namespace names
let mut namespaces = Vec::new();

// Process each namespace directly as we receive it
for result in namespace_iter {
match result {
Ok(meta) => {
// Add just the namespace name to the array
namespaces.push(Frame::BulkString(meta.name.into_bytes()));
}
Err(e) => {
error!("Error processing namespace: {}", e);
// Skip this namespace and continue with others
}
}
}

// Return the array of namespace names
Frame::Array(namespaces)
}
Err(e) => {
error!("Error listing namespaces: {}", e);
Frame::Error(format!("ERR {}", e))
}
}
}
}
9 changes: 3 additions & 6 deletions respd/src/resp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ pub enum RespError {
Protocol(String),
}



/// Helper functions for Redis RESP protocol
pub struct RespHelper;

Expand Down Expand Up @@ -65,8 +63,9 @@ impl RespHelper {
if e.to_string().contains("Buffer too small") {
// If buffer is too small, try with a much larger buffer
let mut larger_buffer = vec![0; 16384]; // 16KB should be enough for most responses
let len = redis_protocol::resp2::encode::encode(&mut larger_buffer, frame, false)
.map_err(|e| RespError::Protocol(e.to_string()))?;
let len =
redis_protocol::resp2::encode::encode(&mut larger_buffer, frame, false)
.map_err(|e| RespError::Protocol(e.to_string()))?;
larger_buffer.truncate(len);
Ok(larger_buffer)
} else {
Expand All @@ -75,6 +74,4 @@ impl RespHelper {
}
}
}


}
6 changes: 6 additions & 0 deletions respd/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ use crate::resp::RespHelper;
use crate::storage::Storage;

pub async fn run(addr: String, storage: Storage) -> Result<()> {
// Initialize the default namespace if it doesn't exist
if let Err(e) = storage.init_namespace() {
error!("Failed to initialize namespace: {}", e);
return Err(anyhow::anyhow!("Failed to initialize namespace: {}", e));
}

// Create a TCP listener
let listener = TcpListener::bind(&addr).await?;
info!("Listening on: {}", addr);
Expand Down
45 changes: 43 additions & 2 deletions respd/src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use std::path::PathBuf;

use anyhow::Result;
use serde::{Deserialize, Serialize};
use tracing::info;

use metastore::{BaseMetaTree, Durability, FjallStore, MetaError, MetaStore};

Expand All @@ -27,6 +28,20 @@ impl Storage {
Self { store }
}

/// Initialize the default namespace if it doesn't exist
pub fn init_namespace(&self) -> Result<(), StorageError> {
let default_namespace = "default";

// Check if the default namespace exists
if !self.store.bucket_exists(default_namespace)? {
info!("Default namespace not found, creating it");
// Create the default namespace
self.create_namespace(default_namespace)?;
}

Ok(())
}

/// Get a namespace instance for a specific namespace name
pub fn get_namespace(&self, name: &str) -> Result<Box<dyn BaseMetaTree>, StorageError> {
if !self.store.bucket_exists(name)? {
Expand All @@ -44,8 +59,8 @@ impl Storage {
if !self.store.bucket_exists(name)? {
return Err(StorageError::NamespaceNotFound);
}
let tree = self.store.get_allbuckets_tree()?;
let raw = tree
let bucketlist_tree = self.store.get_bucketlist_tree()?;
let raw = bucketlist_tree
.get(name.as_bytes())
.map_err(|e| StorageError::MetaError(e.to_string()))?;
if let Some(raw) = raw {
Expand All @@ -65,6 +80,32 @@ impl Storage {
self.store.insert_bucket(name, namespace_meta_raw)?;
self.get_namespace(name)
}

/// Iterate over all namespaces in the storage
///
/// Returns an iterator that yields NamespaceMeta structs for each namespace
pub fn iter_namespace(
&self,
) -> Result<impl Iterator<Item = Result<NamespaceMeta, StorageError>>, StorageError> {
// Get the all buckets tree which contains namespace metadata
let bucketlist_tree = self.store.get_bucketlist_tree()?;

// Use tree.iter_all to iterate over all key-value pairs in the tree
let kv_pairs = bucketlist_tree.iter_all();

// Transform the iterator to yield NamespaceMeta structs
let namespace_iter = kv_pairs.map(|kv_result| {
kv_result
.map_err(|e| StorageError::MetaError(e.to_string()))
.and_then(|(_key, value)| {
// Create NamespaceMeta struct from the value using from_msgpack
NamespaceMeta::from_msgpack(&value)
.map_err(|e| StorageError::MetaError(e.to_string()))
})
});

Ok(namespace_iter)
}
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down
32 changes: 32 additions & 0 deletions respd/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,36 @@ mod test_config {

assert_eq!(value, test_value);
}

#[test]
fn test_nslist() {
let server = TestServer::new();
let mut conn = server.connect();

// Create a few namespaces for testing
let namespaces = vec!["ns1", "ns2", "ns3"];

for ns in &namespaces {
let result: String = redis::cmd("NSNEW")
.arg(ns)
.query(&mut conn)
.expect("Failed to create namespace");
assert_eq!(result, "OK");
}

// Execute NSLIST command
let result: Vec<String> = redis::cmd("NSLIST")
.query(&mut conn)
.expect("Failed to execute NSLIST command");

// Verify that all created namespaces are in the result
// Note: The result should also include the default namespace
assert!(result.contains(&"default".to_string()));
for ns in &namespaces {
assert!(result.contains(&ns.to_string()), "Namespace {} not found in NSLIST result", ns);
}

// Verify the total count (all created namespaces + default)
assert_eq!(result.len(), namespaces.len() + 1);
}
}
4 changes: 2 additions & 2 deletions s3cas/src/cas/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,8 @@ impl CasFS {
// TODO: this is very much not optimal
pub async fn bucket_delete(&self, bucket_name: &str) -> Result<(), MetaError> {
// remove from the bucket list tree/partition
let bmt = self.meta_store.get_allbuckets_tree()?;
bmt.remove(bucket_name.as_bytes())?;
let bucketlist_tree = self.meta_store.get_bucketlist_tree()?;
bucketlist_tree.remove(bucket_name.as_bytes())?;

// removes all objects in the bucket
let bucket = self.meta_store.get_bucket_ext(bucket_name)?;
Expand Down