Skip to main content

kvbm_config/
object.rs

1// SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Object storage configuration for KVBM.
5//!
6//! Defines configuration for object storage backends (S3, NIXL) used for
7//! the G4 tier (object storage) in the cache hierarchy.
8
9use serde::{Deserialize, Serialize};
10use validator::Validate;
11
12/// Top-level object storage configuration.
13///
14/// When present, enables object storage operations on workers.
15#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
16pub struct ObjectConfig {
17    /// Which object client implementation to use.
18    pub client: ObjectClientConfig,
19}
20
21/// Object client implementation selector.
22///
23/// Determines whether to use direct S3 client or NIXL agent for object storage.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(tag = "type", rename_all = "lowercase")]
26pub enum ObjectClientConfig {
27    /// Direct S3/MinIO client using AWS SDK.
28    S3(S3ObjectConfig),
29    /// NIXL agent with object storage backend.
30    Nixl(NixlObjectConfig),
31}
32
33/// S3-compatible object storage configuration.
34///
35/// Used for both direct S3 access and as a backend for NIXL.
36/// Compatible with AWS S3 and S3-compatible services like MinIO.
37#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
38pub struct S3ObjectConfig {
39    /// Custom endpoint URL for S3-compatible services (e.g., MinIO).
40    /// If None, uses the default AWS S3 endpoint.
41    #[serde(default)]
42    pub endpoint_url: Option<String>,
43
44    /// S3 bucket name for storing blocks.
45    pub bucket: String,
46
47    /// AWS region.
48    #[serde(default = "default_region")]
49    pub region: String,
50
51    /// Use path-style URLs instead of virtual-hosted-style.
52    /// Required for MinIO and some S3-compatible services.
53    #[serde(default)]
54    pub force_path_style: bool,
55
56    /// Maximum number of concurrent S3 requests.
57    #[serde(default = "default_max_concurrent")]
58    pub max_concurrent_requests: usize,
59}
60
61fn default_region() -> String {
62    "us-east-1".to_string()
63}
64
65fn default_max_concurrent() -> usize {
66    16
67}
68
69impl Default for S3ObjectConfig {
70    fn default() -> Self {
71        Self {
72            endpoint_url: None,
73            bucket: "kvbm-blocks".to_string(),
74            region: default_region(),
75            force_path_style: false,
76            max_concurrent_requests: default_max_concurrent(),
77        }
78    }
79}
80
81impl S3ObjectConfig {
82    /// Create configuration for AWS S3.
83    pub fn aws(bucket: String, region: String) -> Self {
84        Self {
85            endpoint_url: None,
86            bucket,
87            region,
88            force_path_style: false,
89            max_concurrent_requests: default_max_concurrent(),
90        }
91    }
92
93    /// Create configuration for MinIO or other S3-compatible services.
94    pub fn minio(endpoint_url: String, bucket: String) -> Self {
95        Self {
96            endpoint_url: Some(endpoint_url),
97            bucket,
98            region: default_region(),
99            force_path_style: true,
100            max_concurrent_requests: default_max_concurrent(),
101        }
102    }
103}
104
105/// NIXL object storage backend configuration.
106///
107/// NIXL can use various object storage backends. Each variant
108/// specifies the backend type and its configuration.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110#[serde(tag = "backend", rename_all = "lowercase")]
111pub enum NixlObjectConfig {
112    /// S3-compatible backend via NIXL.
113    S3(S3ObjectConfig),
114    // Future backends can be added here:
115    // Gcs(GcsObjectConfig),
116    // Azure(AzureObjectConfig),
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_s3_config_default() {
125        let config = S3ObjectConfig::default();
126        assert!(config.endpoint_url.is_none());
127        assert_eq!(config.bucket, "kvbm-blocks");
128        assert_eq!(config.region, "us-east-1");
129        assert!(!config.force_path_style);
130        assert_eq!(config.max_concurrent_requests, 16);
131    }
132
133    #[test]
134    fn test_s3_config_aws() {
135        let config = S3ObjectConfig::aws("my-bucket".into(), "us-west-2".into());
136        assert!(config.endpoint_url.is_none());
137        assert_eq!(config.bucket, "my-bucket");
138        assert_eq!(config.region, "us-west-2");
139        assert!(!config.force_path_style);
140    }
141
142    #[test]
143    fn test_s3_config_minio() {
144        let config = S3ObjectConfig::minio("http://localhost:9000".into(), "test".into());
145        assert_eq!(config.endpoint_url, Some("http://localhost:9000".into()));
146        assert_eq!(config.bucket, "test");
147        assert!(config.force_path_style);
148    }
149
150    #[test]
151    fn test_object_config_serde_s3() {
152        let json = r#"{
153            "client": {
154                "type": "s3",
155                "bucket": "my-bucket",
156                "region": "us-west-2"
157            }
158        }"#;
159        let config: ObjectConfig = serde_json::from_str(json).unwrap();
160        match config.client {
161            ObjectClientConfig::S3(s3) => {
162                assert_eq!(s3.bucket, "my-bucket");
163                assert_eq!(s3.region, "us-west-2");
164            }
165            _ => panic!("Expected S3 config"),
166        }
167    }
168
169    #[test]
170    fn test_object_config_serde_nixl_s3() {
171        let json = r#"{
172            "client": {
173                "type": "nixl",
174                "backend": "s3",
175                "bucket": "nixl-bucket",
176                "endpoint_url": "http://minio:9000",
177                "force_path_style": true
178            }
179        }"#;
180        let config: ObjectConfig = serde_json::from_str(json).unwrap();
181        match config.client {
182            ObjectClientConfig::Nixl(NixlObjectConfig::S3(s3)) => {
183                assert_eq!(s3.bucket, "nixl-bucket");
184                assert_eq!(s3.endpoint_url, Some("http://minio:9000".into()));
185                assert!(s3.force_path_style);
186            }
187            _ => panic!("Expected Nixl S3 config"),
188        }
189    }
190}