Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1581a0d
Add proto messages for NodeMetadata and ClusterNodes
CGodiksen Aug 11, 2026
9370183
Add NodeMetrics proto message
CGodiksen Aug 11, 2026
054c01d
Add node to ClusterMode::SingleNode for self identity
CGodiksen Aug 11, 2026
6af2554
Add sysinfo as a dependency to server
CGodiksen Aug 11, 2026
d4a9657
Add getters for the remaining memory in bytes to storage engine
CGodiksen Aug 11, 2026
bdea1a2
Add method to get all nodes in the cluster
CGodiksen Aug 11, 2026
8f3252d
Add util method to get local data folder disk space
CGodiksen Aug 11, 2026
469b6ba
Add ListNodes action
CGodiksen Aug 11, 2026
c6f6916
Add NodeMetrics action
CGodiksen Aug 11, 2026
0ce9d4e
Add new actions to list_actions
CGodiksen Aug 11, 2026
b22605e
Use new nodes method in peer_nodes()
CGodiksen Aug 11, 2026
51d55d5
Separate ListNodes implementation into relevant helper functions
CGodiksen Aug 11, 2026
330ef04
Move ClusterMode from main.rs into cluster.rs
CGodiksen Aug 11, 2026
1d74d9d
Update imports for ClusterMode
CGodiksen Aug 11, 2026
6ad47b6
Move collection of metrics out of do_action and into Context
CGodiksen Aug 11, 2026
f19e3b9
Remove misleading comment
CGodiksen Aug 11, 2026
972ebf7
Fix integration test after adding new actions
CGodiksen Aug 11, 2026
8861c26
Add prost decode error to modelardb_embedded
CGodiksen Aug 12, 2026
50e227b
Add a method to deserialize and extract cluster nodes from protobuf
CGodiksen Aug 12, 2026
259e0d9
Add test for serializing and deserializing nodes
CGodiksen Aug 12, 2026
c4ac14b
Add helper methods to run actions on Client
CGodiksen Aug 12, 2026
8d57405
Refactor action helper so they can be used in modelardb_type and create
CGodiksen Aug 12, 2026
c402383
Add basic node operations to Client struct
CGodiksen Aug 12, 2026
2e173f0
Change order of helper methods for consistency
CGodiksen Aug 12, 2026
1735212
Add a method that uses get_flight_info to get the cloud query node
CGodiksen Aug 12, 2026
7ddb124
Add integration test for list nodes action
CGodiksen Aug 12, 2026
b890074
Add integration test for node metrics action
CGodiksen Aug 12, 2026
d3a6235
Simplify subtraction for used disk space
CGodiksen Aug 12, 2026
33d1bd8
Add better error handling in retrieve_action_bytes()
CGodiksen Aug 13, 2026
20d1376
Simplify local_data_folder_disk_space() based on comments from @skejs…
CGodiksen Aug 14, 2026
0278ae1
Clamp the remaining memory to 0 instead of the used memory
CGodiksen Aug 14, 2026
b0d6607
Update based on comments from @skejserjensen
CGodiksen Aug 14, 2026
b2df518
Merge branch 'main' into dev/node-operations
CGodiksen Aug 14, 2026
50f509d
Update based on comments from @chrthomsen
CGodiksen Aug 14, 2026
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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/modelardb_embedded/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ modelardb_auth.workspace = true
modelardb_compression.workspace = true
modelardb_storage.workspace = true
modelardb_types.workspace = true
prost.workspace = true
tokio.workspace = true
tonic.workspace = true

Expand Down
11 changes: 11 additions & 0 deletions crates/modelardb_embedded/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use deltalake::{DeltaTableError, ObjectStoreError};
use modelardb_compression::error::ModelarDbCompressionError;
use modelardb_storage::error::ModelarDbStorageError;
use modelardb_types::error::ModelarDbTypesError;
use prost::DecodeError;
use tonic::Status as TonicStatusError;
use tonic::transport::Error as TonicTransportError;

Expand Down Expand Up @@ -58,6 +59,8 @@ pub enum ModelarDbEmbeddedError {
ObjectStore(ObjectStoreError),
/// Error returned by Apache Parquet.
Parquet(ParquetError),
/// Error returned by Prost when decoding a message that is not valid.
ProstDecode(DecodeError),
/// Status returned by Tonic.
TonicStatus(Box<TonicStatusError>),
/// Error returned by Tonic.
Expand All @@ -83,6 +86,7 @@ impl Display for ModelarDbEmbeddedError {
Self::ModelarDbTypes(reason) => write!(f, "ModelarDB Types Error: {reason}"),
Self::ObjectStore(reason) => write!(f, "Object Store Error: {reason}"),
Self::Parquet(reason) => write!(f, "Parquet Error: {reason}"),
Self::ProstDecode(reason) => write!(f, "Prost Decode Error: {reason}"),
Self::TonicStatus(reason) => write!(f, "Tonic Status Error: {reason}"),
Self::TonicTransport(reason) => write!(f, "Tonic Transport Error: {reason}"),
Self::Unimplemented(reason) => write!(f, "Unimplemented Error: {reason}"),
Expand All @@ -104,6 +108,7 @@ impl Error for ModelarDbEmbeddedError {
Self::ModelarDbTypes(reason) => Some(reason),
Self::ObjectStore(reason) => Some(reason),
Self::Parquet(reason) => Some(reason),
Self::ProstDecode(reason) => Some(reason),
Self::TonicStatus(reason) => Some(reason),
Self::TonicTransport(reason) => Some(reason),
Self::Unimplemented(_reason) => None,
Expand Down Expand Up @@ -166,6 +171,12 @@ impl From<ParquetError> for ModelarDbEmbeddedError {
}
}

impl From<DecodeError> for ModelarDbEmbeddedError {
fn from(error: DecodeError) -> Self {
Self::ProstDecode(error)
}
}

impl From<TonicStatusError> for ModelarDbEmbeddedError {
fn from(error: TonicStatusError) -> Self {
Self::TonicStatus(Box::new(error))
Expand Down
159 changes: 137 additions & 22 deletions crates/modelardb_embedded/src/operations/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,20 @@ use arrow::record_batch::RecordBatch;
use arrow_flight::decode::FlightRecordBatchStream;
use arrow_flight::encode::FlightDataEncoderBuilder;
use arrow_flight::flight_service_client::FlightServiceClient;
use arrow_flight::{Action, Criteria, FlightDescriptor, Ticket};
use arrow_flight::{Action, Criteria, FlightDescriptor, Result as FlightResult, Ticket};
use async_trait::async_trait;
use datafusion::error::DataFusionError;
use datafusion::execution::RecordBatchStream;
use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
use futures::{StreamExt, TryStreamExt, stream};
use modelardb_auth::BearerInterceptor;
use modelardb_types::flight::protocol;
use modelardb_types::types::Node;
use prost::Message;
use prost::bytes::Bytes;
use tonic::codegen::InterceptedService;
use tonic::transport::{Channel, Endpoint};
use tonic::{Request, Status};
use tonic::{Request, Status, Streaming};

use crate::error::{ModelarDbEmbeddedError, Result};
use crate::operations::{
Expand Down Expand Up @@ -65,6 +69,134 @@ impl Client {

Ok(Client { flight_client })
}

/// Returns the current configuration of the node. If the configuration could not be retrieved,
/// [`ModelarDbEmbeddedError`] is returned.
pub async fn configuration(&mut self) -> Result<protocol::Configuration> {
let bytes = self.retrieve_action_bytes("GetConfiguration").await?;

Ok(protocol::Configuration::decode(bytes)?)
}

/// Updates `setting` in the node configuration to `new_value`. If the setting could not be
/// updated, [`ModelarDbEmbeddedError`] is returned.
pub async fn update_configuration(
&mut self,
setting: protocol::update_configuration::Setting,
new_value: Option<u64>,
) -> Result<()> {
let update_configuration = protocol::UpdateConfiguration {
setting: setting as i32,
new_value,
};

self.send_action("UpdateConfiguration", update_configuration.encode_to_vec())
.await?;

Ok(())
}

/// Flushes all data in memory to disk. If the data could not be flushed,
/// [`ModelarDbEmbeddedError`] is returned.
pub async fn flush_memory(&mut self) -> Result<()> {
self.send_action("FlushMemory", vec![]).await?;

Ok(())
}

/// Flushes all data in memory to disk and then transfers all compressed data to the remote
/// object store. If the data could not be flushed, [`ModelarDbEmbeddedError`] is returned.
pub async fn flush_node(&mut self) -> Result<()> {
self.send_action("FlushNode", vec![]).await?;

Ok(())
}

/// Flushes all data to disk, transfers it to the remote object store, removes the node from the
/// cluster if necessary, and kills the node process. Since the process is killed, a
/// conventional response cannot be returned, so a dropped connection is not treated as an
/// error. If the data could not be flushed before the node was killed,
/// [`ModelarDbEmbeddedError`] is returned.
pub async fn kill_node(&mut self) -> Result<()> {
// The node exits while handling this action, so the response stream is dropped.
let _ = self.send_action("KillNode", vec![]).await;
Comment thread
CGodiksen marked this conversation as resolved.

Ok(())
}

/// Returns the nodes that are currently part of the cluster. A single node returns only itself.
/// If the nodes could not be retrieved, [`ModelarDbEmbeddedError`] is returned.
pub async fn list_nodes(&mut self) -> Result<Vec<Node>> {
let bytes = self.retrieve_action_bytes("ListNodes").await?;

Ok(modelardb_types::flight::deserialize_and_extract_cluster_nodes(&bytes)?)
}

/// Returns the current resource usage metrics of the node. If the metrics could not be
/// retrieved, [`ModelarDbEmbeddedError`] is returned.
pub async fn node_metrics(&mut self) -> Result<protocol::NodeMetrics> {
let bytes = self.retrieve_action_bytes("NodeMetrics").await?;

Ok(protocol::NodeMetrics::decode(bytes)?)
}

/// Sends the action with the type `action_type` and an empty body to the node and returns the
/// body of the response. If the action could not be performed, [`ModelarDbEmbeddedError`] is
/// returned.
async fn retrieve_action_bytes(&mut self, action_type: &str) -> Result<Bytes> {
let mut response = self.send_action(action_type, vec![]).await?;

let message = response.message().await?.ok_or_else(|| {
ModelarDbEmbeddedError::from(Status::internal(format!(
"Action '{action_type}' did not return a response message."
)))
})?;

Ok(message.body)
}

/// Sends the action with the type `action_type` and `body` to the node and returns the response
/// stream. If the action could not be performed, [`ModelarDbEmbeddedError`] is returned.
async fn send_action(
&mut self,
action_type: &str,
body: Vec<u8>,
Comment thread
skejserjensen marked this conversation as resolved.
) -> Result<Streaming<FlightResult>> {
let action = Action {
r#type: action_type.to_owned(),
body: body.into(),
};

let response = self.flight_client.do_action(Request::new(action)).await?;

Ok(response.into_inner())
}

/// Returns the URL of the cloud node that the node assigns to execute the SQL in `sql`. If the
Comment thread
skejserjensen marked this conversation as resolved.
/// node is not running in a cluster, or a cloud node could not be assigned,
/// [`ModelarDbEmbeddedError`] is returned.
pub async fn cloud_query_node(&mut self, sql: &str) -> Result<String> {
let flight_descriptor = FlightDescriptor::new_cmd(sql.to_owned());
let flight_info = self
.flight_client
.get_flight_info(Request::new(flight_descriptor))
.await?
.into_inner();

let endpoint = flight_info.endpoint.into_iter().next().ok_or_else(|| {
ModelarDbEmbeddedError::InvalidArgument(
"The node did not return an endpoint for the query.".to_owned(),
)
})?;

let location = endpoint.location.into_iter().next().ok_or_else(|| {
ModelarDbEmbeddedError::InvalidArgument(
"The endpoint did not return a cloud node location for the query.".to_owned(),
)
})?;

Ok(location.uri)
}
}

#[async_trait]
Expand All @@ -76,21 +208,9 @@ impl Operations for Client {

/// Returns the type of the ModelarDB node that the client is connected to.
async fn modelardb_type(&mut self) -> Result<ModelarDBType> {
// Retrieve the node type from the ModelarDB node.
let action = Action {
r#type: "NodeType".to_owned(),
body: vec![].into(),
};
let bytes = self.retrieve_action_bytes("NodeType").await?;

let response = self.flight_client.do_action(Request::new(action)).await?;

let message = response
.into_inner()
.message()
.await?
.expect("Flight message should exist.");

ModelarDBType::from_str(str::from_utf8(&message.body)?)
ModelarDBType::from_str(str::from_utf8(&bytes)?)
}

/// Creates a table with the name in `table_name` and the information in `table_type`. If the
Expand Down Expand Up @@ -118,12 +238,7 @@ impl Operations for Client {
}
};

let action = Action {
r#type: "CreateTable".to_owned(),
body: protobuf_bytes.into(),
};

self.flight_client.do_action(action).await?;
self.send_action("CreateTable", protobuf_bytes).await?;

Ok(())
}
Expand Down
1 change: 1 addition & 0 deletions crates/modelardb_server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ rand.workspace = true
serde.workspace = true
snmalloc-rs = { workspace = true, features = ["build_cc"] }
sqlparser.workspace = true
sysinfo.workspace = true
tokio = { workspace = true, features = ["rt-multi-thread", "signal"] }
tokio-stream.workspace = true
toml.workspace = true
Expand Down
35 changes: 32 additions & 3 deletions crates/modelardb_server/src/cluster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,26 @@ use tonic::transport::Endpoint;
use crate::context::Context;
use crate::error::{ModelarDbServerError, Result};

/// The different possible modes that a ModelarDB server can be deployed in, assigned when the
/// server is started.
#[derive(Clone)]
pub(crate) enum ClusterMode {
SingleNode(Node),
MultiNode(Cluster),
}

impl ClusterMode {
/// Return all nodes in the deployment. A single node returns only itself, while a node in a
/// cluster returns every node in the cluster. If the nodes could not be retrieved, return
/// [`ModelarDbServerError`].
pub(crate) async fn nodes(&self) -> Result<Vec<Node>> {
match self {
ClusterMode::SingleNode(node) => Ok(vec![node.clone()]),
ClusterMode::MultiNode(cluster) => cluster.nodes().await,
}
}
}

/// Stores the node that represents the local system and allows for performing operations that need
/// to be applied to every peer node in the cluster.
#[derive(Clone)]
Expand Down Expand Up @@ -357,10 +377,19 @@ impl Cluster {
Ok(url.to_owned())
}

/// Return all nodes currently in the cluster. If the nodes could not be retrieved, return
/// [`ModelarDbServerError`].
pub(crate) async fn nodes(&self) -> Result<Vec<Node>> {
self.remote_data_folder
Comment thread
skejserjensen marked this conversation as resolved.
.nodes()
.await
.map_err(|error| error.into())
}

/// Return all nodes in the cluster except the node that was saved when the [`Cluster`] was
/// created. If the nodes could not be retrieved, return [`ModelarDbServerError`].
async fn peer_nodes(&self) -> Result<Vec<Node>> {
let nodes = self.remote_data_folder.nodes().await?;
let nodes = self.nodes().await?;

Ok(nodes
.into_iter()
Expand Down Expand Up @@ -485,8 +514,8 @@ mod test {
use modelardb_types::types::{ArrowTimestamp, ArrowValue, ErrorBound, ServerMode};
use tempfile::TempDir;

use crate::ServerArgs;
use crate::data_folders::DataFolders;
use crate::{ClusterMode, ServerArgs};

// Tests for Cluster.
#[tokio::test]
Expand Down Expand Up @@ -721,7 +750,7 @@ mod test {
Some(cluster.remote_data_folder.clone()),
local_data_folder,
),
ClusterMode::MultiNode(Box::new(cluster)),
ClusterMode::MultiNode(cluster),
&ServerArgs::parse_from(["modelardbd", "edge", "data", "s3://bucket"]),
)
.await
Expand Down
11 changes: 7 additions & 4 deletions crates/modelardb_server/src/configuration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ use prost::Message;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;

use crate::ServerArgs;
use crate::cluster::ClusterMode;
use crate::error::{ModelarDbServerError, Result};
use crate::storage::StorageEngine;
use crate::{ClusterMode, ServerArgs};

const CONFIGURATION_FILE_NAME: &str = "modelardbd.toml";

Expand Down Expand Up @@ -602,9 +603,10 @@ mod tests {
.await
.unwrap();

let node = Node::new("edge".to_owned(), ServerMode::Edge);
let result = ConfigurationManager::try_new(
local_data_folder,
ClusterMode::SingleNode,
ClusterMode::SingleNode(node),
&default_args(),
)
.await;
Expand All @@ -625,9 +627,10 @@ mod tests {
let path = temp_dir.path().join(CONFIGURATION_FILE_NAME);
std::fs::write(path, "invalid_toml").unwrap();

let node = Node::new("edge".to_owned(), ServerMode::Edge);
let result = ConfigurationManager::try_new(
local_data_folder,
ClusterMode::SingleNode,
ClusterMode::SingleNode(node),
&default_args(),
)
.await;
Expand Down Expand Up @@ -1020,7 +1023,7 @@ mod tests {
let configuration_manager = Arc::new(RwLock::new(
ConfigurationManager::try_new(
local_data_folder.clone(),
ClusterMode::MultiNode(Box::new(cluster)),
ClusterMode::MultiNode(cluster),
&default_args(),
)
.await
Expand Down
Loading
Loading