feat: vendor midstream and sublinear-time-solver libraries (#109)

Add ruvnet/midstream (AIMDS real-time inference) and
ruvnet/sublinear-time-solver (sublinear optimization algorithms)
as vendored dependencies under vendor/.
This commit is contained in:
rUv
2026-03-02 23:34:05 -05:00
committed by GitHub
parent 14902e6b4e
commit 407b46b206
1600 changed files with 1852646 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
//! Core aggregation framework for time-series data.
//!
//! This module provides the foundational types and functionality for aggregating
//! time-series data. It defines:
//! - Generic aggregation functions (Sum, Avg, Min, Max, Count)
//! - Time window specifications (None, Fixed, Sliding)
//! - Grouping operations
//! - SQL query generation
//!
//! This framework is used by more specific aggregation implementations, such as
//! the metric-specific aggregation in `crate::metrics::aggregation`.
use std::time::Duration;
use serde::{Serialize, Deserialize};
use std::fmt::{Display, Formatter};
/// Time window for aggregation
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum TimeWindow {
/// No time window, aggregate all data
None,
/// Fixed time window (e.g., 5 minutes, 1 hour)
Fixed(Duration),
/// Sliding time window with window size and slide interval
Sliding {
window: Duration,
slide: Duration,
},
}
/// Generic aggregation functions that can be applied to time-series data
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum AggregateFunction {
/// Count the number of values
Count,
/// Sum all values
Sum,
/// Calculate the average
Avg,
/// Find the minimum value
Min,
/// Find the maximum value
Max,
}
impl Display for AggregateFunction {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
AggregateFunction::Count => write!(f, "COUNT"),
AggregateFunction::Sum => write!(f, "SUM"),
AggregateFunction::Avg => write!(f, "AVG"),
AggregateFunction::Min => write!(f, "MIN"),
AggregateFunction::Max => write!(f, "MAX"),
}
}
}
/// Grouping specification for aggregation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GroupBy {
pub columns: Vec<String>,
pub time_column: Option<String>,
}
/// Result of an aggregation operation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregateResult {
pub value: f64,
pub timestamp: i64,
}
impl TimeWindow {
/// Calculates the window boundaries for a given timestamp
pub fn window_bounds(&self, timestamp: i64) -> (i64, i64) {
match *self {
TimeWindow::None => (i64::MIN, i64::MAX),
TimeWindow::Fixed(duration) => {
let window_size = duration.as_secs() as i64;
let window_start = (timestamp / window_size) * window_size;
(window_start, window_start + window_size)
},
TimeWindow::Sliding { window, slide } => {
let window_size = window.as_secs() as i64;
let slide_size = slide.as_secs() as i64;
let current_slide = (timestamp / slide_size) * slide_size;
(current_slide, current_slide + window_size)
}
}
}
/// Generates SQL expressions for window boundaries
pub fn to_sql(&self) -> Option<String> {
match *self {
TimeWindow::None => None,
TimeWindow::Fixed(duration) => {
let window_size = duration.as_secs();
Some(format!(
"(timestamp / {}) * {} as window_start,
((timestamp / {}) + 1) * {} as window_end",
window_size, window_size, window_size, window_size
))
},
TimeWindow::Sliding { window, slide } => {
let window_size = window.as_secs();
let slide_size = slide.as_secs();
Some(format!(
"(timestamp / {}) * {} as window_start,
((timestamp / {}) * {} + {}) as window_end",
slide_size, slide_size, slide_size, slide_size, window_size
))
}
}
}
}
impl AggregateFunction {
/// Generates SQL for the aggregation function
pub fn to_sql(&self, column: &str) -> String {
match self {
AggregateFunction::Sum => format!("SUM({})", column),
AggregateFunction::Avg => format!("AVG({})", column),
AggregateFunction::Min => format!("MIN({})", column),
AggregateFunction::Max => format!("MAX({})", column),
AggregateFunction::Count => format!("COUNT({})", column),
}
}
}
/// Builds a SQL query for aggregation.
///
/// This is the core query builder used by specific aggregation implementations.
/// It provides a flexible way to build SQL queries for different types of
/// time-series data aggregation.
///
/// # Arguments
///
/// * `table_name` - The source table name
/// * `function` - The aggregation function to apply
/// * `group_by` - The grouping specification
/// * `columns` - The columns to aggregate
/// * `from_timestamp` - Optional start of the time range
/// * `to_timestamp` - Optional end of the time range
///
/// # Returns
///
/// A SQL query string for the specified aggregation
pub fn build_aggregate_query(
table_name: &str,
function: AggregateFunction,
group_by: &GroupBy,
columns: &[&str],
from_timestamp: Option<i64>,
to_timestamp: Option<i64>,
) -> String {
let mut query = String::new();
// Build SELECT clause
query.push_str("SELECT ");
// Add group by columns
if !group_by.columns.is_empty() {
let cols: Vec<&str> = group_by.columns.iter().map(|s| s.as_str()).collect();
query.push_str(&cols.join(", "));
query.push_str(", ");
}
// Add time column if present
if let Some(time_col) = &group_by.time_column {
query.push_str(&format!("{}, ", time_col));
}
// Add aggregation function
match function {
AggregateFunction::Sum => query.push_str("SUM(value)"),
AggregateFunction::Avg => query.push_str("AVG(value)"),
AggregateFunction::Count => query.push_str("COUNT(*)"),
AggregateFunction::Min => query.push_str("MIN(value)"),
AggregateFunction::Max => query.push_str("MAX(value)"),
}
// Add FROM clause
query.push_str(&format!(" FROM {}", table_name));
// Add WHERE clause for timestamp range
if let Some(from_ts) = from_timestamp {
query.push_str(&format!(" WHERE timestamp >= {}", from_ts));
if let Some(to_ts) = to_timestamp {
query.push_str(&format!(" AND timestamp <= {}", to_ts));
}
}
// Add GROUP BY clause
if !group_by.columns.is_empty() || group_by.time_column.is_some() {
query.push_str(" GROUP BY ");
let mut group_cols = Vec::new();
if !group_by.columns.is_empty() {
let cols: Vec<&str> = group_by.columns.iter().map(|s| s.as_str()).collect();
group_cols.extend(cols);
}
if let Some(time_col) = &group_by.time_column {
group_cols.push(time_col.as_str());
}
query.push_str(&group_cols.join(", "));
}
query
}
+247
View File
@@ -0,0 +1,247 @@
//! Hyprstream server binary.
//!
//! This binary provides the main entry point for the Hyprstream service, a next-generation application
//! for real-time data ingestion, windowed aggregation, caching, and serving.
//!
//! # Features
//!
//! - **Data Ingestion**: Ingest data efficiently using Arrow Flight
//! - **Intelligent Caching**: High-performance caching with DuckDB
//! - **Real-time Aggregation**: Dynamic metrics and time-windowed aggregates
//! - **ADBC Integration**: Seamless connection to external databases
//!
//! # Configuration
//!
//! Configuration can be provided through multiple sources, in order of precedence:
//!
//! 1. Command-line arguments (highest precedence)
//! 2. Environment variables (prefixed with `HYPRSTREAM_`)
//! 3. User-specified configuration file (via `--config`)
//! 4. System-wide configuration (`/etc/hyprstream/config.toml`)
//! 5. Default configuration (embedded in binary)
//!
//! ## Command-line Options
//!
//! ```text
//! Options:
//! -c, --config <FILE> Path to configuration file
//! --host <HOST> Server host address [env: HYPRSTREAM_SERVER_HOST]
//! --port <PORT> Server port [env: HYPRSTREAM_SERVER_PORT]
//! --engine <TYPE> Primary storage engine type [env: HYPRSTREAM_ENGINE]
//! --engine-connection <STR> Engine connection string [env: HYPRSTREAM_ENGINE_CONNECTION]
//! --engine-options <KEY=VAL> Engine options (can be specified multiple times) [env: HYPRSTREAM_ENGINE_OPTIONS]
//! --enable-cache Enable caching [env: HYPRSTREAM_ENABLE_CACHE]
//! --cache-engine <TYPE> Cache engine type [env: HYPRSTREAM_CACHE_ENGINE]
//! --cache-connection <STR> Cache connection string [env: HYPRSTREAM_CACHE_CONNECTION]
//! --cache-options <KEY=VAL> Cache options (can be specified multiple times) [env: HYPRSTREAM_CACHE_OPTIONS]
//! --cache-max-duration <SECS> Cache max duration in seconds [env: HYPRSTREAM_CACHE_MAX_DURATION]
//! ```
//!
//! ## Configuration File Format (TOML)
//!
//! ```toml
//! # Server Configuration
//! [server]
//! host = "127.0.0.1" # Server host address
//! port = 50051 # Server port number
//!
//! # Engine Configuration
//! [engine]
//! engine = "duckdb" # Options: "duckdb", "adbc"
//! connection = ":memory:"
//! options = { } # Engine-specific options
//!
//! # Cache Configuration
//! [cache]
//! enabled = true # Enable caching
//! engine = "duckdb" # Options: "duckdb", "adbc"
//! connection = ":memory:"
//! max_duration_secs = 3600
//! options = { } # Cache-specific options
//! ```
//!
//! ## Storage Backends
//!
//! ### DuckDB Backend
//!
//! The DuckDB backend provides high-performance embedded storage:
//!
//! ```toml
//! [engine]
//! engine = "duckdb"
//! connection = ":memory:" # Use ":memory:" for in-memory or file path
//! options = {
//! threads = "4", # Number of threads (optional)
//! read_only = "false" # Read-only mode (optional)
//! }
//! ```
//!
//! ### ADBC Backend
//!
//! The ADBC backend enables connection to external databases:
//!
//! ```toml
//! [engine]
//! engine = "adbc"
//! connection = "postgresql://localhost:5432"
//! options = {
//! driver_path = "/usr/local/lib/libadbc_driver_postgresql.so", # Required
//! username = "postgres", # Optional
//! password = "secret", # Optional
//! database = "metrics", # Optional
//! pool_max = "10", # Optional
//! pool_min = "1", # Optional
//! connect_timeout = "30" # Optional
//! }
//! ```
//!
//! ### Cached Backend
//!
//! The cached backend implements a two-tier storage system:
//!
//! ```toml
//! [engine]
//! engine = "duckdb"
//! connection = "data.db"
//! options = { }
//!
//! [cache]
//! enabled = true
//! engine = "duckdb"
//! connection = ":memory:"
//! max_duration_secs = 3600
//! options = {
//! threads = "2"
//! }
//! ```
//!
//! # Examples
//!
//! ## Basic Usage
//!
//! ```bash
//! # Run with default configuration
//! hyprstream
//!
//! # Run with custom configuration file
//! hyprstream --config /path/to/config.toml
//!
//! # Run with environment variables
//! HYPRSTREAM_SERVER_HOST=0.0.0.0 HYPRSTREAM_SERVER_PORT=50051 hyprstream
//! ```
//!
//! ## Advanced Configuration
//!
//! ```bash
//! # Run with DuckDB storage and caching
//! hyprstream \
//! --engine duckdb \
//! --engine-connection ":memory:" \
//! --engine-options threads=4 \
//! --enable-cache \
//! --cache-engine duckdb \
//! --cache-connection ":memory:" \
//! --cache-max-duration 3600
//!
//! # Run with ADBC PostgreSQL backend
//! hyprstream \
//! --engine adbc \
//! --engine-connection "postgresql://localhost:5432" \
//! --engine-options driver_path=/usr/local/lib/libadbc_driver_postgresql.so \
//! --engine-options username=postgres \
//! --engine-options database=metrics \
//! --engine-options pool_max=10
//!
//! # Run with ADBC backend and DuckDB cache
//! hyprstream \
//! --engine adbc \
//! --engine-connection "postgresql://localhost:5432" \
//! --engine-options driver_path=/usr/local/lib/libadbc_driver_postgresql.so \
//! --engine-options username=postgres \
//! --enable-cache \
//! --cache-engine duckdb \
//! --cache-connection ":memory:" \
//! --cache-options threads=2 \
//! --cache-max-duration 3600
//! ```
//!
//! For more examples and detailed API documentation, visit the
//! [Hyprstream documentation](https://docs.rs/hyprstream).
use clap::Parser;
use hyprstream_core::{
config::{CliArgs, Settings},
service::FlightSqlService,
storage::{StorageBackend, adbc::AdbcBackend, duckdb::DuckDbBackend},
};
use std::sync::Arc;
use tonic::transport::Server;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli_args = CliArgs::parse();
// Load settings from config file and CLI args
let settings = Settings::new(cli_args)?;
// Create the storage backend based on configuration
let engine_backend: Box<dyn StorageBackend> = match settings.engine.engine.as_str() {
"adbc" => {
Box::new(AdbcBackend::new_with_options(
&settings.engine.connection,
&settings.engine.options,
settings.engine.credentials.as_ref(),
)?)
}
"duckdb" => {
Box::new(DuckDbBackend::new_with_options(
&settings.engine.connection,
&settings.engine.options,
settings.engine.credentials.as_ref(),
)?)
}
_ => return Err("Unsupported engine type".into()),
};
// Initialize the storage backend
engine_backend.init().await?;
// Create cache backend if configured
let cache_backend = if settings.cache.enabled {
let cache_config = &settings.cache;
let backend: Box<dyn StorageBackend> = match cache_config.engine.as_str() {
"adbc" => {
Box::new(AdbcBackend::new_with_options(
&cache_config.connection,
&cache_config.options,
cache_config.credentials.as_ref(),
)?)
}
"duckdb" => {
Box::new(DuckDbBackend::new_with_options(
&cache_config.connection,
&cache_config.options,
cache_config.credentials.as_ref(),
)?)
}
_ => return Err("Unsupported cache engine type".into()),
};
Some(backend)
} else {
None
};
// Create the Flight SQL service
let service = FlightSqlService::new(engine_backend);
// Start the server
let addr = format!("{}:{}", settings.server.host, settings.server.port).parse()?;
println!("Starting server on {}", addr);
Server::builder()
.add_service(arrow_flight::flight_service_server::FlightServiceServer::new(service))
.serve(addr)
.await?;
Ok(())
}
+329
View File
@@ -0,0 +1,329 @@
//! Configuration management for Hyprstream service.
//!
//! This module provides configuration handling through multiple sources:
//! 1. Default configuration (embedded in binary)
//! 2. System-wide configuration file (`/etc/hyprstream/config.toml`)
//! 3. User-specified configuration file
//! 4. Environment variables (prefixed with `HYPRSTREAM_`)
//! 5. Command-line arguments
//!
//! Configuration options are loaded in order of precedence, with later sources
//! overriding earlier ones.
//!
//! # Environment Variables
//!
//! Backend-specific credentials should be provided via environment variables:
//! - `HYPRSTREAM_ENGINE_USERNAME` - Primary storage backend username
//! - `HYPRSTREAM_ENGINE_PASSWORD` - Primary storage backend password
//! - `HYPRSTREAM_CACHE_USERNAME` - Cache backend username (if needed)
//! - `HYPRSTREAM_CACHE_PASSWORD` - Cache backend password (if needed)
use clap::Parser;
use config::{Config, ConfigError};
use serde::Deserialize;
use std::env;
use std::path::PathBuf;
use std::collections::HashMap;
const DEFAULT_CONFIG: &str = include_str!("../config/default.toml");
const DEFAULT_CONFIG_PATH: &str = "/etc/hyprstream/config.toml";
/// Command-line arguments parser.
///
/// This structure defines all available command-line options and their
/// corresponding environment variables. It uses clap for parsing and
/// supports both short and long option forms.
#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct CliArgs {
/// Path to the configuration file
#[arg(short, long, value_name = "FILE")]
config: Option<PathBuf>,
/// Server host address
#[arg(long, env = "HYPRSTREAM_SERVER_HOST")]
host: Option<String>,
/// Server port
#[arg(long, env = "HYPRSTREAM_SERVER_PORT")]
port: Option<u16>,
/// Primary storage engine type
#[arg(long, env = "HYPRSTREAM_ENGINE")]
engine: Option<String>,
/// Primary storage engine connection string
#[arg(long, env = "HYPRSTREAM_ENGINE_CONNECTION")]
engine_connection: Option<String>,
/// Primary storage engine options (key=value pairs)
#[arg(long, env = "HYPRSTREAM_ENGINE_OPTIONS")]
engine_options: Option<Vec<String>>,
/// Enable caching
#[arg(long, env = "HYPRSTREAM_ENABLE_CACHE")]
enable_cache: Option<bool>,
/// Cache engine type
#[arg(long, env = "HYPRSTREAM_CACHE_ENGINE")]
cache_engine: Option<String>,
/// Cache engine connection string
#[arg(long, env = "HYPRSTREAM_CACHE_CONNECTION")]
cache_connection: Option<String>,
/// Cache engine options (key=value pairs)
#[arg(long, env = "HYPRSTREAM_CACHE_OPTIONS")]
cache_options: Option<Vec<String>>,
/// Cache maximum duration in seconds
#[arg(long, env = "HYPRSTREAM_CACHE_MAX_DURATION")]
cache_max_duration: Option<u64>,
/// Primary storage engine username
#[arg(long, env = "HYPRSTREAM_ENGINE_USERNAME")]
engine_username: Option<String>,
/// Primary storage engine password
#[arg(long, env = "HYPRSTREAM_ENGINE_PASSWORD")]
engine_password: Option<String>,
/// Cache engine username
#[arg(long, env = "HYPRSTREAM_CACHE_USERNAME")]
cache_username: Option<String>,
/// Cache engine password
#[arg(long, env = "HYPRSTREAM_CACHE_PASSWORD")]
cache_password: Option<String>,
}
/// Complete service configuration.
///
/// This structure holds all configuration options for the service,
/// including server settings, storage backend configuration, and
/// cache settings.
#[derive(Debug, Deserialize)]
pub struct Settings {
/// Server configuration
pub server: ServerConfig,
/// Engine configuration
pub engine: EngineConfig,
/// Cache configuration
pub cache: CacheConfig,
}
/// Server configuration options.
///
/// Defines the network interface and port for the Flight SQL service.
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
/// Host address to bind to
pub host: String,
/// Port number to listen on
pub port: u16,
}
/// Engine configuration.
///
/// Specifies the primary storage engine to use for metric data.
#[derive(Debug, Deserialize)]
pub struct EngineConfig {
/// Engine type ("duckdb" or "adbc")
pub engine: String,
/// Connection string for the engine
pub connection: String,
/// Engine-specific options
#[serde(default)]
pub options: std::collections::HashMap<String, String>,
/// Authentication credentials (not serialized)
#[serde(skip)]
pub credentials: Option<Credentials>,
}
/// Authentication credentials for storage backends.
#[derive(Debug, Clone, Deserialize)]
pub struct Credentials {
/// Username for authentication
pub username: String,
/// Password for authentication
pub password: String,
}
/// Cache configuration for the storage backend.
#[derive(Debug, Deserialize)]
pub struct CacheConfig {
/// Whether caching is enabled
pub enabled: bool,
/// Cache storage engine type (e.g., "duckdb", "adbc")
pub engine: String,
/// Cache connection string
pub connection: String,
/// Cache engine options
pub options: HashMap<String, String>,
/// Cache credentials (optional)
#[serde(default)]
pub credentials: Option<Credentials>,
/// Maximum duration to keep entries in cache (in seconds)
#[serde(default = "default_ttl")]
pub ttl: Option<u64>,
}
fn default_ttl() -> Option<u64> {
Some(3600) // Default 1 hour TTL
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
enabled: false,
engine: "duckdb".to_string(),
connection: ":memory:".to_string(),
options: HashMap::new(),
credentials: None,
ttl: default_ttl(),
}
}
}
impl Settings {
/// Loads configuration from all available sources.
pub fn new(cli: CliArgs) -> Result<Self, ConfigError> {
let mut builder = Config::builder();
// Load default configuration
builder = builder.add_source(config::File::from_str(
DEFAULT_CONFIG,
config::FileFormat::Toml,
));
// Load system configuration if it exists
if let Ok(metadata) = std::fs::metadata(DEFAULT_CONFIG_PATH) {
if metadata.is_file() {
builder = builder.add_source(config::File::from(PathBuf::from(DEFAULT_CONFIG_PATH)));
}
}
// Load user configuration if specified
if let Some(ref config_path) = cli.config {
builder = builder.add_source(config::File::from(config_path.clone()));
}
// Add environment variables (prefixed with HYPRSTREAM_)
builder = builder.add_source(config::Environment::with_prefix("HYPRSTREAM"));
// Override with command line arguments
if let Some(ref host) = cli.host {
builder = builder.set_override("server.host", host.as_str())?;
}
if let Some(port) = cli.port {
builder = builder.set_override("server.port", port)?;
}
// Engine settings
if let Some(ref engine) = cli.engine {
builder = builder.set_override("engine.engine", engine.as_str())?;
}
if let Some(ref connection) = cli.engine_connection {
builder = builder.set_override("engine.connection", connection.as_str())?;
}
if let Some(ref options) = cli.engine_options {
let options: std::collections::HashMap<String, String> = options
.iter()
.filter_map(|opt| {
let parts: Vec<&str> = opt.split('=').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect();
builder = builder.set_override("engine.options", options)?;
}
// Cache settings
if let Some(enabled) = cli.enable_cache {
builder = builder.set_override("cache.enabled", enabled)?;
}
if let Some(ref engine) = cli.cache_engine {
builder = builder.set_override("cache.engine", engine.as_str())?;
}
if let Some(ref connection) = cli.cache_connection {
builder = builder.set_override("cache.connection", connection.as_str())?;
}
if let Some(ref options) = cli.cache_options {
let options: std::collections::HashMap<String, String> = options
.iter()
.filter_map(|opt| {
let parts: Vec<&str> = opt.split('=').collect();
if parts.len() == 2 {
Some((parts[0].to_string(), parts[1].to_string()))
} else {
None
}
})
.collect();
builder = builder.set_override("cache.options", options)?;
}
if let Some(duration) = cli.cache_max_duration {
builder = builder.set_override("cache.max_duration_secs", duration)?;
}
// Build initial settings
let mut settings: Settings = builder.build()?.try_deserialize()?;
// Load credentials from environment variables and CLI args
settings.engine.credentials = Self::load_engine_credentials(&cli);
settings.cache.credentials = Self::load_cache_credentials(&cli);
Ok(settings)
}
/// Load engine credentials from all available sources.
/// Priority order (highest to lowest):
/// 1. Environment variables
/// 2. Command line arguments
fn load_engine_credentials(cli: &CliArgs) -> Option<Credentials> {
// Try environment variables first
if let (Some(username), Some(password)) = (
env::var("HYPRSTREAM_ENGINE_USERNAME").ok(),
env::var("HYPRSTREAM_ENGINE_PASSWORD").ok(),
) {
return Some(Credentials { username, password });
}
// Try command line arguments
if let (Some(username), Some(password)) = (&cli.engine_username, &cli.engine_password) {
return Some(Credentials {
username: username.clone(),
password: password.clone(),
});
}
None
}
/// Load cache credentials from all available sources.
/// Priority order (highest to lowest):
/// 1. Environment variables
/// 2. Command line arguments
fn load_cache_credentials(cli: &CliArgs) -> Option<Credentials> {
// Try environment variables first
if let (Some(username), Some(password)) = (
env::var("HYPRSTREAM_CACHE_USERNAME").ok(),
env::var("HYPRSTREAM_CACHE_PASSWORD").ok(),
) {
return Some(Credentials { username, password });
}
// Try command line arguments
if let (Some(username), Some(password)) = (&cli.cache_username, &cli.cache_password) {
return Some(Credentials {
username: username.clone(),
password: password.clone(),
});
}
None
}
}
+83
View File
@@ -0,0 +1,83 @@
/*!
# Hyprstream: Real-time Aggregation Windows and High-Performance Cache for Apache Arrow Flight SQL
Hyprstream is a next-generation application for real-time data ingestion, windowed aggregation, caching, and serving.
Built on Apache Arrow Flight and DuckDB, and developed in Rust, Hyprstream dynamically calculates metrics like running
sums, counts, and averages, enabling blazing-fast data workflows, intelligent caching, and seamless integration with
ADBC-compliant datastores.
## Key Features
### Data Ingestion via Apache Arrow Flight
- Streamlined ingestion using Arrow Flight for efficient columnar data transport
- Real-time streaming support for metrics, datasets, and vectorized data
- Seamless integration with data producers for high-throughput ingestion
- Write-through to ADBC datastores for eventual data consistency
### Intelligent Read Caching with DuckDB
- In-memory performance using DuckDB for lightning-fast caching
- Optimized querying for analytics workloads
- Automatic cache management with configurable expiry policies
- Time-based expiry with future support for LRU/LFU policies
### Data Serving with Arrow Flight SQL
- High-performance queries via Arrow Flight SQL
- Support for vectorized data and analytical queries
- Seamless integration with analytics and visualization tools
### Real-Time Aggregation
- Dynamic metrics with running sums, counts, and averages
- Lightweight state management for aggregate calculations
- Dynamic weight computation for AI/ML pipelines
- Time window partitioning for granular analysis
## Usage
Basic usage example with programmatic configuration:
```rust,no_run
use hyprstream::config::{Settings, EngineConfig, CacheConfig};
use hyprstream::service::FlightServiceImpl;
use std::sync::Arc;
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create configuration programmatically
let mut settings = Settings::default();
// Configure primary storage engine
settings.engine.engine = "duckdb".to_string();
settings.engine.connection = ":memory:".to_string();
settings.engine.options.insert("threads".to_string(), "4".to_string());
// Configure caching (optional)
settings.cache.enabled = true;
settings.cache.engine = "duckdb".to_string();
settings.cache.connection = ":memory:".to_string();
settings.cache.max_duration_secs = 3600;
// Create and initialize the service
let service = FlightServiceImpl::from_settings(&settings).await?;
// Use the service in your application...
Ok(())
}
```
For detailed configuration options and examples, see:
- [`config`](crate::config) module for configuration options
- [`storage`](crate::storage) module for storage backend details
- [`examples`](examples/) directory for more usage examples
*/
pub mod metrics;
pub mod storage;
pub mod service;
pub mod config;
pub mod aggregation;
pub use service::FlightSqlService;
pub use storage::StorageBackend;
pub use metrics::MetricRecord;
pub use aggregation::{TimeWindow, AggregateFunction, GroupBy, AggregateResult};
@@ -0,0 +1,97 @@
//! Metric-specific aggregation functionality.
//!
//! This module provides metric-specific implementations of aggregation functions
//! while leveraging the core aggregation framework from `crate::aggregation`.
//! It handles the specific requirements of metric aggregation, such as:
//! - Running window calculations (sum, avg, count)
//! - Metric-specific SQL query generation
//! - Direct aggregation of MetricRecord instances
//!
//! The implementation reuses the generic aggregation types and query building
//! functionality from the core aggregation module while adding metric-specific
//! logic and optimizations.
use crate::metrics::MetricRecord;
use crate::aggregation::{AggregateFunction, GroupBy, build_aggregate_query};
use tonic::Status;
/// The standard metric value columns used in aggregation queries
const METRIC_VALUE_COLUMNS: [&str; 3] = [
"value_running_window_sum",
"value_running_window_avg",
"value_running_window_count"
];
/// Applies the aggregation function to a set of metrics.
///
/// This function implements metric-specific aggregation by operating directly
/// on MetricRecord instances. It uses the appropriate running window value
/// based on the aggregation function type.
///
/// # Arguments
///
/// * `function` - The aggregation function to apply
/// * `metrics` - The set of metrics to aggregate
///
/// # Returns
///
/// The aggregated value as a float, or an error if the operation fails
pub fn apply_function(function: AggregateFunction, metrics: &[MetricRecord]) -> Result<f64, Status> {
if metrics.is_empty() {
return Ok(0.0);
}
match function {
AggregateFunction::Sum => Ok(metrics.iter().map(|m| m.value_running_window_sum).sum()),
AggregateFunction::Avg => {
let sum: f64 = metrics.iter().map(|m| m.value_running_window_avg).sum();
Ok(sum / metrics.len() as f64)
},
AggregateFunction::Min => Ok(metrics
.iter()
.map(|m| m.value_running_window_sum)
.fold(f64::INFINITY, f64::min)),
AggregateFunction::Max => Ok(metrics
.iter()
.map(|m| m.value_running_window_sum)
.fold(f64::NEG_INFINITY, f64::max)),
AggregateFunction::Count => Ok(metrics.len() as f64),
}
}
/// Builds a SQL query for metrics aggregation.
///
/// This function specializes the generic aggregate query builder for metrics
/// by providing the metric-specific value columns and table name. It reuses
/// the core query building logic while adding metric-specific context.
///
/// # Arguments
///
/// * `function` - The aggregation function to apply
/// * `group_by` - The grouping specification
/// * `from_timestamp` - The start of the time range
/// * `to_timestamp` - The optional end of the time range
///
/// # Returns
///
/// A SQL query string optimized for metric aggregation
pub fn build_metrics_query(
function: AggregateFunction,
group_by: &GroupBy,
from_timestamp: i64,
to_timestamp: Option<i64>,
) -> String {
let columns = METRIC_VALUE_COLUMNS.iter()
.map(|&c| c.to_string())
.collect::<Vec<_>>();
let column_refs: Vec<&str> = columns.iter().map(|s| s.as_str()).collect();
build_aggregate_query(
"metrics",
function,
group_by,
&column_refs,
Some(from_timestamp),
to_timestamp,
)
}
+90
View File
@@ -0,0 +1,90 @@
pub mod aggregation;
use arrow_array::{ArrayRef, Float64Array, Int64Array, RecordBatch, StringArray};
use arrow_schema::{DataType, Field, Schema};
use std::sync::Arc;
use tonic::Status;
/// A single metric record with running window calculations.
#[derive(Debug, Clone)]
pub struct MetricRecord {
/// Unique identifier for the metric
pub metric_id: String,
/// Unix timestamp in seconds
pub timestamp: i64,
/// Running sum within the window
pub value_running_window_sum: f64,
/// Running average within the window
pub value_running_window_avg: f64,
/// Running count within the window
pub value_running_window_count: i64,
}
/// Gets the schema for metric records in Arrow format.
pub fn get_metrics_schema() -> Schema {
Schema::new(vec![
Field::new("metric_id", DataType::Utf8, false),
Field::new("timestamp", DataType::Int64, false),
Field::new("value_running_window_sum", DataType::Float64, false),
Field::new("value_running_window_avg", DataType::Float64, false),
Field::new("value_running_window_count", DataType::Int64, false),
])
}
/// Creates a RecordBatch from a vector of MetricRecords.
pub fn create_record_batch(metrics: &[MetricRecord]) -> Result<RecordBatch, Status> {
let schema = get_metrics_schema();
let metric_ids = StringArray::from_iter_values(metrics.iter().map(|m| m.metric_id.as_str()));
let timestamps = Int64Array::from_iter_values(metrics.iter().map(|m| m.timestamp));
let sums = Float64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_sum));
let avgs = Float64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_avg));
let counts = Int64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_count));
let arrays: Vec<ArrayRef> = vec![
Arc::new(metric_ids),
Arc::new(timestamps),
Arc::new(sums),
Arc::new(avgs),
Arc::new(counts),
];
RecordBatch::try_new(Arc::new(schema), arrays)
.map_err(|e| Status::internal(format!("Failed to create record batch: {}", e)))
}
/// Encodes a RecordBatch into a vector of MetricRecords.
pub fn encode_record_batch(batch: &RecordBatch) -> Result<Vec<MetricRecord>, Status> {
let metric_ids = batch.column_by_name("metric_id")
.and_then(|col| col.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| Status::internal("Invalid metric_id column"))?;
let timestamps = batch.column_by_name("timestamp")
.and_then(|col| col.as_any().downcast_ref::<Int64Array>())
.ok_or_else(|| Status::internal("Invalid timestamp column"))?;
let sums = batch.column_by_name("value_running_window_sum")
.and_then(|col| col.as_any().downcast_ref::<Float64Array>())
.ok_or_else(|| Status::internal("Invalid value_running_window_sum column"))?;
let avgs = batch.column_by_name("value_running_window_avg")
.and_then(|col| col.as_any().downcast_ref::<Float64Array>())
.ok_or_else(|| Status::internal("Invalid value_running_window_avg column"))?;
let counts = batch.column_by_name("value_running_window_count")
.and_then(|col| col.as_any().downcast_ref::<Int64Array>())
.ok_or_else(|| Status::internal("Invalid value_running_window_count column"))?;
let mut metrics = Vec::with_capacity(batch.num_rows());
for i in 0..batch.num_rows() {
metrics.push(MetricRecord {
metric_id: metric_ids.value(i).to_string(),
timestamp: timestamps.value(i),
value_running_window_sum: sums.value(i),
value_running_window_avg: avgs.value(i),
value_running_window_count: counts.value(i),
});
}
Ok(metrics)
}
+194
View File
@@ -0,0 +1,194 @@
//! Arrow Flight SQL service implementation for high-performance data transport.
//!
//! This module provides the core Flight SQL service implementation that enables:
//! - High-performance data queries via Arrow Flight protocol
//! - Support for vectorized data operations
//! - Real-time metric aggregation queries
//! - Time-windowed data access
//!
//! The service implementation is designed to work with multiple storage backends
//! while maintaining consistent query semantics and high performance.
use crate::storage::StorageBackend;
use arrow_flight::{
flight_service_server::FlightService,
Action, ActionType, Criteria, FlightData, FlightDescriptor, FlightInfo,
HandshakeRequest, HandshakeResponse, PutResult, SchemaResult, Ticket,
Empty, PollInfo,
};
use arrow_schema::Schema;
use bytes::Bytes;
use futures::Stream;
use std::pin::Pin;
use std::sync::Arc;
use tonic::{Request, Response, Status, Streaming};
use crate::storage::table_manager::AggregationView;
use serde::Deserialize;
use serde_json;
/// Command types for table and view operations
#[derive(Debug)]
enum TableCommand {
CreateTable {
name: String,
schema: Arc<Schema>,
},
CreateAggregationView(AggregationView),
DropTable(String),
DropAggregationView(String),
}
impl TableCommand {
fn from_json(cmd: &[u8]) -> Result<Self, Status> {
#[derive(Deserialize)]
struct CreateTableCmd {
name: String,
schema_bytes: Vec<u8>,
}
let value: serde_json::Value = serde_json::from_slice(cmd)
.map_err(|e| Status::invalid_argument(format!("Invalid JSON: {}", e)))?;
match value.get("type").and_then(|t| t.as_str()) {
Some("create_table") => {
let cmd: CreateTableCmd = serde_json::from_value(value["data"].clone())
.map_err(|e| Status::invalid_argument(format!("Invalid create table command: {}", e)))?;
// Convert schema bytes to Schema using Arrow IPC
let message = arrow_ipc::root_as_message(&cmd.schema_bytes[..])
.map_err(|e| Status::invalid_argument(format!("Invalid schema bytes: {}", e)))?;
let schema = message.header_as_schema()
.ok_or_else(|| Status::invalid_argument("Message is not a schema"))?;
let schema = arrow_ipc::convert::fb_to_schema(schema);
Ok(TableCommand::CreateTable {
name: cmd.name,
schema: Arc::new(schema),
})
}
Some("create_aggregation_view") => {
let view: AggregationView = serde_json::from_value(value["data"].clone())
.map_err(|e| Status::invalid_argument(format!("Invalid view command: {}", e)))?;
Ok(TableCommand::CreateAggregationView(view))
}
Some("drop_table") => {
let name = value["data"]["name"].as_str()
.ok_or_else(|| Status::invalid_argument("Missing table name"))?;
Ok(TableCommand::DropTable(name.to_string()))
}
Some("drop_aggregation_view") => {
let name = value["data"]["name"].as_str()
.ok_or_else(|| Status::invalid_argument("Missing view name"))?;
Ok(TableCommand::DropAggregationView(name.to_string()))
}
_ => Err(Status::invalid_argument("Invalid command type")),
}
}
}
pub struct FlightSqlService {
backend: Box<dyn StorageBackend>,
}
impl FlightSqlService {
pub fn new(backend: Box<dyn StorageBackend>) -> Self {
Self { backend }
}
}
#[tonic::async_trait]
impl FlightService for FlightSqlService {
type HandshakeStream = Pin<Box<dyn Stream<Item = Result<HandshakeResponse, Status>> + Send + 'static>>;
type ListFlightsStream = Pin<Box<dyn Stream<Item = Result<FlightInfo, Status>> + Send + 'static>>;
type DoGetStream = Pin<Box<dyn Stream<Item = Result<FlightData, Status>> + Send + 'static>>;
type DoPutStream = Pin<Box<dyn Stream<Item = Result<PutResult, Status>> + Send + 'static>>;
type DoActionStream = Pin<Box<dyn Stream<Item = Result<arrow_flight::Result, Status>> + Send + 'static>>;
type ListActionsStream = Pin<Box<dyn Stream<Item = Result<ActionType, Status>> + Send + 'static>>;
type DoExchangeStream = Pin<Box<dyn Stream<Item = Result<FlightData, Status>> + Send + 'static>>;
async fn get_schema(
&self,
_request: Request<FlightDescriptor>,
) -> Result<Response<SchemaResult>, Status> {
// Implementation here
todo!()
}
async fn do_get(
&self,
_request: Request<Ticket>,
) -> Result<Response<Self::DoGetStream>, Status> {
// Implementation here
todo!()
}
async fn handshake(
&self,
_request: Request<Streaming<HandshakeRequest>>,
) -> Result<Response<Self::HandshakeStream>, Status> {
// Implementation here
todo!()
}
async fn list_flights(
&self,
_request: Request<Criteria>,
) -> Result<Response<Self::ListFlightsStream>, Status> {
// Implementation here
todo!()
}
async fn get_flight_info(
&self,
_request: Request<FlightDescriptor>,
) -> Result<Response<FlightInfo>, Status> {
Ok(Response::new(FlightInfo {
schema: Bytes::new(),
flight_descriptor: None,
endpoint: vec![],
total_records: -1,
total_bytes: -1,
app_metadata: Bytes::new(),
ordered: false,
}))
}
async fn poll_flight_info(
&self,
_request: Request<FlightDescriptor>,
) -> Result<Response<PollInfo>, Status> {
Err(Status::unimplemented("poll_flight_info not implemented"))
}
async fn do_put(
&self,
_request: Request<Streaming<FlightData>>,
) -> Result<Response<Self::DoPutStream>, Status> {
// Implementation here
todo!()
}
async fn do_action(
&self,
_request: Request<Action>,
) -> Result<Response<Self::DoActionStream>, Status> {
// Implementation here
todo!()
}
async fn list_actions(
&self,
_request: Request<Empty>,
) -> Result<Response<Self::ListActionsStream>, Status> {
// Implementation here
todo!()
}
async fn do_exchange(
&self,
_request: Request<Streaming<FlightData>>,
) -> Result<Response<Self::DoExchangeStream>, Status> {
// Implementation here
todo!()
}
}
+870
View File
@@ -0,0 +1,870 @@
//! ADBC (Arrow Database Connectivity) storage backend implementation.
//!
//! This module provides a storage backend using ADBC, enabling:
//! - Connection to any ADBC-compliant database
//! - High-performance data transport using Arrow's columnar format
//! - Connection pooling and prepared statements
//! - Support for various database systems (PostgreSQL, MySQL, etc.)
//!
//! # Configuration
//!
//! The ADBC backend can be configured using the following options:
//!
//! ```toml
//! [engine]
//! engine = "adbc"
//! # Base connection without credentials
//! connection = "postgresql://localhost:5432/metrics"
//! options = {
//! driver_path = "/usr/local/lib/libadbc_driver_postgresql.so", # Required: Path to ADBC driver
//! pool_max = "10", # Optional: Maximum pool connections
//! pool_min = "1", # Optional: Minimum pool connections
//! connect_timeout = "30" # Optional: Connection timeout in seconds
//! }
//! ```
//!
//! For security, credentials should be provided via environment variables:
//! ```bash
//! export HYPRSTREAM_DB_USERNAME=postgres
//! export HYPRSTREAM_DB_PASSWORD=secret
//! ```
//!
//! Or via command line:
//!
//! ```bash
//! hyprstream \
//! --engine adbc \
//! --engine-connection "postgresql://localhost:5432/metrics" \
//! --engine-options driver_path=/usr/local/lib/libadbc_driver_postgresql.so \
//! --engine-options pool_max=10
//! ```
//!
//! The implementation is optimized for efficient data transfer and
//! query execution using Arrow's native formats.
use adbc_core::{
driver_manager::{ManagedConnection, ManagedDriver},
options::{AdbcVersion, OptionDatabase, OptionValue},
Connection, Database, Driver, Statement, Optionable,
};
use arrow_array::{
Array, Int8Array, Int16Array, Int32Array, Int64Array,
Float32Array, Float64Array, BooleanArray, StringArray,
BinaryArray, TimestampNanosecondArray,
};
use arrow_schema::{Schema, DataType, Field};
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::sync::Mutex;
use tonic::Status;
use crate::aggregation::{AggregateFunction, GroupBy, AggregateResult, build_aggregate_query};
use crate::storage::table_manager::{TableManager, AggregationView};
use crate::config::Credentials;
use crate::metrics::MetricRecord;
use crate::storage::StorageBackend;
use crate::storage::cache::{CacheManager, CacheEviction};
use arrow_array::ArrayRef;
use arrow_array::RecordBatch;
use crate::aggregation::TimeWindow;
use crate::storage::BatchAggregation;
use std::time::Duration;
use hex;
pub struct AdbcBackend {
conn: Arc<Mutex<ManagedConnection>>,
statement_counter: AtomicU64,
prepared_statements: Arc<Mutex<Vec<(u64, String)>>>,
cache_manager: CacheManager,
table_manager: TableManager,
}
#[async_trait]
impl CacheEviction for AdbcBackend {
async fn execute_eviction(&self, query: &str) -> Result<(), Status> {
let conn = self.conn.clone();
let query = query.to_string(); // Clone for background task
tokio::spawn(async move {
let mut conn_guard = conn.lock().await;
if let Err(e) = conn_guard.new_statement()
.and_then(|mut stmt| {
stmt.set_sql_query(&query)?;
stmt.execute_update()
}) {
eprintln!("Background eviction error: {}", e);
}
});
Ok(())
}
}
impl AdbcBackend {
pub fn new(driver_path: &str, connection: Option<&str>, credentials: Option<&Credentials>) -> Result<Self, Status> {
let mut driver = ManagedDriver::load_dynamic_from_filename(
driver_path,
None,
AdbcVersion::V100,
).map_err(|e| Status::internal(format!("Failed to load ADBC driver: {}", e)))?;
let mut database = driver.new_database()
.map_err(|e| Status::internal(format!("Failed to create database: {}", e)))?;
// Set connection string if provided
if let Some(conn_str) = connection {
database.set_option(OptionDatabase::Uri, OptionValue::String(conn_str.to_string()))
.map_err(|e| Status::internal(format!("Failed to set connection string: {}", e)))?;
}
// Set credentials if provided
if let Some(creds) = credentials {
database.set_option(OptionDatabase::Username, OptionValue::String(creds.username.clone()))
.map_err(|e| Status::internal(format!("Failed to set username: {}", e)))?;
database.set_option(OptionDatabase::Password, OptionValue::String(creds.password.clone()))
.map_err(|e| Status::internal(format!("Failed to set password: {}", e)))?;
}
let connection = database.new_connection()
.map_err(|e| Status::internal(format!("Failed to create connection: {}", e)))?;
Ok(Self {
conn: Arc::new(Mutex::new(connection)),
statement_counter: AtomicU64::new(0),
prepared_statements: Arc::new(Mutex::new(Vec::new())),
cache_manager: CacheManager::new(None), // Initialize without TTL
table_manager: TableManager::new(),
})
}
async fn get_connection(&self) -> Result<tokio::sync::MutexGuard<'_, ManagedConnection>, Status> {
Ok(self.conn.lock().await)
}
async fn execute_statement(&self, conn: &mut ManagedConnection, query: &str) -> Result<(), Status> {
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query(query)
.map_err(|e| Status::internal(format!("Failed to set query: {}", e)))?;
stmt.execute_update()
.map_err(|e| Status::internal(format!("Failed to execute statement: {}", e)))?;
Ok(())
}
async fn execute_query(&self, conn: &mut ManagedConnection, query: &str, params: Option<RecordBatch>) -> Result<Vec<MetricRecord>, Status> {
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query(query)
.map_err(|e| Status::internal(format!("Failed to set query: {}", e)))?;
if let Some(batch) = params {
// Create a new statement for binding parameters
let mut bind_stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create bind statement: {}", e)))?;
// Set the parameters using SQL directly
let mut param_values = Vec::new();
for i in 0..batch.num_rows() {
for j in 0..batch.num_columns() {
let col = batch.column(j);
match col.data_type() {
DataType::Int64 => {
let array = col.as_any().downcast_ref::<Int64Array>().unwrap();
param_values.push(array.value(i).to_string());
}
DataType::Float64 => {
let array = col.as_any().downcast_ref::<Float64Array>().unwrap();
param_values.push(array.value(i).to_string());
}
DataType::Utf8 => {
let array = col.as_any().downcast_ref::<StringArray>().unwrap();
param_values.push(format!("'{}'", array.value(i)));
}
_ => return Err(Status::internal("Unsupported parameter type")),
}
}
}
let params_sql = format!("VALUES ({})", param_values.join(", "));
bind_stmt.set_sql_query(&params_sql)
.map_err(|e| Status::internal(format!("Failed to set parameters: {}", e)))?;
let mut bind_result = bind_stmt.execute()
.map_err(|e| Status::internal(format!("Failed to execute parameter binding: {}", e)))?;
while let Some(batch_result) = bind_result.next() {
let _ = batch_result.map_err(|e| Status::internal(format!("Failed to bind parameters: {}", e)))?;
}
}
let mut reader = stmt.execute()
.map_err(|e| Status::internal(format!("Failed to execute query: {}", e)))?;
let mut metrics = Vec::new();
while let Some(batch_result) = reader.next() {
let batch = batch_result.map_err(|e| Status::internal(format!("Failed to get next batch: {}", e)))?;
let metric_ids = batch.column_by_name("metric_id")
.and_then(|col| col.as_any().downcast_ref::<StringArray>())
.ok_or_else(|| Status::internal("Invalid metric_id column"))?;
let timestamps = batch.column_by_name("timestamp")
.and_then(|col| col.as_any().downcast_ref::<Int64Array>())
.ok_or_else(|| Status::internal("Invalid timestamp column"))?;
let sums = batch.column_by_name("value_running_window_sum")
.and_then(|col| col.as_any().downcast_ref::<Float64Array>())
.ok_or_else(|| Status::internal("Invalid value_running_window_sum column"))?;
let avgs = batch.column_by_name("value_running_window_avg")
.and_then(|col| col.as_any().downcast_ref::<Float64Array>())
.ok_or_else(|| Status::internal("Invalid value_running_window_avg column"))?;
let counts = batch.column_by_name("value_running_window_count")
.and_then(|col| col.as_any().downcast_ref::<Int64Array>())
.ok_or_else(|| Status::internal("Invalid value_running_window_count column"))?;
for i in 0..batch.num_rows() {
metrics.push(MetricRecord {
metric_id: metric_ids.value(i).to_string(),
timestamp: timestamps.value(i),
value_running_window_sum: sums.value(i),
value_running_window_avg: avgs.value(i),
value_running_window_count: counts.value(i),
});
}
}
Ok(metrics)
}
fn prepare_timestamp_param(timestamp: i64) -> Result<RecordBatch, Status> {
let schema = Arc::new(Schema::new(vec![
Field::new("timestamp", DataType::Int64, false),
]));
let timestamps: ArrayRef = Arc::new(Int64Array::from(vec![timestamp]));
RecordBatch::try_new(schema, vec![timestamps])
.map_err(|e| Status::internal(format!("Failed to create parameter batch: {}", e)))
}
fn prepare_params(metrics: &[MetricRecord]) -> Result<RecordBatch, Status> {
let schema = Arc::new(Schema::new(vec![
Field::new("metric_id", DataType::Utf8, false),
Field::new("timestamp", DataType::Int64, false),
Field::new("value_running_window_sum", DataType::Float64, false),
Field::new("value_running_window_avg", DataType::Float64, false),
Field::new("value_running_window_count", DataType::Int64, false),
]));
let metric_ids = StringArray::from_iter_values(metrics.iter().map(|m| m.metric_id.as_str()));
let timestamps = Int64Array::from_iter_values(metrics.iter().map(|m| m.timestamp));
let sums = Float64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_sum));
let avgs = Float64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_avg));
let counts = Int64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_count));
let arrays: Vec<ArrayRef> = vec![
Arc::new(metric_ids),
Arc::new(timestamps),
Arc::new(sums),
Arc::new(avgs),
Arc::new(counts),
];
RecordBatch::try_new(schema, arrays)
.map_err(|e| Status::internal(format!("Failed to create parameter batch: {}", e)))
}
/// Inserts a batch of metrics with optimized aggregation updates.
async fn insert_batch_optimized(&self, metrics: &[MetricRecord], _window: TimeWindow) -> Result<(), Status> {
// Begin transaction
self.begin_transaction().await?;
let mut conn = self.conn.lock().await;
// Insert metrics
let batch = Self::prepare_params(metrics)?;
let sql = self.build_insert_sql("metrics", &batch);
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query(&sql)
.map_err(|e| Status::internal(format!("Failed to set query: {}", e)))?;
// Bind parameters
let mut bind_stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create bind statement: {}", e)))?;
let mut param_values = Vec::new();
for i in 0..batch.num_rows() {
for j in 0..batch.num_columns() {
let col = batch.column(j);
match col.data_type() {
DataType::Int64 => {
let array = col.as_any().downcast_ref::<Int64Array>().unwrap();
param_values.push(array.value(i).to_string());
}
DataType::Float64 => {
let array = col.as_any().downcast_ref::<Float64Array>().unwrap();
param_values.push(array.value(i).to_string());
}
DataType::Utf8 => {
let array = col.as_any().downcast_ref::<StringArray>().unwrap();
param_values.push(format!("'{}'", array.value(i)));
}
_ => return Err(Status::internal("Unsupported parameter type")),
}
}
}
let params_sql = format!("VALUES ({})", param_values.join(", "));
bind_stmt.set_sql_query(&params_sql)
.map_err(|e| Status::internal(format!("Failed to set parameters: {}", e)))?;
let mut bind_result = bind_stmt.execute()
.map_err(|e| Status::internal(format!("Failed to execute parameter binding: {}", e)))?;
while let Some(batch_result) = bind_result.next() {
let _ = batch_result.map_err(|e| Status::internal(format!("Failed to bind parameters: {}", e)))?;
}
stmt.execute_update()
.map_err(|e| Status::internal(format!("Failed to insert metrics: {}", e)))?;
// Commit transaction
self.commit_transaction().await?;
Ok(())
}
/// Prepares parameters for aggregation insertion
fn prepare_aggregation_params(agg: &BatchAggregation) -> Result<RecordBatch, Status> {
let schema = Arc::new(Schema::new(vec![
Field::new("metric_id", DataType::Utf8, false),
Field::new("window_start", DataType::Int64, false),
Field::new("window_end", DataType::Int64, false),
Field::new("running_sum", DataType::Float64, false),
Field::new("running_count", DataType::Int64, false),
Field::new("min_value", DataType::Float64, false),
Field::new("max_value", DataType::Float64, false),
]));
let arrays: Vec<ArrayRef> = vec![
Arc::new(StringArray::from(vec![agg.metric_id.as_str()])),
Arc::new(Int64Array::from(vec![agg.window_start])),
Arc::new(Int64Array::from(vec![agg.window_end])),
Arc::new(Float64Array::from(vec![agg.running_sum])),
Arc::new(Int64Array::from(vec![agg.running_count])),
Arc::new(Float64Array::from(vec![agg.min_value])),
Arc::new(Float64Array::from(vec![agg.max_value])),
];
RecordBatch::try_new(schema, arrays)
.map_err(|e| Status::internal(format!("Failed to create aggregation batch: {}", e)))
}
async fn begin_transaction(&self) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query("BEGIN")
.map_err(|e| Status::internal(format!("Failed to set SQL query: {}", e)))?;
stmt.execute_update()
.map_err(|e| Status::internal(format!("Failed to execute query: {}", e)))?;
Ok(())
}
async fn commit_transaction(&self) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query("COMMIT")
.map_err(|e| Status::internal(format!("Failed to set SQL query: {}", e)))?;
stmt.execute_update()
.map_err(|e| Status::internal(format!("Failed to execute query: {}", e)))?;
Ok(())
}
async fn rollback_transaction(&self, conn: &mut ManagedConnection) -> Result<(), Status> {
self.execute_statement(conn, "ROLLBACK").await
}
async fn create_table(&self, table_name: &str, schema: &Schema) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let sql = self.build_create_table_sql(table_name, schema);
self.execute_statement(&mut conn, &sql).await
}
async fn create_view(&self, view: &AggregationView, sql: &str) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let create_view_sql = format!("CREATE VIEW {} AS {}", view.source_table, sql);
self.execute_statement(&mut conn, &create_view_sql).await
}
async fn drop_table(&self, table_name: &str) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let sql = format!("DROP TABLE IF EXISTS {}", table_name);
self.execute_statement(&mut conn, &sql).await
}
async fn drop_view(&self, view_name: &str) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let sql = format!("DROP VIEW IF EXISTS {}", view_name);
self.execute_statement(&mut conn, &sql).await
}
fn build_create_table_sql(&self, table_name: &str, schema: &Schema) -> String {
let mut sql = format!("CREATE TABLE IF NOT EXISTS {} (", table_name);
let mut first = true;
for field in schema.fields() {
if !first {
sql.push_str(", ");
}
first = false;
sql.push_str(&format!("{} {}", field.name(), self.arrow_type_to_sql_type(field.data_type())));
}
sql.push_str(")");
sql
}
fn build_insert_sql(&self, table_name: &str, batch: &RecordBatch) -> String {
let mut sql = format!("INSERT INTO {} (", table_name);
let mut first = true;
for field in batch.schema().fields() {
if !first {
sql.push_str(", ");
}
first = false;
sql.push_str(field.name());
}
sql.push_str(") VALUES (");
first = true;
for _i in 0..batch.num_columns() {
if !first {
sql.push_str(", ");
}
first = false;
sql.push('?');
}
sql.push(')');
sql
}
fn arrow_type_to_sql_type(&self, data_type: &DataType) -> &'static str {
match data_type {
DataType::Boolean => "BOOLEAN",
DataType::Int8 => "TINYINT",
DataType::Int16 => "SMALLINT",
DataType::Int32 => "INTEGER",
DataType::Int64 => "BIGINT",
DataType::UInt8 => "TINYINT UNSIGNED",
DataType::UInt16 => "SMALLINT UNSIGNED",
DataType::UInt32 => "INTEGER UNSIGNED",
DataType::UInt64 => "BIGINT UNSIGNED",
DataType::Float32 => "FLOAT",
DataType::Float64 => "DOUBLE",
DataType::Utf8 => "VARCHAR",
DataType::Binary => "BLOB",
DataType::Date32 => "DATE",
DataType::Date64 => "DATE",
DataType::Time32(_) => "TIME",
DataType::Time64(_) => "TIME",
DataType::Timestamp(_, _) => "TIMESTAMP",
_ => "VARCHAR",
}
}
}
#[async_trait]
impl StorageBackend for AdbcBackend {
async fn init(&self) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
// Create metrics table
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query(r#"
CREATE TABLE IF NOT EXISTS metrics (
metric_id VARCHAR NOT NULL,
timestamp BIGINT NOT NULL,
value_running_window_sum DOUBLE PRECISION NOT NULL,
value_running_window_avg DOUBLE PRECISION NOT NULL,
value_running_window_count BIGINT NOT NULL,
PRIMARY KEY (metric_id, timestamp)
);
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
CREATE TABLE IF NOT EXISTS metric_aggregations (
metric_id VARCHAR NOT NULL,
window_start BIGINT NOT NULL,
window_end BIGINT NOT NULL,
running_sum DOUBLE PRECISION NOT NULL,
running_count BIGINT NOT NULL,
min_value DOUBLE PRECISION NOT NULL,
max_value DOUBLE PRECISION NOT NULL,
PRIMARY KEY (metric_id, window_start, window_end)
);
CREATE INDEX IF NOT EXISTS idx_aggregations_window
ON metric_aggregations(window_start, window_end);
"#).map_err(|e| Status::internal(format!("Failed to set query: {}", e)))?;
stmt.execute_update()
.map_err(|e| Status::internal(format!("Failed to create tables: {}", e)))?;
Ok(())
}
async fn insert_metrics(&self, metrics: Vec<MetricRecord>) -> Result<(), Status> {
if metrics.is_empty() {
return Ok(());
}
// Check if eviction is needed
if let Some(cutoff) = self.cache_manager.should_evict().await? {
let query = self.cache_manager.eviction_query(cutoff);
self.execute_eviction(&query).await?;
}
// Use sliding window for batch-level aggregations
let window = TimeWindow::Sliding {
window: Duration::from_secs(3600), // 1 hour window
slide: Duration::from_secs(60), // 1 minute slide
};
// Use optimized batch insertion
self.insert_batch_optimized(&metrics, window).await
}
async fn query_metrics(&self, from_timestamp: i64) -> Result<Vec<MetricRecord>, Status> {
// Check if eviction is needed
if let Some(cutoff) = self.cache_manager.should_evict().await? {
let query = self.cache_manager.eviction_query(cutoff);
self.execute_eviction(&query).await?;
}
let mut conn = self.conn.lock().await;
let query = r#"
SELECT
metric_id,
timestamp,
value_running_window_sum,
value_running_window_avg,
value_running_window_count
FROM metrics
WHERE timestamp >= ?
ORDER BY timestamp ASC
"#;
let params = Self::prepare_timestamp_param(from_timestamp)?;
self.execute_query(&mut conn, query, Some(params)).await
}
async fn prepare_sql(&self, query: &str) -> Result<Vec<u8>, Status> {
let handle = self.statement_counter.fetch_add(1, Ordering::SeqCst);
let mut statements = self.prepared_statements.lock().await;
statements.push((handle, query.to_string()));
Ok(handle.to_le_bytes().to_vec())
}
async fn query_sql(&self, statement_handle: &[u8]) -> Result<Vec<MetricRecord>, Status> {
let handle = u64::from_le_bytes(
statement_handle.try_into()
.map_err(|_| Status::invalid_argument("Invalid statement handle"))?
);
let statements = self.prepared_statements.lock().await;
let sql = statements
.iter()
.find(|(h, _)| *h == handle)
.map(|(_, sql)| sql.as_str())
.ok_or_else(|| Status::invalid_argument("Statement handle not found"))?;
let mut conn = self.conn.lock().await;
self.execute_query(&mut conn, sql, None).await
}
async fn aggregate_metrics(
&self,
function: AggregateFunction,
group_by: &GroupBy,
from_timestamp: i64,
to_timestamp: Option<i64>,
) -> Result<Vec<AggregateResult>, Status> {
// Check if eviction is needed
if let Some(cutoff) = self.cache_manager.should_evict().await? {
let query = self.cache_manager.eviction_query(cutoff);
self.execute_eviction(&query).await?;
}
const DEFAULT_COLUMNS: [&str; 5] = [
"metric_id",
"timestamp",
"value_running_window_sum",
"value_running_window_avg",
"value_running_window_count"
];
let query = build_aggregate_query(
"metrics",
function,
group_by,
&DEFAULT_COLUMNS,
Some(from_timestamp),
to_timestamp,
);
let mut conn = self.conn.lock().await;
let metrics = self.execute_query(&mut conn, &query, None).await?;
let mut results = Vec::new();
for metric in metrics {
let result = AggregateResult {
value: metric.value_running_window_sum,
timestamp: metric.timestamp,
// Add any other fields required by AggregateResult
};
results.push(result);
}
Ok(results)
}
fn new_with_options(
connection_string: &str,
options: &HashMap<String, String>,
credentials: Option<&Credentials>,
) -> Result<Self, Status> {
let driver_path = options.get("driver_path")
.ok_or_else(|| Status::invalid_argument("driver_path is required"))?;
let mut driver = ManagedDriver::load_dynamic_from_filename(
driver_path,
None,
AdbcVersion::V100,
).map_err(|e| Status::internal(format!("Failed to load ADBC driver: {}", e)))?;
let mut database = driver.new_database()
.map_err(|e| Status::internal(format!("Failed to create database: {}", e)))?;
// Set connection string
database.set_option(OptionDatabase::Uri, OptionValue::String(connection_string.to_string()))
.map_err(|e| Status::internal(format!("Failed to set connection string: {}", e)))?;
// Set credentials if provided
if let Some(creds) = credentials {
database.set_option(OptionDatabase::Username, OptionValue::String(creds.username.clone()))
.map_err(|e| Status::internal(format!("Failed to set username: {}", e)))?;
database.set_option(OptionDatabase::Password, OptionValue::String(creds.password.clone()))
.map_err(|e| Status::internal(format!("Failed to set password: {}", e)))?;
}
let connection = database.new_connection()
.map_err(|e| Status::internal(format!("Failed to create connection: {}", e)))?;
Ok(Self {
conn: Arc::new(Mutex::new(connection)),
statement_counter: AtomicU64::new(0),
prepared_statements: Arc::new(Mutex::new(Vec::new())),
cache_manager: CacheManager::new(None), // Initialize without TTL
table_manager: TableManager::new(),
})
}
async fn create_table(&self, table_name: &str, schema: &Schema) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let sql = self.build_create_table_sql(table_name, schema);
self.execute_statement(&mut conn, &sql).await
}
async fn insert_into_table(&self, table_name: &str, batch: RecordBatch) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let sql = self.build_insert_sql(table_name, &batch);
self.execute_statement(&mut conn, &sql).await
}
async fn query_table(&self, table_name: &str, projection: Option<Vec<String>>) -> Result<RecordBatch, Status> {
let mut conn = self.conn.lock().await;
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
let columns = projection.map(|cols| cols.join(", ")).unwrap_or_else(|| "*".to_string());
let sql = format!("SELECT {} FROM {}", columns, table_name);
stmt.set_sql_query(&sql)
.map_err(|e| Status::internal(format!("Failed to set query: {}", e)))?;
let mut reader = stmt.execute()
.map_err(|e| Status::internal(format!("Failed to execute query: {}", e)))?;
let batch = reader.next()
.ok_or_else(|| Status::internal("No data returned"))?
.map_err(|e| Status::internal(format!("Failed to read record batch: {}", e)))?;
// Convert ADBC RecordBatch to Arrow RecordBatch
let schema = batch.schema();
let mut arrays = Vec::with_capacity(batch.num_columns());
for i in 0..batch.num_columns() {
let col = batch.column(i);
let array: ArrayRef = match col.data_type() {
&duckdb::arrow::datatypes::DataType::Int64 => {
Arc::new(col.as_any().downcast_ref::<Int64Array>().unwrap().clone())
},
&duckdb::arrow::datatypes::DataType::Float64 => {
Arc::new(col.as_any().downcast_ref::<Float64Array>().unwrap().clone())
},
&duckdb::arrow::datatypes::DataType::Utf8 => {
Arc::new(col.as_any().downcast_ref::<StringArray>().unwrap().clone())
},
_ => return Err(Status::internal("Unsupported column type")),
};
arrays.push(array);
}
// Convert DuckDB schema to Arrow schema
let fields: Vec<Field> = schema.fields().iter().map(|f| {
Field::new(
f.name(),
match f.data_type() {
&duckdb::arrow::datatypes::DataType::Int64 => DataType::Int64,
&duckdb::arrow::datatypes::DataType::Float64 => DataType::Float64,
&duckdb::arrow::datatypes::DataType::Utf8 => DataType::Utf8,
_ => DataType::Utf8, // Default to string for unsupported types
},
f.is_nullable()
)
}).collect();
let arrow_schema = Schema::new(fields);
RecordBatch::try_new(Arc::new(arrow_schema), arrays)
.map_err(|e| Status::internal(format!("Failed to create record batch: {}", e)))
}
async fn create_aggregation_view(&self, view: &AggregationView) -> Result<(), Status> {
let columns: Vec<&str> = view.aggregate_columns.iter()
.map(|s| s.as_str())
.collect();
let sql = build_aggregate_query(
&view.source_table,
view.function,
&view.group_by,
&columns,
None,
None
);
let mut conn = self.conn.lock().await;
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query(&format!("CREATE VIEW {} AS {}", view.source_table, sql))
.map_err(|e| Status::internal(format!("Failed to set SQL query: {}", e)))?;
stmt.execute_update()
.map_err(|e| Status::internal(format!("Failed to execute query: {}", e)))?;
Ok(())
}
async fn query_aggregation_view(&self, view_name: &str) -> Result<RecordBatch, Status> {
let sql = format!("SELECT * FROM {}", view_name);
let mut conn = self.conn.lock().await;
let mut stmt = conn.new_statement()
.map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query(&sql)
.map_err(|e| Status::internal(format!("Failed to set SQL query: {}", e)))?;
let mut reader = stmt.execute()
.map_err(|e| Status::internal(format!("Failed to execute query: {}", e)))?;
let batch = reader.next()
.ok_or_else(|| Status::internal("No data returned"))?
.map_err(|e| Status::internal(format!("Failed to read record batch: {}", e)))?;
// Convert DuckDB RecordBatch to Arrow RecordBatch
let schema = batch.schema();
let mut arrays = Vec::with_capacity(batch.num_columns());
for i in 0..batch.num_columns() {
let col = batch.column(i);
let array: ArrayRef = match col.data_type() {
&duckdb::arrow::datatypes::DataType::Int64 => {
Arc::new(col.as_any().downcast_ref::<Int64Array>().unwrap().clone())
},
&duckdb::arrow::datatypes::DataType::Float64 => {
Arc::new(col.as_any().downcast_ref::<Float64Array>().unwrap().clone())
},
&duckdb::arrow::datatypes::DataType::Utf8 => {
Arc::new(col.as_any().downcast_ref::<StringArray>().unwrap().clone())
},
_ => return Err(Status::internal("Unsupported column type")),
};
arrays.push(array);
}
// Convert DuckDB schema to Arrow schema
let fields: Vec<Field> = schema.fields().iter().map(|f| {
Field::new(
f.name(),
match f.data_type() {
&duckdb::arrow::datatypes::DataType::Int64 => DataType::Int64,
&duckdb::arrow::datatypes::DataType::Float64 => DataType::Float64,
&duckdb::arrow::datatypes::DataType::Utf8 => DataType::Utf8,
_ => DataType::Utf8, // Default to string for unsupported types
},
f.is_nullable()
)
}).collect();
let arrow_schema = Schema::new(fields);
RecordBatch::try_new(Arc::new(arrow_schema), arrays)
.map_err(|e| Status::internal(format!("Failed to create record batch: {}", e)))
}
async fn drop_table(&self, table_name: &str) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let mut stmt = conn.new_statement().map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query(&format!("DROP TABLE IF EXISTS {}", table_name))
.map_err(|e| Status::internal(format!("Failed to set SQL query: {}", e)))?;
stmt.execute_update().map_err(|e| Status::internal(format!("Failed to drop table: {}", e)))?;
Ok(())
}
async fn drop_aggregation_view(&self, view_name: &str) -> Result<(), Status> {
let mut conn = self.conn.lock().await;
let mut stmt = conn.new_statement().map_err(|e| Status::internal(format!("Failed to create statement: {}", e)))?;
stmt.set_sql_query(&format!("DROP VIEW IF EXISTS {}", view_name))
.map_err(|e| Status::internal(format!("Failed to set SQL query: {}", e)))?;
stmt.execute_update().map_err(|e| Status::internal(format!("Failed to drop view: {}", e)))?;
Ok(())
}
fn table_manager(&self) -> &TableManager {
&self.table_manager
}
}
+78
View File
@@ -0,0 +1,78 @@
use std::sync::Arc;
use std::time::{SystemTime, Duration, UNIX_EPOCH};
use tokio::sync::RwLock;
use tonic::Status;
/// Shared cache eviction manager that can be used across different storage backends.
#[derive(Clone)]
pub struct CacheManager {
ttl: Option<u64>,
last_eviction: Arc<RwLock<SystemTime>>,
min_eviction_interval: Duration,
}
impl CacheManager {
/// Creates a new cache manager with the specified TTL.
pub fn new(ttl: Option<u64>) -> Self {
Self {
ttl,
last_eviction: Arc::new(RwLock::new(SystemTime::now())),
min_eviction_interval: Duration::from_secs(60), // Default 60s between evictions
}
}
/// Sets a custom minimum interval between evictions.
pub fn set_min_eviction_interval(&mut self, interval: Duration) {
self.min_eviction_interval = interval;
}
/// Checks if eviction should be performed based on TTL and rate limiting.
/// Returns the cutoff timestamp if eviction should proceed, None otherwise.
pub async fn should_evict(&self) -> Result<Option<i64>, Status> {
match self.ttl {
None | Some(0) => Ok(None), // No TTL or TTL=0 means no eviction
Some(ttl) => {
let now = SystemTime::now();
let last = *self.last_eviction.read().await;
// Check if enough time has passed since last eviction
if now.duration_since(last).unwrap_or(Duration::from_secs(0)) < self.min_eviction_interval {
return Ok(None);
}
// Calculate cutoff timestamp
let cutoff = now
.duration_since(UNIX_EPOCH)
.map_err(|e| Status::internal(e.to_string()))?
.as_secs() as i64
- ttl as i64;
// Update last eviction time
*self.last_eviction.write().await = now;
Ok(Some(cutoff))
}
}
}
/// Generates an optimized SQL query for evicting expired entries.
pub fn eviction_query(&self, cutoff: i64) -> String {
format!(
"DELETE FROM metrics USING (
SELECT timestamp
FROM metrics
WHERE timestamp < {}
LIMIT 10000
) as expired
WHERE metrics.timestamp = expired.timestamp",
cutoff
)
}
}
/// Trait for storage backends that support cache eviction.
#[async_trait::async_trait]
pub trait CacheEviction {
/// Executes the eviction query in the background.
async fn execute_eviction(&self, query: &str) -> Result<(), Status>;
}
+245
View File
@@ -0,0 +1,245 @@
//! Two-tier cached storage backend implementation.
//!
//! This module provides a caching layer on top of any storage backend:
//! - Fast access to frequently used data
//! - Write-through caching for consistency
//! - Configurable cache duration
//! - Support for any StorageBackend as cache or store
//!
//! # Configuration
//!
//! The cached backend can be configured using the following options:
//!
//! ```toml
//! # Primary storage configuration
//! [engine]
//! engine = "adbc"
//! connection = "postgresql://localhost:5432"
//! options = {
//! driver_path = "/usr/local/lib/libadbc_driver_postgresql.so",
//! username = "postgres",
//! database = "metrics"
//! }
//!
//! # Cache configuration
//! [cache]
//! enabled = true
//! engine = "duckdb"
//! connection = ":memory:"
//! max_duration_secs = 3600
//! options = {
//! threads = "2"
//! }
//! ```
//!
//! Or via command line:
//!
//! ```bash
//! hyprstream \
//! --engine adbc \
//! --engine-connection "postgresql://localhost:5432" \
//! --engine-options driver_path=/usr/local/lib/libadbc_driver_postgresql.so \
//! --engine-options username=postgres \
//! --enable-cache \
//! --cache-engine duckdb \
//! --cache-connection ":memory:" \
//! --cache-options threads=2 \
//! --cache-max-duration 3600
//! ```
//!
//! The implementation follows standard caching patterns while ensuring
//! data consistency between cache and backing store.
use crate::config::Credentials;
use crate::metrics::MetricRecord;
use crate::storage::{StorageBackend, adbc::AdbcBackend, duckdb::DuckDbBackend};
use std::sync::Arc;
use std::collections::HashMap;
use tonic::Status;
/// Two-tier storage backend with caching support.
///
/// This backend provides:
/// - Fast access to recent data through caching
/// - Write-through caching for data consistency
/// - Configurable cache duration
/// - Support for any StorageBackend implementation
///
/// The implementation uses two storage backends:
/// 1. A fast cache (e.g., in-memory DuckDB)
/// 2. A persistent store (e.g., PostgreSQL via ADBC)
pub struct CachedStorageBackend {
/// Fast storage backend for caching
cache: Arc<dyn StorageBackend>,
/// Persistent storage backend for data
store: Arc<dyn StorageBackend>,
/// Maximum cache entry lifetime in seconds
max_duration_secs: u64,
}
impl CachedStorageBackend {
/// Creates a new cached storage backend.
///
/// This method sets up a two-tier storage system with:
/// - A fast cache layer for frequent access
/// - A persistent backing store
/// - Configurable cache duration
///
/// # Arguments
///
/// * `cache` - Fast storage backend for caching
/// * `store` - Persistent storage backend
/// * `max_duration_secs` - Maximum cache entry lifetime in seconds
pub fn new(
cache: Arc<dyn StorageBackend>,
store: Arc<dyn StorageBackend>,
max_duration_secs: u64,
) -> Self {
Self {
cache,
store,
max_duration_secs,
}
}
}
#[async_trait::async_trait]
impl StorageBackend for CachedStorageBackend {
/// Initializes both cache and backing store.
///
/// This method ensures both storage layers are properly
/// initialized and ready for use.
async fn init(&self) -> Result<(), Status> {
// Initialize both cache and backing store
self.cache.init().await?;
self.store.init().await?;
Ok(())
}
/// Inserts metrics into both cache and backing store.
///
/// This method implements write-through caching:
/// 1. Writes to cache for fast access
/// 2. Writes to backing store for persistence
///
/// # Arguments
///
/// * `metrics` - Vector of MetricRecord instances to insert
async fn insert_metrics(&self, metrics: Vec<MetricRecord>) -> Result<(), Status> {
// Insert into both cache and backing store
self.cache.insert_metrics(metrics.clone()).await?;
self.store.insert_metrics(metrics).await?;
Ok(())
}
/// Queries metrics with caching support.
///
/// This method implements a cache-first query strategy:
/// 1. Attempts to read from cache
/// 2. On cache miss, reads from backing store
/// 3. Updates cache with results from backing store
///
/// # Arguments
///
/// * `from_timestamp` - Unix timestamp to query from
async fn query_metrics(&self, from_timestamp: i64) -> Result<Vec<MetricRecord>, Status> {
// Calculate cache cutoff time
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let cache_cutoff = now - self.max_duration_secs as i64;
// Only use cache for data within cache window
if from_timestamp >= cache_cutoff {
match self.cache.query_metrics(from_timestamp).await {
Ok(metrics) if !metrics.is_empty() => return Ok(metrics),
_ => {}
}
}
// Cache miss or data too old, query backing store
let metrics = self.store.query_metrics(from_timestamp).await?;
// Update cache with results if within cache window
if !metrics.is_empty() && from_timestamp >= cache_cutoff {
self.cache.insert_metrics(metrics.clone()).await?;
}
Ok(metrics)
}
/// Prepares a SQL statement on the backing store.
///
/// This method bypasses the cache and prepares statements
/// directly on the backing store, as prepared statements
/// are typically used for complex queries.
///
/// # Arguments
///
/// * `query` - SQL query to prepare
async fn prepare_sql(&self, query: &str) -> Result<Vec<u8>, Status> {
// Prepare on backing store only
self.store.prepare_sql(query).await
}
/// Executes a prepared SQL statement on the backing store.
///
/// This method bypasses the cache and executes statements
/// directly on the backing store, ensuring consistent results
/// for complex queries.
///
/// # Arguments
///
/// * `statement_handle` - Handle of the prepared statement
async fn query_sql(&self, statement_handle: &[u8]) -> Result<Vec<MetricRecord>, Status> {
// Execute on backing store only
self.store.query_sql(statement_handle).await
}
fn new_with_options(
connection_string: &str,
options: &HashMap<String, String>,
credentials: Option<&Credentials>,
) -> Result<Self, Status> {
// Parse cache duration from options
let max_duration_secs = options
.get("max_duration_secs")
.and_then(|s| s.parse().ok())
.unwrap_or(3600);
// Create cache backend
let default_engine = "duckdb".to_string();
let default_connection = ":memory:".to_string();
let cache_engine = options.get("cache_engine").unwrap_or(&default_engine);
let cache_connection = options.get("cache_connection").unwrap_or(&default_connection);
let cache_options: HashMap<String, String> = options
.iter()
.filter(|(k, _)| k.starts_with("cache_"))
.map(|(k, v)| (k[6..].to_string(), v.clone()))
.collect();
let cache: Arc<dyn StorageBackend> = match cache_engine.as_str() {
"duckdb" => Arc::new(DuckDbBackend::new_with_options(
cache_connection,
&cache_options,
None,
)?),
"adbc" => Arc::new(AdbcBackend::new_with_options(
cache_connection,
&cache_options,
None,
)?),
_ => return Err(Status::invalid_argument("Invalid cache engine type")),
};
// Create store backend
let store = Arc::new(AdbcBackend::new_with_options(
connection_string,
options,
credentials,
)?);
Ok(Self::new(cache, store, max_duration_secs))
}
}
+637
View File
@@ -0,0 +1,637 @@
//! DuckDB storage backend implementation.
//!
//! This module provides a high-performance storage backend using DuckDB,
//! an embedded analytical database. The implementation supports:
//! - In-memory and persistent storage options
//! - Efficient batch operations
//! - SQL query capabilities
//! - Time-based filtering
//!
//! # Configuration
//!
//! The DuckDB backend can be configured using the following options:
//!
//! ```toml
//! [engine]
//! engine = "duckdb"
//! connection = ":memory:" # Use ":memory:" for in-memory or file path
//! options = {
//! threads = "4", # Optional: Number of threads (default: 4)
//! read_only = "false" # Optional: Read-only mode (default: false)
//! }
//! ```
//!
//! Or via command line:
//!
//! ```bash
//! hyprstream \
//! --engine duckdb \
//! --engine-connection ":memory:" \
//! --engine-options threads=4 \
//! --engine-options read_only=false
//! ```
//!
//! DuckDB is particularly well-suited for analytics workloads and
//! provides excellent performance for both caching and primary storage.
use std::collections::HashMap;
use std::sync::Arc;
use duckdb::{Connection, Config, params, ToSql};
use tokio::sync::Mutex;
use tonic::Status;
use crate::metrics::MetricRecord;
use crate::config::Credentials;
use crate::storage::{StorageBackend, BatchAggregation};
use crate::storage::cache::{CacheManager, CacheEviction};
use crate::storage::table_manager::{TableManager, AggregationView};
use crate::aggregation::{TimeWindow, AggregateFunction, GroupBy, AggregateResult, build_aggregate_query};
use async_trait::async_trait;
use arrow::datatypes::{DataType, Field, Schema};
use arrow::array::{
Array, ArrayRef, RecordBatch, Int64Array, Float64Array, StringArray,
};
use arrow::array::builder::{
ArrayBuilder, Int64Builder, Float64Builder, StringBuilder,
};
use std::time::Duration;
/// DuckDB-based storage backend for metrics.
#[derive(Clone)]
pub struct DuckDbBackend {
conn: Arc<Mutex<Connection>>,
cache_manager: CacheManager,
table_manager: TableManager,
}
impl DuckDbBackend {
/// Creates a new DuckDB backend instance.
pub fn new(connection_string: String, _options: HashMap<String, String>, ttl: Option<u64>) -> Result<Self, Status> {
let config = Config::default();
let conn = Connection::open_with_flags(&connection_string, config)
.map_err(|e| Status::internal(e.to_string()))?;
let backend = Self {
conn: Arc::new(Mutex::new(conn)),
cache_manager: CacheManager::new(ttl),
table_manager: TableManager::new(),
};
// Initialize tables
let backend_clone = backend.clone();
tokio::spawn(async move {
if let Err(e) = backend_clone.init().await {
eprintln!("Failed to initialize tables: {}", e);
}
});
Ok(backend)
}
/// Creates a new DuckDB backend with an in-memory database.
pub fn new_in_memory() -> Result<Self, Status> {
Self::new(":memory:".to_string(), HashMap::new(), Some(0))
}
/// Inserts a batch of metrics with optimized aggregation updates.
async fn insert_batch_optimized(&self, metrics: &[MetricRecord], window: TimeWindow) -> Result<(), Status> {
let conn = self.conn.lock().await;
// Begin transaction
conn.execute("BEGIN TRANSACTION", params![])
.map_err(|e| Status::internal(format!("Failed to begin transaction: {}", e)))?;
// Convert metrics to RecordBatch for efficient insertion
let batch = Self::prepare_params(metrics)?;
// Insert metrics using prepared statement
let mut stmt = conn.prepare(r#"
INSERT INTO metrics (
metric_id,
timestamp,
value_running_window_sum,
value_running_window_avg,
value_running_window_count
) VALUES (?, ?, ?, ?, ?)
"#).map_err(|e| Status::internal(format!("Failed to prepare statement: {}", e)))?;
// Bind and execute in batches
for i in 0..batch.num_rows() {
let metric_id = batch.column(0).as_any().downcast_ref::<StringArray>().unwrap().value(i);
let timestamp = batch.column(1).as_any().downcast_ref::<Int64Array>().unwrap().value(i);
let sum = batch.column(2).as_any().downcast_ref::<Float64Array>().unwrap().value(i);
let avg = batch.column(3).as_any().downcast_ref::<Float64Array>().unwrap().value(i);
let count = batch.column(4).as_any().downcast_ref::<Int64Array>().unwrap().value(i);
stmt.execute(params![
metric_id,
timestamp,
sum,
avg,
count,
]).map_err(|e| Status::internal(format!("Failed to insert metrics: {}", e)))?;
}
// Update aggregations based on window
let window_start = match window {
TimeWindow::Sliding { window, slide: _ } => {
let now = metrics.iter().map(|m| m.timestamp).max().unwrap_or(0);
now - window.as_nanos() as i64
}
TimeWindow::Fixed(start) => start.as_nanos() as i64,
TimeWindow::None => metrics.iter().map(|m| m.timestamp).min().unwrap_or(0),
};
let window_end = match window {
TimeWindow::Sliding { window: _, slide: _ } => {
metrics.iter().map(|m| m.timestamp).max().unwrap_or(0)
}
TimeWindow::Fixed(end) => end.as_nanos() as i64,
TimeWindow::None => metrics.iter().map(|m| m.timestamp).max().unwrap_or(0),
};
// Group metrics by ID and calculate aggregations
let mut aggregations = HashMap::new();
for metric in metrics {
let entry = aggregations.entry(metric.metric_id.clone()).or_insert_with(|| BatchAggregation {
metric_id: metric.metric_id.clone(),
window_start,
window_end,
running_sum: 0.0,
running_count: 0,
min_value: f64::INFINITY,
max_value: f64::NEG_INFINITY,
});
entry.running_sum += metric.value_running_window_sum;
entry.running_count += metric.value_running_window_count as i64;
entry.min_value = entry.min_value.min(metric.value_running_window_sum);
entry.max_value = entry.max_value.max(metric.value_running_window_sum);
}
// Update aggregations table using prepared statement with proper type handling
let mut agg_stmt = conn.prepare(r#"
INSERT INTO metric_aggregations (
metric_id, window_start, window_end,
running_sum, running_count, min_value, max_value
) VALUES (?, ?, ?, ?, ?, ?, ?)
ON CONFLICT (metric_id, window_start, window_end) DO UPDATE
SET running_sum = metric_aggregations.running_sum + EXCLUDED.running_sum,
running_count = metric_aggregations.running_count + EXCLUDED.running_count,
min_value = LEAST(metric_aggregations.min_value, EXCLUDED.min_value),
max_value = GREATEST(metric_aggregations.max_value, EXCLUDED.max_value)
"#).map_err(|e| Status::internal(format!("Failed to prepare aggregation statement: {}", e)))?;
for agg in aggregations.values() {
agg_stmt.execute(params![
agg.metric_id,
agg.window_start,
agg.window_end,
agg.running_sum,
agg.running_count,
agg.min_value,
agg.max_value,
]).map_err(|e| Status::internal(format!("Failed to update aggregations: {}", e)))?;
}
// Commit transaction
conn.execute("COMMIT", params![])
.map_err(|e| Status::internal(format!("Failed to commit transaction: {}", e)))?;
Ok(())
}
/// Prepares parameters for batch insertion
fn prepare_params(metrics: &[MetricRecord]) -> Result<RecordBatch, Status> {
let schema = Arc::new(Schema::new(vec![
Field::new("metric_id", DataType::Utf8, false),
Field::new("timestamp", DataType::Int64, false),
Field::new("value_running_window_sum", DataType::Float64, false),
Field::new("value_running_window_avg", DataType::Float64, false),
Field::new("value_running_window_count", DataType::Int64, false),
]));
let metric_ids = StringArray::from_iter_values(metrics.iter().map(|m| m.metric_id.as_str()));
let timestamps = Int64Array::from_iter_values(metrics.iter().map(|m| m.timestamp));
let sums = Float64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_sum));
let avgs = Float64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_avg));
let counts = Int64Array::from_iter_values(metrics.iter().map(|m| m.value_running_window_count));
let arrays: Vec<ArrayRef> = vec![
Arc::new(metric_ids),
Arc::new(timestamps),
Arc::new(sums),
Arc::new(avgs),
Arc::new(counts),
];
RecordBatch::try_new(schema, arrays)
.map_err(|e| Status::internal(format!("Failed to create parameter batch: {}", e)))
}
}
#[async_trait]
impl CacheEviction for DuckDbBackend {
async fn execute_eviction(&self, query: &str) -> Result<(), Status> {
let conn = self.conn.clone();
let query = query.to_string();
tokio::spawn(async move {
let conn_guard = conn.lock().await;
if let Err(e) = conn_guard.execute_batch(&query) {
eprintln!("Background eviction error: {}", e);
}
});
Ok(())
}
}
#[async_trait]
impl StorageBackend for DuckDbBackend {
async fn init(&self) -> Result<(), Status> {
let conn = self.conn.lock().await;
// Create metrics table with optimized schema
conn.execute_batch(r#"
CREATE TABLE IF NOT EXISTS metrics (
metric_id VARCHAR NOT NULL,
timestamp BIGINT NOT NULL,
value_running_window_sum DOUBLE NOT NULL,
value_running_window_avg DOUBLE NOT NULL,
value_running_window_count BIGINT NOT NULL,
PRIMARY KEY (metric_id, timestamp)
);
CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics(timestamp);
CREATE TABLE IF NOT EXISTS metric_aggregations (
metric_id VARCHAR NOT NULL,
window_start BIGINT NOT NULL,
window_end BIGINT NOT NULL,
running_sum DOUBLE NOT NULL,
running_count BIGINT NOT NULL,
min_value DOUBLE NOT NULL,
max_value DOUBLE NOT NULL,
PRIMARY KEY (metric_id, window_start, window_end)
);
CREATE INDEX IF NOT EXISTS idx_aggregations_window
ON metric_aggregations(window_start, window_end);
"#).map_err(|e| Status::internal(format!("Failed to create tables: {}", e)))?;
Ok(())
}
async fn insert_metrics(&self, metrics: Vec<MetricRecord>) -> Result<(), Status> {
if metrics.is_empty() {
return Ok(());
}
// Check if eviction is needed
if let Some(cutoff) = self.cache_manager.should_evict().await? {
let query = self.cache_manager.eviction_query(cutoff);
self.execute_eviction(&query).await?;
}
// Use sliding window for batch-level aggregations
let window = TimeWindow::Sliding {
window: Duration::from_secs(3600), // 1 hour window
slide: Duration::from_secs(60), // 1 minute slide
};
// Use optimized batch insertion
self.insert_batch_optimized(&metrics, window).await
}
async fn query_metrics(&self, from_timestamp: i64) -> Result<Vec<MetricRecord>, Status> {
// Check if eviction is needed
if let Some(cutoff) = self.cache_manager.should_evict().await? {
let query = self.cache_manager.eviction_query(cutoff);
self.execute_eviction(&query).await?;
}
let query = format!(
"SELECT metric_id, timestamp, value_running_window_sum, value_running_window_avg, value_running_window_count \
FROM metrics WHERE timestamp >= {} ORDER BY timestamp ASC",
from_timestamp
);
let conn = self.conn.lock().await;
let mut stmt = conn.prepare(&query)
.map_err(|e| Status::internal(e.to_string()))?;
let mut rows = stmt.query(params![])
.map_err(|e| Status::internal(e.to_string()))?;
let mut metrics = Vec::new();
while let Some(row) = rows.next().map_err(|e| Status::internal(e.to_string()))? {
metrics.push(MetricRecord {
metric_id: row.get(0).map_err(|e| Status::internal(e.to_string()))?,
timestamp: row.get(1).map_err(|e| Status::internal(e.to_string()))?,
value_running_window_sum: row.get(2).map_err(|e| Status::internal(e.to_string()))?,
value_running_window_avg: row.get(3).map_err(|e| Status::internal(e.to_string()))?,
value_running_window_count: row.get(4).map_err(|e| Status::internal(e.to_string()))?,
});
}
Ok(metrics)
}
async fn prepare_sql(&self, query: &str) -> Result<Vec<u8>, Status> {
Ok(query.as_bytes().to_vec())
}
async fn query_sql(&self, statement_handle: &[u8]) -> Result<Vec<MetricRecord>, Status> {
let sql = std::str::from_utf8(statement_handle)
.map_err(|e| Status::internal(e.to_string()))?;
self.query_metrics(sql.parse().unwrap_or(0)).await
}
async fn aggregate_metrics(
&self,
function: AggregateFunction,
group_by: &GroupBy,
from_timestamp: i64,
to_timestamp: Option<i64>,
) -> Result<Vec<AggregateResult>, Status> {
// Check if eviction is needed
if let Some(cutoff) = self.cache_manager.should_evict().await? {
let query = self.cache_manager.eviction_query(cutoff);
self.execute_eviction(&query).await?;
}
let query = build_aggregate_query(
"metrics",
function,
group_by,
&["value_running_window_sum"],
Some(from_timestamp),
to_timestamp,
);
let conn = self.conn.lock().await;
let mut stmt = conn.prepare(&query)
.map_err(|e| Status::internal(e.to_string()))?;
let mut rows = stmt.query(params![])
.map_err(|e| Status::internal(e.to_string()))?;
let mut results = Vec::new();
while let Some(row) = rows.next().map_err(|e| Status::internal(e.to_string()))? {
let value: f64 = row.get(0).map_err(|e| Status::internal(e.to_string()))?;
let timestamp: i64 = row.get(1).map_err(|e| Status::internal(e.to_string()))?;
results.push(AggregateResult {
value,
timestamp,
});
}
Ok(results)
}
fn new_with_options(
connection_string: &str,
options: &HashMap<String, String>,
credentials: Option<&Credentials>,
) -> Result<Self, Status> {
let mut all_options = options.clone();
if let Some(creds) = credentials {
all_options.insert("username".to_string(), creds.username.clone());
all_options.insert("password".to_string(), creds.password.clone());
}
let ttl = all_options.get("ttl")
.and_then(|s| s.parse().ok())
.map(|ttl| if ttl == 0 { None } else { Some(ttl) })
.unwrap_or(None);
Self::new(connection_string.to_string(), all_options, ttl)
}
async fn create_table(&self, table_name: &str, schema: &Schema) -> Result<(), Status> {
// Create table in DuckDB
let sql = Self::schema_to_create_table_sql(table_name, schema);
self.execute(&sql).await?;
// Register table in manager
self.table_manager.create_table(table_name.to_string(), schema.clone()).await?;
Ok(())
}
async fn insert_into_table(&self, table_name: &str, batch: RecordBatch) -> Result<(), Status> {
let conn = self.conn.lock().await;
let mut stmt = conn.prepare(&format!("INSERT INTO {} VALUES ({})",
table_name,
(0..batch.num_columns()).map(|_| "?").collect::<Vec<_>>().join(", ")
)).map_err(|e| Status::internal(e.to_string()))?;
for row_idx in 0..batch.num_rows() {
let mut param_values: Vec<Box<dyn ToSql>> = Vec::new();
for col_idx in 0..batch.num_columns() {
let col = batch.column(col_idx);
match col.data_type() {
DataType::Int64 => {
let array = col.as_any().downcast_ref::<Int64Array>().unwrap();
param_values.push(Box::new(array.value(row_idx)));
}
DataType::Float64 => {
let array = col.as_any().downcast_ref::<Float64Array>().unwrap();
param_values.push(Box::new(array.value(row_idx)));
}
DataType::Utf8 => {
let array = col.as_any().downcast_ref::<StringArray>().unwrap();
param_values.push(Box::new(array.value(row_idx).to_string()));
}
_ => return Err(Status::internal("Unsupported column type")),
}
}
let param_refs: Vec<&dyn ToSql> = param_values.iter().map(|p| p.as_ref()).collect();
stmt.execute(param_refs.as_slice()).map_err(|e| Status::internal(e.to_string()))?;
}
Ok(())
}
async fn query_table(&self, table_name: &str, projection: Option<Vec<String>>) -> Result<RecordBatch, Status> {
let schema = self.table_manager.get_table_schema(table_name).await?;
let mut builders: Vec<Box<dyn ArrayBuilder>> = schema.fields().iter()
.map(|field| Self::create_array_builder(field))
.collect();
let projection = projection.unwrap_or_else(|| {
schema.fields().iter().map(|f| f.name().clone()).collect()
});
let sql = format!(
"SELECT {} FROM {}",
projection.join(", "),
table_name
);
let conn = self.conn.lock().await;
let mut stmt = conn.prepare(&sql)
.map_err(|e| Status::internal(e.to_string()))?;
let mut rows = stmt.query(params![])
.map_err(|e| Status::internal(e.to_string()))?;
while let Some(row) = rows.next().map_err(|e| Status::internal(e.to_string()))? {
for (i, field) in schema.fields().iter().enumerate() {
match field.data_type() {
DataType::Int64 => {
let builder = builders[i].as_any_mut().downcast_mut::<Int64Builder>().unwrap();
match row.get::<usize, i64>(i) {
Ok(value) => builder.append_value(value),
Err(_) => builder.append_null(),
}
}
DataType::Float64 => {
let builder = builders[i].as_any_mut().downcast_mut::<Float64Builder>().unwrap();
match row.get::<usize, f64>(i) {
Ok(value) => builder.append_value(value),
Err(_) => builder.append_null(),
}
}
DataType::Utf8 => {
let builder = builders[i].as_any_mut().downcast_mut::<StringBuilder>().unwrap();
match row.get::<usize, String>(i) {
Ok(value) => builder.append_value(value),
Err(_) => builder.append_null(),
}
}
_ => return Err(Status::internal("Unsupported column type")),
}
}
}
let arrays: Vec<ArrayRef> = builders.into_iter()
.map(|mut builder| Arc::new(builder.finish()) as ArrayRef)
.collect();
Ok(RecordBatch::try_new(Arc::new(schema), arrays)
.map_err(|e| Status::internal(format!("Failed to create record batch: {}", e)))?)
}
async fn create_aggregation_view(&self, view: &AggregationView) -> Result<(), Status> {
let columns: Vec<&str> = view.aggregate_columns.iter()
.map(|s| s.as_str())
.collect();
let sql = build_aggregate_query(
&view.source_table,
view.function,
&view.group_by,
&columns,
None,
None
);
let view_name = format!("agg_view_{}", view.source_table);
let conn = self.conn.lock().await;
conn.execute(&format!("CREATE VIEW {} AS {}", view_name, sql), params![])
.map_err(|e| Status::internal(format!("Failed to create view: {}", e)))?;
// Register view in manager
self.table_manager.create_aggregation_view(
view_name,
view.source_table.clone(),
view.function.clone(),
view.group_by.clone(),
view.window.clone(),
view.aggregate_columns.clone(),
).await?;
Ok(())
}
async fn query_aggregation_view(&self, view_name: &str) -> Result<RecordBatch, Status> {
self.query_table(view_name, None).await
}
async fn drop_table(&self, table_name: &str) -> Result<(), Status> {
let conn = self.conn.lock().await;
conn.execute(&format!("DROP TABLE IF EXISTS {}", table_name), params![])
.map_err(|e| Status::internal(format!("Failed to drop table: {}", e)))?;
self.table_manager.drop_table(table_name).await?;
Ok(())
}
async fn drop_aggregation_view(&self, view_name: &str) -> Result<(), Status> {
let conn = self.conn.lock().await;
conn.execute(&format!("DROP VIEW IF EXISTS {}", view_name), params![])
.map_err(|e| Status::internal(format!("Failed to drop view: {}", e)))?;
self.table_manager.drop_aggregation_view(view_name).await?;
Ok(())
}
fn table_manager(&self) -> &TableManager {
&self.table_manager
}
}
impl DuckDbBackend {
/// Executes a SQL query.
async fn execute(&self, query: &str) -> Result<(), Status> {
let conn = self.conn.lock().await;
conn.execute(query, params![])
.map_err(|e| Status::internal(e.to_string()))?;
Ok(())
}
/// Converts an Arrow schema to a DuckDB CREATE TABLE statement
fn schema_to_create_table_sql(table_name: &str, schema: &Schema) -> String {
let mut sql = format!("CREATE TABLE IF NOT EXISTS \"{}\" (", table_name);
let mut first = true;
for field in schema.fields() {
if !first {
sql.push_str(", ");
}
first = false;
sql.push_str(&format!("\"{}\" {}", field.name(), Self::arrow_type_to_duckdb_type(field.data_type())));
}
sql.push_str(")");
sql
}
/// Converts an Arrow data type to a DuckDB type string
fn arrow_type_to_duckdb_type(data_type: &DataType) -> &'static str {
match data_type {
DataType::Boolean => "BOOLEAN",
DataType::Int8 => "TINYINT",
DataType::Int16 => "SMALLINT",
DataType::Int32 => "INTEGER",
DataType::Int64 => "BIGINT",
DataType::UInt8 => "TINYINT",
DataType::UInt16 => "SMALLINT",
DataType::UInt32 => "INTEGER",
DataType::UInt64 => "BIGINT",
DataType::Float32 => "REAL",
DataType::Float64 => "DOUBLE",
DataType::Utf8 => "VARCHAR",
DataType::Binary => "BLOB",
DataType::Date32 => "DATE",
DataType::Date64 => "DATE",
DataType::Time32(_) => "TIME",
DataType::Time64(_) => "TIME",
DataType::Timestamp(_, _) => "TIMESTAMP",
_ => "VARCHAR", // Default to VARCHAR for unsupported types
}
}
fn create_array_builder(field: &Field) -> Box<dyn ArrayBuilder> {
match field.data_type() {
DataType::Int64 => Box::new(Int64Builder::new()),
DataType::Float64 => Box::new(Float64Builder::new()),
DataType::Utf8 => Box::new(StringBuilder::new()),
_ => panic!("Unsupported column type"),
}
}
}
+170
View File
@@ -0,0 +1,170 @@
//! Storage backends for metric data persistence and caching.
//!
//! This module provides multiple storage backend implementations:
//! - `duckdb`: High-performance embedded database for caching and local storage
//! - `adbc`: Arrow Database Connectivity for external database integration
//! - `cached`: Two-tier storage with configurable caching layer
//!
//! Each backend implements the `StorageBackend` trait, providing a consistent
//! interface for metric storage and retrieval operations.
pub mod adbc;
pub mod duckdb;
pub mod cache;
pub mod table_manager;
use crate::config::Credentials;
use crate::metrics::MetricRecord;
use crate::aggregation::{AggregateFunction, GroupBy, AggregateResult, TimeWindow};
use crate::storage::table_manager::{TableManager, AggregationView};
use async_trait::async_trait;
use std::collections::HashMap;
use tonic::Status;
use arrow_schema::Schema;
use arrow_array::RecordBatch;
/// Batch-level aggregation state for efficient updates
#[derive(Debug, Clone)]
pub struct BatchAggregation {
/// The metric ID this aggregation belongs to
pub metric_id: String,
/// Start of the time window
pub window_start: i64,
/// End of the time window
pub window_end: i64,
/// Running sum within the window
pub running_sum: f64,
/// Running count within the window
pub running_count: i64,
/// Minimum value in the window
pub min_value: f64,
/// Maximum value in the window
pub max_value: f64,
}
/// Storage backend trait for metric data persistence.
///
/// This trait defines the interface that all storage backends must implement.
/// It provides methods for:
/// - Initialization and configuration
/// - Metric data insertion
/// - Metric data querying
/// - SQL query preparation and execution
/// - Aggregation of metrics
/// - Table and view management
#[async_trait]
pub trait StorageBackend: Send + Sync + 'static {
/// Initialize the storage backend.
async fn init(&self) -> Result<(), Status>;
/// Insert metrics into storage.
async fn insert_metrics(&self, metrics: Vec<MetricRecord>) -> Result<(), Status>;
/// Query metrics from storage.
async fn query_metrics(&self, from_timestamp: i64) -> Result<Vec<MetricRecord>, Status>;
/// Prepare a SQL query and return a handle.
/// The handle is backend-specific and opaque to the caller.
async fn prepare_sql(&self, query: &str) -> Result<Vec<u8>, Status>;
/// Execute a prepared SQL query using its handle.
/// The handle must have been obtained from prepare_sql.
async fn query_sql(&self, statement_handle: &[u8]) -> Result<Vec<MetricRecord>, Status>;
/// Aggregate metrics using the specified function and grouping.
async fn aggregate_metrics(
&self,
function: AggregateFunction,
group_by: &GroupBy,
from_timestamp: i64,
to_timestamp: Option<i64>,
) -> Result<Vec<AggregateResult>, Status>;
/// Create a new instance with the given options.
/// The connection string and options are backend-specific.
fn new_with_options(
connection_string: &str,
options: &HashMap<String, String>,
credentials: Option<&Credentials>,
) -> Result<Self, Status>
where
Self: Sized;
/// Create a new table with the given schema
async fn create_table(&self, table_name: &str, schema: &Schema) -> Result<(), Status>;
/// Insert data into a table
async fn insert_into_table(&self, table_name: &str, batch: RecordBatch) -> Result<(), Status>;
/// Query data from a table
async fn query_table(&self, table_name: &str, projection: Option<Vec<String>>) -> Result<RecordBatch, Status>;
/// Create an aggregation view
async fn create_aggregation_view(&self, view: &AggregationView) -> Result<(), Status>;
/// Query data from an aggregation view
async fn query_aggregation_view(&self, view_name: &str) -> Result<RecordBatch, Status>;
/// Drop a table
async fn drop_table(&self, table_name: &str) -> Result<(), Status>;
/// Drop an aggregation view
async fn drop_aggregation_view(&self, view_name: &str) -> Result<(), Status>;
/// Get the table manager instance
fn table_manager(&self) -> &TableManager;
/// Update batch-level aggregations.
/// This is called during batch writes to maintain running aggregations.
async fn update_batch_aggregations(
&self,
batch: &[MetricRecord],
window: TimeWindow,
) -> Result<Vec<BatchAggregation>, Status> {
// Default implementation that processes the batch and updates aggregations
let mut aggregations = HashMap::new();
for metric in batch {
let (window_start, window_end) = window.window_bounds(metric.timestamp);
let key = (metric.metric_id.clone(), window_start, window_end);
let agg = aggregations.entry(key).or_insert_with(|| BatchAggregation {
metric_id: metric.metric_id.clone(),
window_start,
window_end,
running_sum: 0.0,
running_count: 0,
min_value: f64::INFINITY,
max_value: f64::NEG_INFINITY,
});
// Update running aggregations
agg.running_sum += metric.value_running_window_sum;
agg.running_count += 1;
agg.min_value = agg.min_value.min(metric.value_running_window_sum);
agg.max_value = agg.max_value.max(metric.value_running_window_sum);
}
Ok(aggregations.into_values().collect())
}
/// Insert batch-level aggregations.
/// This is called after update_batch_aggregations to persist the aggregations.
async fn insert_batch_aggregations(
&self,
aggregations: Vec<BatchAggregation>,
) -> Result<(), Status> {
// Default implementation that stores aggregations in a separate table
let mut batch = Vec::new();
for agg in aggregations {
batch.push(MetricRecord {
metric_id: agg.metric_id,
timestamp: agg.window_start,
value_running_window_sum: agg.running_sum,
value_running_window_avg: agg.running_sum / agg.running_count as f64,
value_running_window_count: agg.running_count,
});
}
self.insert_metrics(batch).await
}
}
@@ -0,0 +1,140 @@
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use arrow_schema::Schema;
use tonic::Status;
use serde::{Serialize, Deserialize};
use crate::aggregation::{TimeWindow, AggregateFunction, GroupBy};
/// Configuration for an aggregation view
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregationView {
pub source_table: String,
pub function: AggregateFunction,
pub group_by: GroupBy,
pub window: TimeWindow,
pub aggregate_columns: Vec<String>,
}
#[derive(Debug)]
pub struct TableManager {
tables: Arc<RwLock<HashMap<String, Schema>>>,
views: Arc<RwLock<HashMap<String, AggregationView>>>,
}
impl Clone for TableManager {
fn clone(&self) -> Self {
Self {
tables: self.tables.clone(),
views: self.views.clone(),
}
}
}
impl Default for TableManager {
fn default() -> Self {
Self::new()
}
}
impl TableManager {
pub fn new() -> Self {
Self {
tables: Arc::new(RwLock::new(HashMap::new())),
views: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn create_table(&self, name: String, schema: Schema) -> Result<(), Status> {
let mut tables = self.tables.write().await;
if tables.contains_key(&name) {
return Err(Status::already_exists(format!("Table {} already exists", name)));
}
tables.insert(name, schema);
Ok(())
}
pub async fn get_table_schema(&self, name: &str) -> Result<Schema, Status> {
let tables = self.tables.read().await;
tables.get(name)
.cloned()
.ok_or_else(|| Status::not_found(format!("Table {} not found", name)))
}
pub async fn create_aggregation_view(
&self,
name: String,
source_table: String,
function: AggregateFunction,
group_by: GroupBy,
window: TimeWindow,
aggregate_columns: Vec<String>,
) -> Result<(), Status> {
// Verify source table exists
{
let tables = self.tables.read().await;
if !tables.contains_key(&source_table) {
return Err(Status::not_found(format!("Source table {} not found", source_table)));
}
// Verify aggregate columns exist in source table
let schema = tables.get(&source_table).unwrap();
for col in &aggregate_columns {
if !schema.fields().iter().any(|f| f.name() == col) {
return Err(Status::invalid_argument(format!(
"Column {} not found in source table {}",
col, source_table
)));
}
}
}
let view = AggregationView {
source_table,
function,
group_by,
window,
aggregate_columns,
};
let mut views = self.views.write().await;
if views.contains_key(&name) {
return Err(Status::already_exists(format!("View {} already exists", name)));
}
views.insert(name, view);
Ok(())
}
pub async fn get_aggregation_view(&self, name: &str) -> Result<AggregationView, Status> {
let views = self.views.read().await;
views.get(name)
.cloned()
.ok_or_else(|| Status::not_found(format!("View {} not found", name)))
}
pub async fn list_tables(&self) -> Vec<String> {
let tables = self.tables.read().await;
tables.keys().cloned().collect()
}
pub async fn list_aggregation_views(&self) -> Vec<String> {
let views = self.views.read().await;
views.keys().cloned().collect()
}
pub async fn drop_table(&self, name: &str) -> Result<(), Status> {
let mut tables = self.tables.write().await;
if tables.remove(name).is_none() {
return Err(Status::not_found(format!("Table {} not found", name)));
}
Ok(())
}
pub async fn drop_aggregation_view(&self, name: &str) -> Result<(), Status> {
let mut views = self.views.write().await;
if views.remove(name).is_none() {
return Err(Status::not_found(format!("View {} not found", name)));
}
Ok(())
}
}