Skip to main content

spark_connect/
resource.rs

1//! Resource profile for specifying executor and task resource requirements.
2//!
3//! Mirrors `pyspark.resource` and allows building and registering resource profiles
4//! on the Spark Connect server.
5
6use spark_connect_proto as proto;
7use std::collections::HashMap;
8
9/// Builder for executor resource requests.
10///
11/// Mirrors `pyspark.resource.ExecutorResourceRequests`. Allows specifying
12/// resources requested by executors, including memory, cores, and custom resources.
13#[derive(Debug, Clone, Default)]
14pub struct ExecutorResourceRequests {
15    resources: HashMap<String, proto::ExecutorResourceRequest>,
16}
17
18impl ExecutorResourceRequests {
19    /// Create a new empty ExecutorResourceRequests builder.
20    pub fn new() -> Self {
21        ExecutorResourceRequests {
22            resources: HashMap::new(),
23        }
24    }
25
26    /// Set the memory requirement in MB.
27    pub fn memory(mut self, memory_mb: i64) -> Self {
28        self.resources.insert(
29            "memory".to_string(),
30            proto::ExecutorResourceRequest {
31                resource_name: "memory".to_string(),
32                amount: memory_mb,
33                discovery_script: None,
34                vendor: None,
35            },
36        );
37        self
38    }
39
40    /// Set the off-heap memory requirement in MB.
41    pub fn off_heap_memory(mut self, memory_mb: i64) -> Self {
42        self.resources.insert(
43            "offHeap".to_string(),
44            proto::ExecutorResourceRequest {
45                resource_name: "offHeap".to_string(),
46                amount: memory_mb,
47                discovery_script: None,
48                vendor: None,
49            },
50        );
51        self
52    }
53
54    /// Set the number of cores.
55    pub fn cores(mut self, num_cores: i64) -> Self {
56        self.resources.insert(
57            "cores".to_string(),
58            proto::ExecutorResourceRequest {
59                resource_name: "cores".to_string(),
60                amount: num_cores,
61                discovery_script: None,
62                vendor: None,
63            },
64        );
65        self
66    }
67
68    /// Add a custom resource request.
69    ///
70    /// # Arguments
71    /// * `name` - The resource name (e.g., "gpu", "fpga")
72    /// * `amount` - The amount of the resource being requested
73    /// * `discovery_script` - Optional script to discover the resource on the executor
74    /// * `vendor` - Optional vendor name for the resource
75    pub fn resource(
76        mut self,
77        name: &str,
78        amount: i64,
79        discovery_script: Option<String>,
80        vendor: Option<String>,
81    ) -> Self {
82        self.resources.insert(
83            name.to_string(),
84            proto::ExecutorResourceRequest {
85                resource_name: name.to_string(),
86                amount,
87                discovery_script,
88                vendor,
89            },
90        );
91        self
92    }
93}
94
95/// Builder for task resource requests.
96///
97/// Mirrors `pyspark.resource.TaskResourceRequests`. Allows specifying
98/// resources requested per task, including cores and custom resources.
99#[derive(Debug, Clone, Default)]
100pub struct TaskResourceRequests {
101    resources: HashMap<String, proto::TaskResourceRequest>,
102}
103
104impl TaskResourceRequests {
105    /// Create a new empty TaskResourceRequests builder.
106    pub fn new() -> Self {
107        TaskResourceRequests {
108            resources: HashMap::new(),
109        }
110    }
111
112    /// Set the number of CPUs requested per task.
113    pub fn cpus(mut self, num_cpus: f64) -> Self {
114        self.resources.insert(
115            "cpus".to_string(),
116            proto::TaskResourceRequest {
117                resource_name: "cpus".to_string(),
118                amount: num_cpus,
119            },
120        );
121        self
122    }
123
124    /// Add a custom resource request per task.
125    ///
126    /// # Arguments
127    /// * `name` - The resource name (e.g., "gpu", "fpga")
128    /// * `amount` - The fractional amount of the resource per task
129    pub fn resource(mut self, name: &str, amount: f64) -> Self {
130        self.resources.insert(
131            name.to_string(),
132            proto::TaskResourceRequest {
133                resource_name: name.to_string(),
134                amount,
135            },
136        );
137        self
138    }
139}
140
141/// Builder for ResourceProfile.
142///
143/// Mirrors `pyspark.resource.ResourceProfile`. Allows configuring both executor
144/// and task resource requests, then building and registering the profile with Spark.
145#[derive(Debug, Clone, Default)]
146pub struct ResourceProfileBuilder {
147    executor_requests: ExecutorResourceRequests,
148    task_requests: TaskResourceRequests,
149}
150
151impl ResourceProfileBuilder {
152    /// Create a new empty ResourceProfileBuilder.
153    pub fn new() -> Self {
154        ResourceProfileBuilder {
155            executor_requests: ExecutorResourceRequests::new(),
156            task_requests: TaskResourceRequests::new(),
157        }
158    }
159
160    /// Set the executor resource requests.
161    pub fn executor_resources(mut self, requests: ExecutorResourceRequests) -> Self {
162        self.executor_requests = requests;
163        self
164    }
165
166    /// Set the task resource requests.
167    pub fn task_resources(mut self, requests: TaskResourceRequests) -> Self {
168        self.task_requests = requests;
169        self
170    }
171
172    /// Build the ResourceProfile.
173    pub fn build(self) -> ResourceProfile {
174        ResourceProfile {
175            proto_profile: proto::ResourceProfile {
176                executor_resources: self.executor_requests.resources.clone(),
177                task_resources: self.task_requests.resources.clone(),
178            },
179            profile_id: None,
180        }
181    }
182}
183
184/// A Spark ResourceProfile, optionally with a server-assigned id.
185///
186/// Returned by a ResourceProfileBuilder.build() and registered with the server
187/// via SparkSession::build_resource_profile().
188#[derive(Debug, Clone)]
189pub struct ResourceProfile {
190    pub(crate) proto_profile: proto::ResourceProfile,
191    pub(crate) profile_id: Option<i32>,
192}
193
194impl ResourceProfile {
195    /// Get the profile id if this profile has been registered with the server.
196    pub fn id(&self) -> Option<i32> {
197        self.profile_id
198    }
199
200    /// Get a reference to the underlying proto ResourceProfile.
201    pub(crate) fn proto(&self) -> &proto::ResourceProfile {
202        &self.proto_profile
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use prost::Message;
210
211    #[test]
212    fn executor_resource_requests_encode_decode() {
213        let reqs = ExecutorResourceRequests::new()
214            .memory(2048)
215            .cores(4)
216            .resource("gpu", 2, None, Some("nvidia".to_string()));
217
218        let profile = proto::ResourceProfile {
219            executor_resources: reqs.resources.clone(),
220            task_resources: HashMap::new(),
221        };
222
223        let encoded = profile.encode_to_vec();
224        let decoded = proto::ResourceProfile::decode(encoded.as_slice()).unwrap();
225
226        assert_eq!(decoded.executor_resources.len(), 3);
227        assert_eq!(
228            decoded.executor_resources.get("memory").unwrap().amount,
229            2048
230        );
231        assert_eq!(decoded.executor_resources.get("cores").unwrap().amount, 4);
232        assert_eq!(decoded.executor_resources.get("gpu").unwrap().amount, 2);
233        assert_eq!(
234            decoded.executor_resources.get("gpu").unwrap().vendor,
235            Some("nvidia".to_string())
236        );
237    }
238
239    #[test]
240    fn task_resource_requests_encode_decode() {
241        let reqs = TaskResourceRequests::new().cpus(0.5).resource("gpu", 0.25);
242
243        let profile = proto::ResourceProfile {
244            executor_resources: HashMap::new(),
245            task_resources: reqs.resources.clone(),
246        };
247
248        let encoded = profile.encode_to_vec();
249        let decoded = proto::ResourceProfile::decode(encoded.as_slice()).unwrap();
250
251        assert_eq!(decoded.task_resources.len(), 2);
252        assert_eq!(decoded.task_resources.get("cpus").unwrap().amount, 0.5);
253        assert_eq!(decoded.task_resources.get("gpu").unwrap().amount, 0.25);
254    }
255
256    #[test]
257    fn resource_profile_builder() {
258        let executor_reqs = ExecutorResourceRequests::new().memory(4096).cores(8);
259        let task_reqs = TaskResourceRequests::new().cpus(1.0);
260
261        let profile = ResourceProfileBuilder::new()
262            .executor_resources(executor_reqs)
263            .task_resources(task_reqs)
264            .build();
265
266        assert!(profile.profile_id.is_none());
267        assert_eq!(profile.proto_profile.executor_resources.len(), 2);
268        assert_eq!(profile.proto_profile.task_resources.len(), 1);
269
270        let executor_mem = profile
271            .proto_profile
272            .executor_resources
273            .get("memory")
274            .unwrap();
275        assert_eq!(executor_mem.amount, 4096);
276
277        let task_cpus = profile.proto_profile.task_resources.get("cpus").unwrap();
278        assert_eq!(task_cpus.amount, 1.0);
279    }
280
281    #[test]
282    fn off_heap_memory_and_profile_accessors() {
283        let reqs = ExecutorResourceRequests::new()
284            .off_heap_memory(1024)
285            .cores(2);
286        assert_eq!(reqs.resources.get("offHeap").unwrap().amount, 1024);
287
288        let profile = ResourceProfileBuilder::new()
289            .executor_resources(reqs)
290            .build();
291        // id() is None before the profile is registered with the server.
292        assert!(profile.id().is_none());
293        // proto() exposes the underlying proto with both resource entries.
294        assert_eq!(profile.proto().executor_resources.len(), 2);
295    }
296}