1use std::{fmt::Debug, sync::Arc, time::Duration};
4
5use temporalio_sdk_core::{
6 ResourceBasedSlotsOptions as CoreResourceBasedSlotsOptions,
7 ResourceBasedTunerConfig as CoreResourceBasedTunerConfig,
8 ResourceController as CoreResourceController, ResourceSlotOptions as CoreResourceSlotOptions,
9 SlotKind as CoreSlotKind, SlotSupplierOptions as CoreSlotSupplierOptions,
10 TunerHolderOptions as CoreTunerHolderOptions, WorkerTuner as CoreWorkerTuner,
11};
12
13const DEFAULT_FIXED_SIZE_SLOTS: usize = 100;
14
15#[derive(Clone, Debug)]
17#[non_exhaustive]
18pub enum WorkerTuner {
19 ResourceBased(ResourceBasedTuner),
21 ResourceBasedWithController(ResourceBasedTunerWithController),
23 TunerHolder(TunerHolder),
25}
26
27impl WorkerTuner {
28 pub(crate) fn to_core(&self) -> Result<Arc<dyn CoreWorkerTuner + Send + Sync>, String> {
29 match self {
30 Self::ResourceBased(tuner) => tuner.to_tuner_holder().to_core(),
31 Self::ResourceBasedWithController(tuner) => tuner.to_tuner_holder().to_core(),
32 Self::TunerHolder(tuner) => tuner.to_core(),
33 }
34 }
35}
36
37impl Default for WorkerTuner {
38 fn default() -> Self {
39 TunerHolder::builder()
40 .workflow_task_slot_supplier(FixedSizeSlotSupplier::new(DEFAULT_FIXED_SIZE_SLOTS))
41 .activity_task_slot_supplier(FixedSizeSlotSupplier::new(DEFAULT_FIXED_SIZE_SLOTS))
42 .local_activity_task_slot_supplier(FixedSizeSlotSupplier::new(DEFAULT_FIXED_SIZE_SLOTS))
43 .nexus_task_slot_supplier(FixedSizeSlotSupplier::new(DEFAULT_FIXED_SIZE_SLOTS))
44 .build()
45 .into()
46 }
47}
48
49impl From<ResourceBasedTuner> for WorkerTuner {
50 fn from(value: ResourceBasedTuner) -> Self {
51 Self::ResourceBased(value)
52 }
53}
54
55impl From<ResourceBasedTunerWithController> for WorkerTuner {
56 fn from(value: ResourceBasedTunerWithController) -> Self {
57 Self::ResourceBasedWithController(value)
58 }
59}
60
61impl From<TunerHolder> for WorkerTuner {
62 fn from(value: TunerHolder) -> Self {
63 Self::TunerHolder(value)
64 }
65}
66
67#[derive(Clone, Debug, bon::Builder)]
69#[builder(state_mod(vis = "pub"))]
70#[non_exhaustive]
71pub struct TunerHolder {
72 #[builder(into)]
74 pub workflow_task_slot_supplier: SlotSupplier,
75 #[builder(into)]
77 pub activity_task_slot_supplier: SlotSupplier,
78 #[builder(into)]
80 pub local_activity_task_slot_supplier: SlotSupplier,
81 #[builder(into)]
83 pub nexus_task_slot_supplier: SlotSupplier,
84}
85
86impl TunerHolder {
87 fn to_core(&self) -> Result<Arc<dyn CoreWorkerTuner + Send + Sync>, String> {
88 let configurations = [
89 self.workflow_task_slot_supplier.resource_configuration(),
90 self.activity_task_slot_supplier.resource_configuration(),
91 self.local_activity_task_slot_supplier
92 .resource_configuration(),
93 self.nexus_task_slot_supplier.resource_configuration(),
94 ];
95 let mut configurations = configurations.into_iter().flatten();
96 let resource_based_config = configurations.next();
97 if let Some(first) = resource_based_config
98 && configurations.any(|other| !first.is_compatible_with(other))
99 {
100 return Err(
101 "cannot construct worker tuner with multiple different resource-based tuner configurations"
102 .to_owned(),
103 );
104 }
105
106 CoreTunerHolderOptions::builder()
107 .workflow_slot_options(
108 self.workflow_task_slot_supplier
109 .to_core(ResourceKind::Workflow),
110 )
111 .activity_slot_options(
112 self.activity_task_slot_supplier
113 .to_core(ResourceKind::Activity),
114 )
115 .local_activity_slot_options(
116 self.local_activity_task_slot_supplier
117 .to_core(ResourceKind::Activity),
118 )
119 .nexus_slot_options(self.nexus_task_slot_supplier.to_core(ResourceKind::Nexus))
120 .maybe_resource_based_config(resource_based_config.map(ResourceBasedConfig::to_core))
121 .build()
122 .map_err(|error| error.to_string())?
123 .build_tuner_holder()
124 .map(|tuner| Arc::new(tuner) as Arc<dyn CoreWorkerTuner + Send + Sync>)
125 .map_err(|error| error.to_string())
126 }
127}
128
129#[derive(Clone, Debug, bon::Builder)]
131#[builder(state_mod(vis = "pub"))]
132#[non_exhaustive]
133pub struct ResourceBasedTuner {
134 pub tuner_options: ResourceBasedTunerOptions,
136 pub workflow_task_slot_options: Option<ResourceBasedSlotOptions>,
138 pub activity_task_slot_options: Option<ResourceBasedSlotOptions>,
140 pub local_activity_task_slot_options: Option<ResourceBasedSlotOptions>,
142 pub nexus_task_slot_options: Option<ResourceBasedSlotOptions>,
144}
145
146impl ResourceBasedTuner {
147 fn to_tuner_holder(&self) -> TunerHolder {
148 TunerHolder::builder()
149 .workflow_task_slot_supplier(ResourceBasedSlotsForType::new(
150 self.tuner_options,
151 self.workflow_task_slot_options.unwrap_or_default(),
152 ))
153 .activity_task_slot_supplier(ResourceBasedSlotsForType::new(
154 self.tuner_options,
155 self.activity_task_slot_options.unwrap_or_default(),
156 ))
157 .local_activity_task_slot_supplier(ResourceBasedSlotsForType::new(
158 self.tuner_options,
159 self.local_activity_task_slot_options.unwrap_or_default(),
160 ))
161 .nexus_task_slot_supplier(ResourceBasedSlotsForType::new(
162 self.tuner_options,
163 self.nexus_task_slot_options.unwrap_or_default(),
164 ))
165 .build()
166 }
167}
168
169#[derive(Clone, Debug, bon::Builder)]
171#[builder(state_mod(vis = "pub"))]
172#[non_exhaustive]
173pub struct ResourceBasedTunerWithController {
174 pub controller: ResourceBasedController,
176 pub workflow_task_slot_options: Option<ResourceBasedSlotOptions>,
178 pub activity_task_slot_options: Option<ResourceBasedSlotOptions>,
180 pub local_activity_task_slot_options: Option<ResourceBasedSlotOptions>,
182 pub nexus_task_slot_options: Option<ResourceBasedSlotOptions>,
184}
185
186impl ResourceBasedTunerWithController {
187 fn to_tuner_holder(&self) -> TunerHolder {
188 TunerHolder::builder()
189 .workflow_task_slot_supplier(ResourceBasedSlotsForType::with_controller(
190 self.controller.clone(),
191 self.workflow_task_slot_options.unwrap_or_default(),
192 ))
193 .activity_task_slot_supplier(ResourceBasedSlotsForType::with_controller(
194 self.controller.clone(),
195 self.activity_task_slot_options.unwrap_or_default(),
196 ))
197 .local_activity_task_slot_supplier(ResourceBasedSlotsForType::with_controller(
198 self.controller.clone(),
199 self.local_activity_task_slot_options.unwrap_or_default(),
200 ))
201 .nexus_task_slot_supplier(ResourceBasedSlotsForType::with_controller(
202 self.controller.clone(),
203 self.nexus_task_slot_options.unwrap_or_default(),
204 ))
205 .build()
206 }
207}
208
209#[derive(Clone, Copy, Debug, PartialEq, bon::Builder)]
211#[builder(state_mod(vis = "pub"))]
212#[non_exhaustive]
213pub struct ResourceBasedTunerOptions {
214 pub target_memory_usage: f64,
216 pub target_cpu_usage: f64,
218}
219
220impl ResourceBasedTunerOptions {
221 fn to_core(self) -> CoreResourceBasedSlotsOptions {
222 CoreResourceBasedSlotsOptions::builder()
223 .target_mem_usage(self.target_memory_usage)
224 .target_cpu_usage(self.target_cpu_usage)
225 .build()
226 }
227}
228
229#[derive(Clone, derive_more::Debug)]
231pub struct ResourceBasedController(#[debug(skip)] Arc<CoreResourceController>);
232
233impl ResourceBasedController {
234 pub fn new(options: ResourceBasedTunerOptions) -> Self {
236 Self(Arc::new(CoreResourceController::new(options.to_core())))
237 }
238}
239
240#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, bon::Builder)]
242#[builder(state_mod(vis = "pub"))]
243#[non_exhaustive]
244pub struct ResourceBasedSlotOptions {
245 pub minimum_slots: Option<usize>,
247 pub maximum_slots: Option<usize>,
249 pub ramp_throttle: Option<Duration>,
251}
252
253impl ResourceBasedSlotOptions {
254 fn to_core(self, kind: ResourceKind) -> CoreResourceSlotOptions {
255 let defaults = kind.slot_defaults();
256 CoreResourceSlotOptions::new(
257 self.minimum_slots.unwrap_or(defaults.minimum_slots),
258 self.maximum_slots.unwrap_or(defaults.maximum_slots),
259 self.ramp_throttle.unwrap_or(defaults.ramp_throttle),
260 )
261 }
262}
263
264#[derive(Clone, Debug)]
266pub struct ResourceBasedSlotsForType {
267 configuration: ResourceBasedConfig,
268 pub slot_options: ResourceBasedSlotOptions,
270}
271
272impl ResourceBasedSlotsForType {
273 pub fn new(
275 tuner_options: ResourceBasedTunerOptions,
276 slot_options: ResourceBasedSlotOptions,
277 ) -> Self {
278 Self {
279 configuration: ResourceBasedConfig::Options(tuner_options),
280 slot_options,
281 }
282 }
283
284 pub fn with_controller(
286 controller: ResourceBasedController,
287 slot_options: ResourceBasedSlotOptions,
288 ) -> Self {
289 Self {
290 configuration: ResourceBasedConfig::Controller(controller),
291 slot_options,
292 }
293 }
294
295 pub fn tuner_options(&self) -> Option<ResourceBasedTunerOptions> {
297 match self.configuration {
298 ResourceBasedConfig::Options(options) => Some(options),
299 ResourceBasedConfig::Controller(_) => None,
300 }
301 }
302
303 pub fn controller(&self) -> Option<&ResourceBasedController> {
305 match &self.configuration {
306 ResourceBasedConfig::Options(_) => None,
307 ResourceBasedConfig::Controller(controller) => Some(controller),
308 }
309 }
310}
311
312#[derive(Clone, Debug)]
313enum ResourceBasedConfig {
314 Options(ResourceBasedTunerOptions),
315 Controller(ResourceBasedController),
316}
317
318impl ResourceBasedConfig {
319 fn is_compatible_with(&self, other: &Self) -> bool {
320 match (self, other) {
321 (Self::Options(left), Self::Options(right)) => left == right,
322 (Self::Controller(left), Self::Controller(right)) => Arc::ptr_eq(&left.0, &right.0),
323 (Self::Options(_), Self::Controller(_)) | (Self::Controller(_), Self::Options(_)) => {
324 false
325 }
326 }
327 }
328
329 fn to_core(&self) -> CoreResourceBasedTunerConfig {
330 match self {
331 Self::Options(options) => CoreResourceBasedTunerConfig::Options(options.to_core()),
332 Self::Controller(controller) => {
333 CoreResourceBasedTunerConfig::Controller(controller.0.clone())
334 }
335 }
336 }
337}
338
339#[derive(Clone, Copy, Debug, Eq, PartialEq)]
341#[non_exhaustive]
342pub struct FixedSizeSlotSupplier {
343 pub num_slots: usize,
345}
346
347impl FixedSizeSlotSupplier {
348 pub fn new(num_slots: usize) -> Self {
350 Self { num_slots }
351 }
352}
353
354#[derive(Clone, Debug)]
356#[non_exhaustive]
357pub enum SlotSupplier {
358 FixedSize(FixedSizeSlotSupplier),
360 ResourceBased(ResourceBasedSlotsForType),
362}
363
364impl From<FixedSizeSlotSupplier> for SlotSupplier {
365 fn from(value: FixedSizeSlotSupplier) -> Self {
366 Self::FixedSize(value)
367 }
368}
369
370impl From<ResourceBasedSlotsForType> for SlotSupplier {
371 fn from(value: ResourceBasedSlotsForType) -> Self {
372 Self::ResourceBased(value)
373 }
374}
375
376impl SlotSupplier {
377 fn resource_configuration(&self) -> Option<&ResourceBasedConfig> {
378 match self {
379 Self::ResourceBased(options) => Some(&options.configuration),
380 Self::FixedSize(_) => None,
381 }
382 }
383
384 fn to_core<SK: CoreSlotKind>(&self, kind: ResourceKind) -> CoreSlotSupplierOptions<SK> {
385 match self {
386 Self::FixedSize(supplier) => CoreSlotSupplierOptions::FixedSize {
387 slots: supplier.num_slots,
388 },
389 Self::ResourceBased(options) => {
390 CoreSlotSupplierOptions::ResourceBased(options.slot_options.to_core(kind))
391 }
392 }
393 }
394}
395
396#[derive(Clone, Copy)]
397enum ResourceKind {
398 Workflow,
399 Activity,
400 Nexus,
401}
402
403impl ResourceKind {
404 fn slot_defaults(self) -> ResourceSlotDefaults {
405 match self {
406 Self::Workflow => ResourceSlotDefaults {
407 minimum_slots: 2,
408 maximum_slots: 1_000,
409 ramp_throttle: Duration::from_millis(10),
410 },
411 Self::Activity | Self::Nexus => ResourceSlotDefaults {
412 minimum_slots: 1,
413 maximum_slots: 2_000,
414 ramp_throttle: Duration::from_millis(50),
415 },
416 }
417 }
418}
419
420struct ResourceSlotDefaults {
421 minimum_slots: usize,
422 maximum_slots: usize,
423 ramp_throttle: Duration,
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn composite_tuner_rejects_different_resource_options() {
432 let first_options = ResourceBasedTunerOptions::builder()
433 .target_memory_usage(0.5)
434 .target_cpu_usage(0.5)
435 .build();
436 let second_options = ResourceBasedTunerOptions::builder()
437 .target_memory_usage(0.6)
438 .target_cpu_usage(0.5)
439 .build();
440 let result = WorkerTuner::from(
441 TunerHolder::builder()
442 .workflow_task_slot_supplier(ResourceBasedSlotsForType::new(
443 first_options,
444 Default::default(),
445 ))
446 .activity_task_slot_supplier(ResourceBasedSlotsForType::new(
447 second_options,
448 Default::default(),
449 ))
450 .local_activity_task_slot_supplier(FixedSizeSlotSupplier::new(1))
451 .nexus_task_slot_supplier(FixedSizeSlotSupplier::new(1))
452 .build(),
453 )
454 .to_core();
455 assert!(
456 result
457 .err()
458 .is_some_and(|error| error.contains("different resource-based tuner"))
459 );
460 }
461}