From 56ef361ae7119f7e44a009fb4281322ad982f0ff Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 24 Jun 2024 10:47:27 -0700 Subject: [PATCH 01/10] Autotune progress from laptop --- temporalio/bridge/src/worker.rs | 120 ++++++++++++++++++++++++++-- temporalio/bridge/worker.py | 35 +++++++- temporalio/worker/__init__.py | 3 + temporalio/worker/_replayer.py | 14 +++- temporalio/worker/_tuning.py | 136 ++++++++++++++++++++++++++++++++ temporalio/worker/_worker.py | 59 +++++++++++--- 6 files changed, 344 insertions(+), 23 deletions(-) create mode 100644 temporalio/worker/_tuning.py diff --git a/temporalio/bridge/src/worker.rs b/temporalio/bridge/src/worker.rs index 5c21272b2..e955619fd 100644 --- a/temporalio/bridge/src/worker.rs +++ b/temporalio/bridge/src/worker.rs @@ -34,9 +34,7 @@ pub struct WorkerConfig { build_id: String, identity_override: Option, max_cached_workflows: usize, - max_outstanding_workflow_tasks: usize, - max_outstanding_activities: usize, - max_outstanding_local_activities: usize, + tuner: TunerHolder, max_concurrent_workflow_task_polls: usize, nonsticky_to_sticky_poll_ratio: f32, max_concurrent_activity_task_polls: usize, @@ -52,6 +50,33 @@ pub struct WorkerConfig { nondeterminism_as_workflow_fail_for_types: HashSet, } +#[derive(FromPyObject)] +pub struct TunerHolder { + workflow_slot_supplier: SlotSupplier, + activity_slot_supplier: SlotSupplier, + local_activity_slot_supplier: SlotSupplier, +} + +#[derive(FromPyObject)] +pub enum SlotSupplier { + FixedSize(usize), + ResourceBased(ResourceBasedSlotSupplier), +} + +#[derive(FromPyObject)] +pub struct ResourceBasedSlotSupplier { + minimum_slots: usize, + maximum_slots: usize, + ramp_throttle: Duration, + tuner_options: ResourceBasedTunerOptions, +} + +#[derive(FromPyObject, Clone, Copy, PartialEq)] +pub struct ResourceBasedTunerOptions { + target_memory_usage: f64, + target_cpu_usage: f64, +} + macro_rules! enter_sync { ($runtime:expr) => { if let Some(subscriber) = $runtime.core.telemetry().trace_subscriber() { @@ -226,16 +251,15 @@ impl TryFrom for temporal_sdk_core::WorkerConfig { type Error = PyErr; fn try_from(conf: WorkerConfig) -> PyResult { + let converted_tuner: temporal_sdk_core::TunerHolder = conf.tuner.try_into()?; temporal_sdk_core::WorkerConfigBuilder::default() .namespace(conf.namespace) .task_queue(conf.task_queue) .worker_build_id(conf.build_id) .client_identity_override(conf.identity_override) .max_cached_workflows(conf.max_cached_workflows) - .max_outstanding_workflow_tasks(conf.max_outstanding_workflow_tasks) - .max_outstanding_activities(conf.max_outstanding_activities) - .max_outstanding_local_activities(conf.max_outstanding_local_activities) .max_concurrent_wft_polls(conf.max_concurrent_workflow_task_polls) + .tuner(Arc::new(converted_tuner)) .nonsticky_to_sticky_poll_ratio(conf.nonsticky_to_sticky_poll_ratio) .max_concurrent_at_polls(conf.max_concurrent_activity_task_polls) .no_remote_activities(conf.no_remote_activities) @@ -276,6 +300,90 @@ impl TryFrom for temporal_sdk_core::WorkerConfig { } } +impl TryFrom for temporal_sdk_core::TunerHolder { + type Error = PyErr; + + fn try_from(holder: TunerHolder) -> PyResult { + // Verify all resource-based options are the same if any are set + let maybe_wf_resource_opts = + if let SlotSupplier::ResourceBased(ref ss) = holder.workflow_slot_supplier { + Some(&ss.tuner_options) + } else { + None + }; + let maybe_act_resource_opts = + if let SlotSupplier::ResourceBased(ref ss) = holder.activity_slot_supplier { + Some(&ss.tuner_options) + } else { + None + }; + let maybe_local_act_resource_opts = + if let SlotSupplier::ResourceBased(ref ss) = holder.local_activity_slot_supplier { + Some(&ss.tuner_options) + } else { + None + }; + let all_resource_opts = [ + maybe_wf_resource_opts, + maybe_act_resource_opts, + maybe_local_act_resource_opts, + ]; + let mut set_resource_opts = all_resource_opts.iter().flatten(); + let first = set_resource_opts.next(); + let all_are_same = if let Some(first) = first { + set_resource_opts.all(|elem| elem == first) + } else { + true + }; + if !all_are_same { + return Err(PyValueError::new_err( + "All resource-based slot suppliers must have the same ResourceBasedTunerOptions", + )); + } + + let mut options = temporal_sdk_core::TunerHolderOptionsBuilder::default(); + if let Some(first) = first { + options.resource_based_options( + temporal_sdk_core::ResourceBasedSlotsOptionsBuilder::default() + .target_mem_usage(first.target_memory_usage) + .target_cpu_usage(first.target_cpu_usage) + .build() + .expect("Building ResourceBasedSlotsOptions is infallible"), + ); + }; + options + .workflow_slot_options(holder.workflow_slot_supplier.try_into()?) + .activity_slot_options(holder.activity_slot_supplier.try_into()?) + .local_activity_slot_options(holder.local_activity_slot_supplier.try_into()?); + Ok(options + .build() + .map_err(|e| PyValueError::new_err(format!("Invalid tuner holder options: {}", e)))? + .build_tuner_holder() + .map_err(|e| PyRuntimeError::new_err(format!("Failed to build tuner holder: {}", e)))?) + } +} + +impl TryFrom for temporal_sdk_core::SlotSupplierOptions { + type Error = PyErr; + + fn try_from(supplier: SlotSupplier) -> PyResult { + Ok(match supplier { + SlotSupplier::FixedSize(size) => { + temporal_sdk_core::SlotSupplierOptions::FixedSize { slots: size } + } + SlotSupplier::ResourceBased(ss) => { + temporal_sdk_core::SlotSupplierOptions::ResourceBased( + temporal_sdk_core::ResourceSlotOptions::new( + ss.minimum_slots, + ss.maximum_slots, + ss.ramp_throttle, + ), + ) + } + }) + } +} + /// For feeding histories into core during replay #[pyclass] pub struct HistoryPusher { diff --git a/temporalio/bridge/worker.py b/temporalio/bridge/worker.py index 9060d6469..b6f44b6e7 100644 --- a/temporalio/bridge/worker.py +++ b/temporalio/bridge/worker.py @@ -6,6 +6,7 @@ from __future__ import annotations from dataclasses import dataclass +from datetime import timedelta from typing import ( TYPE_CHECKING, Awaitable, @@ -15,6 +16,7 @@ Sequence, Set, Tuple, + Union, ) import google.protobuf.internal.containers @@ -43,9 +45,7 @@ class WorkerConfig: build_id: str identity_override: Optional[str] max_cached_workflows: int - max_outstanding_workflow_tasks: int - max_outstanding_activities: int - max_outstanding_local_activities: int + tuner: TunerHolder max_concurrent_workflow_task_polls: int nonsticky_to_sticky_poll_ratio: float max_concurrent_activity_task_polls: int @@ -61,6 +61,35 @@ class WorkerConfig: nondeterminism_as_workflow_fail_for_types: Set[str] +@dataclass +class ResourceBasedTunerOptions: + target_memory_usage: float + target_cpu_usage: float + + +@dataclass +class ResourceBasedSlotSupplier: + minimum_slots: int + maximum_slots: int + ramp_throttle: timedelta + tuner_options: ResourceBasedTunerOptions + + +@dataclass(frozen=True) +class FixedSizeSlotSupplier: + num_slots: int + + +SlotSupplier: TypeAlias = Union[FixedSizeSlotSupplier, ResourceBasedSlotSupplier] + + +@dataclass +class TunerHolder: + workflow_slot_supplier: SlotSupplier + activity_slot_supplier: SlotSupplier + local_activity_slot_supplier: SlotSupplier + + class Worker: """SDK Core worker.""" diff --git a/temporalio/worker/__init__.py b/temporalio/worker/__init__.py index aa7936e60..b5b94bb66 100644 --- a/temporalio/worker/__init__.py +++ b/temporalio/worker/__init__.py @@ -26,6 +26,7 @@ WorkflowReplayResult, WorkflowReplayResults, ) +from ._tuning import WorkerTuner from ._worker import Worker, WorkerConfig from ._workflow_instance import ( UnsandboxedWorkflowRunner, @@ -69,4 +70,6 @@ "WorkflowInstance", "WorkflowInstanceDetails", "UnsandboxedWorkflowRunner", + # Tuning types + "WorkerTuner", ] diff --git a/temporalio/worker/_replayer.py b/temporalio/worker/_replayer.py index d7d540fe2..05f65cbd7 100644 --- a/temporalio/worker/_replayer.py +++ b/temporalio/worker/_replayer.py @@ -223,9 +223,17 @@ def on_eviction_hook( nondeterminism_as_workflow_fail_for_types=workflow_worker.nondeterminism_as_workflow_fail_for_types(), # All values below are ignored but required by Core max_cached_workflows=2, - max_outstanding_workflow_tasks=2, - max_outstanding_activities=1, - max_outstanding_local_activities=1, + tuner=temporalio.bridge.worker.TunerHolder( + workflow_slot_supplier=temporalio.bridge.worker.FixedSizeSlotSupplier( + 2 + ), + activity_slot_supplier=temporalio.bridge.worker.FixedSizeSlotSupplier( + 1 + ), + local_activity_slot_supplier=temporalio.bridge.worker.FixedSizeSlotSupplier( + 1 + ), + ), max_concurrent_workflow_task_polls=1, nonsticky_to_sticky_poll_ratio=1, max_concurrent_activity_task_polls=1, diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py new file mode 100644 index 000000000..068b4eb40 --- /dev/null +++ b/temporalio/worker/_tuning.py @@ -0,0 +1,136 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from datetime import timedelta +from typing import Literal, Optional, TypeAlias, Union + +import temporalio.bridge.worker + + +@dataclass(frozen=True) +class FixedSizeSlotSupplier: + num_slots: int + + +@dataclass(frozen=True) +class ResourceBasedTunerOptions: + target_memory_usage: float + """A value between 0 and 1 that represents the target (system) memory usage. It's not recommended + to set this higher than 0.8, since how much memory a workflow may use is not predictable, and + you don't want to encounter OOM errors.""" + target_cpu_usage: float + """A value between 0 and 1 that represents the target (system) CPU usage. This can be set to 1.0 + if desired, but it's recommended to leave some headroom for other processes.""" + + +@dataclass(frozen=True) +class ResourceBasedSlotOptions: + minimum_slots: Optional[int] + maximum_slots: Optional[int] + ramp_throttle: Optional[timedelta] + + +@dataclass(frozen=True) +class ResourceBasedSlotSupplier: + slot_options: ResourceBasedSlotOptions + tuner_options: ResourceBasedTunerOptions + + +SlotSupplier: TypeAlias = Union[FixedSizeSlotSupplier, ResourceBasedSlotSupplier] + + +def _to_bridge_slot_supplier( + slot_supplier: SlotSupplier, kind: Literal["workflow", "activity", "local_activity"] +) -> temporalio.bridge.worker.SlotSupplier: + if isinstance(slot_supplier, FixedSizeSlotSupplier): + return temporalio.bridge.worker.FixedSizeSlotSupplier(slot_supplier.num_slots) + elif isinstance(slot_supplier, ResourceBasedSlotSupplier): + min_slots = 5 if kind == "workflow" else 1 + max_slots = 500 + ramp_throttle = ( + timedelta(seconds=0) if kind == "workflow" else timedelta(milliseconds=50) + ) + if slot_supplier.slot_options.minimum_slots is not None: + min_slots = slot_supplier.slot_options.minimum_slots + if slot_supplier.slot_options.maximum_slots is not None: + max_slots = slot_supplier.slot_options.maximum_slots + if slot_supplier.slot_options.ramp_throttle is not None: + ramp_throttle = slot_supplier.slot_options.ramp_throttle + return temporalio.bridge.worker.ResourceBasedSlotSupplier( + min_slots, + max_slots, + ramp_throttle, + temporalio.bridge.worker.ResourceBasedTunerOptions( + slot_supplier.tuner_options.target_memory_usage, + slot_supplier.tuner_options.target_cpu_usage, + ), + ) + else: + raise TypeError(f"Unknown slot supplier type: {slot_supplier}") + + +class WorkerTuner(ABC): + """WorkerTuners allow for the dynamic customization of some aspects of worker configuration""" + + @abstractmethod + def get_workflow_task_slot_supplier(self) -> SlotSupplier: + raise NotImplementedError + + @abstractmethod + def get_activity_task_slot_supplier(self) -> SlotSupplier: + raise NotImplementedError + + @abstractmethod + def get_local_activity_task_slot_supplier(self) -> SlotSupplier: + raise NotImplementedError + + +class ResourceBasedTuner(WorkerTuner): + def __init__(self, options: ResourceBasedTunerOptions): + self.options = options + self.workflow_task_options: Optional[ResourceBasedSlotOptions] = None + self.activity_task_options: Optional[ResourceBasedSlotOptions] = None + self.local_activity_task_options: Optional[ResourceBasedSlotOptions] = None + + def set_workflow_task_options(self, options: ResourceBasedSlotOptions): + self.workflow_task_options = options + + def set_activity_task_options(self, options: ResourceBasedSlotOptions): + self.activity_task_options = options + + def set_local_activity_task_options(self, options: ResourceBasedSlotOptions): + self.local_activity_task_options = options + + def get_workflow_task_slot_supplier(self) -> SlotSupplier: + return ResourceBasedSlotSupplier( + self.workflow_task_options or ResourceBasedSlotOptions(None, None, None), + self.options, + ) + + def get_activity_task_slot_supplier(self) -> SlotSupplier: + return ResourceBasedSlotSupplier( + self.activity_task_options or ResourceBasedSlotOptions(None, None, None), + self.options, + ) + + def get_local_activity_task_slot_supplier(self) -> SlotSupplier: + return ResourceBasedSlotSupplier( + self.local_activity_task_options + or ResourceBasedSlotOptions(None, None, None), + self.options, + ) + + +@dataclass(frozen=True) +class CompositeTuner(WorkerTuner): + workflow_slot_supplier: SlotSupplier + activity_slot_supplier: SlotSupplier + local_activity_slot_supplier: SlotSupplier + + def get_workflow_task_slot_supplier(self) -> SlotSupplier: + return self.workflow_slot_supplier + + def get_activity_task_slot_supplier(self) -> SlotSupplier: + return self.activity_slot_supplier + + def get_local_activity_task_slot_supplier(self) -> SlotSupplier: + return self.local_activity_slot_supplier diff --git a/temporalio/worker/_worker.py b/temporalio/worker/_worker.py index 7f6f068f8..d746d2791 100644 --- a/temporalio/worker/_worker.py +++ b/temporalio/worker/_worker.py @@ -29,6 +29,7 @@ from ._activity import SharedStateManager, _ActivityWorker from ._interceptor import Interceptor +from ._tuning import WorkerTuner, _to_bridge_slot_supplier from ._workflow import _WorkflowWorker from ._workflow_instance import UnsandboxedWorkflowRunner, WorkflowRunner from .workflow_sandbox import SandboxedWorkflowRunner @@ -60,9 +61,10 @@ def __init__( build_id: Optional[str] = None, identity: Optional[str] = None, max_cached_workflows: int = 1000, - max_concurrent_workflow_tasks: int = 100, - max_concurrent_activities: int = 100, - max_concurrent_local_activities: int = 100, + max_concurrent_workflow_tasks: Optional[int] = None, + max_concurrent_activities: Optional[int] = None, + max_concurrent_local_activities: Optional[int] = None, + tuner: Optional[WorkerTuner] = None, max_concurrent_workflow_task_polls: int = 5, nonsticky_to_sticky_poll_ratio: float = 0.2, max_concurrent_activity_task_polls: int = 5, @@ -131,6 +133,7 @@ def __init__( will ever be given to this worker concurrently. max_concurrent_local_activities: Maximum number of local activity tasks that will ever be given to this worker concurrently. + tuner: TODO max_concurrent_workflow_task_polls: Maximum number of concurrent poll workflow task requests we will perform at a time on this worker's task queue. @@ -202,7 +205,7 @@ def __init__( workflow collect its tasks properly, the worker will simply let the Python garbage collector collect the tasks. WARNING: Users should not set this value to true. The garbage collector will - throw ``GeneratorExit`` in coroutines causing them to to wake up + throw ``GeneratorExit`` in coroutines causing them to wake up in different threads and run ``finally`` and other code in the wrong workflow environment. """ @@ -276,7 +279,9 @@ def __init__( # concurrent activities. We do this here instead of in # _ActivityWorker so the stack level is predictable. max_workers = getattr(activity_executor, "_max_workers", None) - if isinstance(max_workers, int) and max_workers < max_concurrent_activities: + if isinstance(max_workers, int) and max_workers < ( + max_concurrent_activities or 0 + ): warnings.warn( f"Worker max_concurrent_activities is {max_concurrent_activities} " + f"but activity_executor's max_workers is only {max_workers}", @@ -313,6 +318,39 @@ def __init__( disable_safe_eviction=disable_safe_workflow_eviction, ) + workflow_slot_supplier: temporalio.bridge.worker.SlotSupplier + activity_slot_supplier: temporalio.bridge.worker.SlotSupplier + local_activity_slot_supplier: temporalio.bridge.worker.SlotSupplier + + if tuner is not None: + workflow_slot_supplier = _to_bridge_slot_supplier( + tuner.get_workflow_task_slot_supplier(), "workflow" + ) + activity_slot_supplier = _to_bridge_slot_supplier( + tuner.get_activity_task_slot_supplier(), "activity" + ) + local_activity_slot_supplier = _to_bridge_slot_supplier( + tuner.get_local_activity_task_slot_supplier(), "local_activity" + ) + else: + workflow_slot_supplier = temporalio.bridge.worker.FixedSizeSlotSupplier( + max_concurrent_workflow_tasks if max_concurrent_workflow_tasks else 100 + ) + activity_slot_supplier = temporalio.bridge.worker.FixedSizeSlotSupplier( + max_concurrent_activities if max_concurrent_activities else 100 + ) + local_activity_slot_supplier = ( + temporalio.bridge.worker.FixedSizeSlotSupplier( + max_concurrent_local_activities + if max_concurrent_local_activities + else 100 + ) + ) + + bridge_tuner = temporalio.bridge.worker.TunerHolder( + workflow_slot_supplier, activity_slot_supplier, local_activity_slot_supplier + ) + # Create bridge worker last. We have empirically observed that if it is # created before an error is raised from the activity worker # constructor, a deadlock/hang will occur presumably while trying to @@ -328,9 +366,7 @@ def __init__( build_id=build_id or load_default_build_id(), identity_override=identity, max_cached_workflows=max_cached_workflows, - max_outstanding_workflow_tasks=max_concurrent_workflow_tasks, - max_outstanding_activities=max_concurrent_activities, - max_outstanding_local_activities=max_concurrent_local_activities, + tuner=bridge_tuner, max_concurrent_workflow_task_polls=max_concurrent_workflow_task_polls, nonsticky_to_sticky_poll_ratio=nonsticky_to_sticky_poll_ratio, max_concurrent_activity_task_polls=max_concurrent_activity_task_polls, @@ -613,9 +649,10 @@ class WorkerConfig(TypedDict, total=False): build_id: Optional[str] identity: Optional[str] max_cached_workflows: int - max_concurrent_workflow_tasks: int - max_concurrent_activities: int - max_concurrent_local_activities: int + max_concurrent_workflow_tasks: Optional[int] + max_concurrent_activities: Optional[int] + max_concurrent_local_activities: Optional[int] + tuner: Optional[WorkerTuner] max_concurrent_workflow_task_polls: int nonsticky_to_sticky_poll_ratio: float max_concurrent_activity_task_polls: int From 1929bdaaa8ed2e89bb62a3ed4ca7c0693e74fa90 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 24 Jun 2024 14:17:18 -0700 Subject: [PATCH 02/10] Conversions are compiling / basic tests running --- temporalio/bridge/Cargo.lock | 332 ++++++++++++++++++++------------ temporalio/bridge/Cargo.toml | 7 +- temporalio/bridge/src/worker.rs | 43 +++-- temporalio/worker/_tuning.py | 4 +- 4 files changed, 240 insertions(+), 146 deletions(-) diff --git a/temporalio/bridge/Cargo.lock b/temporalio/bridge/Cargo.lock index cdbe111b6..b96f49584 100644 --- a/temporalio/bridge/Cargo.lock +++ b/temporalio/bridge/Cargo.lock @@ -95,7 +95,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -106,7 +106,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -143,7 +143,7 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", + "sync_wrapper 0.1.2", "tower", "tower-layer", "tower-service", @@ -179,9 +179,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.72" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -266,9 +266,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "c891175c3fb232128f48de6590095e59198bbeb8620c310be349bfc3afd12c7b" dependencies = [ "jobserver", "libc", @@ -436,7 +436,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.66", + "syn", ] [[package]] @@ -447,7 +447,7 @@ checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -486,7 +486,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -507,7 +507,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -517,20 +517,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.66", + "syn", ] [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn", ] [[package]] @@ -546,13 +546,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -584,7 +584,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -596,7 +596,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -736,7 +736,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -893,6 +893,12 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -959,12 +965,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", "http-body 1.0.0", "pin-project-lite", @@ -972,9 +978,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -1029,19 +1035,20 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.26.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", "hyper 1.3.1", "hyper-util", - "rustls", + "rustls 0.23.10", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tower-service", + "webpki-roots", ] [[package]] @@ -1114,9 +1121,9 @@ dependencies = [ [[package]] name = "indoc" -version = "1.0.9" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "inout" @@ -1192,9 +1199,9 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" @@ -1266,9 +1273,9 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -1287,9 +1294,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -1329,7 +1336,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1396,9 +1403,9 @@ dependencies = [ [[package]] name = "object" -version = "0.35.0" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +checksum = "576dfe1fc8f9df304abb159d767a29d0476f7750fbf8aa7ad07816004a207434" dependencies = [ "memchr", ] @@ -1527,7 +1534,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.2", "smallvec", "windows-targets 0.52.5", ] @@ -1584,7 +1591,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1656,14 +1663,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2", - "syn 2.0.66", + "syn", ] [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -1700,7 +1707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck", + "heck 0.5.0", "itertools 0.12.1", "log", "multimap", @@ -1710,7 +1717,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.66", + "syn", "tempfile", ] @@ -1724,7 +1731,7 @@ dependencies = [ "itertools 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -1757,7 +1764,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598b7365952c2ed4e32902de0533653aafbe5ae3da436e8e2335c7d375a1cef3" dependencies = [ - "heck", + "heck 0.5.0", "prost", "prost-build", "prost-types", @@ -1790,15 +1797,17 @@ checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "pyo3" -version = "0.19.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ + "anyhow", "cfg-if", "indoc", "libc", "memoffset", "parking_lot", + "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", @@ -1807,9 +1816,9 @@ dependencies = [ [[package]] name = "pyo3-asyncio" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2cc34c1f907ca090d7add03dc523acdd91f3a4dab12286604951e2f5152edad" +checksum = "6ea6b68e93db3622f3bb3bf363246cf948ed5375afe7abff98ccbdd50b184995" dependencies = [ "futures", "once_cell", @@ -1820,9 +1829,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.19.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" dependencies = [ "once_cell", "target-lexicon", @@ -1830,9 +1839,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.19.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" dependencies = [ "libc", "pyo3-build-config", @@ -1840,32 +1849,34 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.19.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "pyo3-macros-backend" -version = "0.19.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" dependencies = [ + "heck 0.4.1", "proc-macro2", + "pyo3-build-config", "quote", - "syn 1.0.109", + "syn", ] [[package]] name = "pythonize" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e35b716d430ace57e2d1b4afb51c9e5b7c46d2bce72926e07f9be6a98ced03e" +checksum = "ffd1c3ef39c725d63db5f9bc455461bafd80540cb7824c61afb823501921a850" dependencies = [ "pyo3", "serde", @@ -1886,6 +1897,53 @@ dependencies = [ "winapi", ] +[[package]] +name = "quinn" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.10", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash", + "rustls 0.23.10", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -1965,23 +2023,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ "bitflags 2.5.0", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -1995,13 +2053,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -2012,15 +2070,15 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "base64 0.22.1", "bytes", @@ -2039,15 +2097,16 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls", + "quinn", + "rustls 0.23.10", "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.1", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tokio-util", "tower-service", "url", @@ -2089,6 +2148,12 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -2114,7 +2179,7 @@ dependencies = [ "proc-macro2", "quote", "rustfsm_trait", - "syn 2.0.66", + "syn", ] [[package]] @@ -2148,6 +2213,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls" +version = "0.23.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.7.0" @@ -2261,7 +2340,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -2385,15 +2464,15 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" dependencies = [ "proc-macro2", "quote", @@ -2401,21 +2480,16 @@ dependencies = [ ] [[package]] -name = "syn" -version = "2.0.66" +name = "sync_wrapper" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "sysinfo" @@ -2434,9 +2508,9 @@ dependencies = [ [[package]] name = "tar" -version = "0.4.40" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" dependencies = [ "filetime", "libc", @@ -2494,6 +2568,7 @@ dependencies = [ name = "temporal-sdk-bridge" version = "0.1.0" dependencies = [ + "anyhow", "futures", "log", "once_cell", @@ -2634,7 +2709,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -2718,7 +2793,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -2727,7 +2802,18 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.10", "rustls-pki-types", "tokio", ] @@ -2779,7 +2865,7 @@ dependencies = [ "rustls-pemfile", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.25.0", "tokio-stream", "tower", "tower-layer", @@ -2797,7 +2883,7 @@ dependencies = [ "proc-macro2", "prost-build", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -2852,7 +2938,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -2934,7 +3020,7 @@ checksum = "ac73887f47b9312552aa90ef477927ff014d63d1920ca8037c6c1951eab64bb1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -2960,9 +3046,9 @@ dependencies = [ [[package]] name = "unindent" -version = "0.1.11" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" [[package]] name = "untrusted" @@ -2972,9 +3058,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -2983,9 +3069,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "3ea73390fe27785838dcbf75b91b1d84799e28f1ce71e6f372a5dc2200c80de5" dependencies = [ "getrandom", ] @@ -3038,7 +3124,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn", "wasm-bindgen-shared", ] @@ -3072,7 +3158,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3108,9 +3194,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c452ad30530b54a4d8e71952716a212b08efd0f3562baa66c29a618b07da7c3" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -3333,7 +3419,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] @@ -3353,14 +3439,14 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn", ] [[package]] name = "zip" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "098d5d7737fb0b70814faa73c17df84f047d38dd31d13bbf2ec3fb354b5abf45" +checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" dependencies = [ "aes", "arbitrary", @@ -3419,9 +3505,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "2.0.10+zstd.1.5.6" +version = "2.0.11+zstd.1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +checksum = "75652c55c0b6f3e6f12eb786fe1bc960396bf05a1eb3bf1f3691c3610ac2e6d4" dependencies = [ "cc", "pkg-config", diff --git a/temporalio/bridge/Cargo.toml b/temporalio/bridge/Cargo.toml index 1e5d1335b..852188a29 100644 --- a/temporalio/bridge/Cargo.toml +++ b/temporalio/bridge/Cargo.toml @@ -8,14 +8,15 @@ name = "temporal_sdk_bridge" crate-type = ["cdylib"] [dependencies] +anyhow = "1.0" futures = "0.3" log = "0.4" once_cell = "1.16" prost = "0.12" prost-types = "0.12" -pyo3 = { version = "0.19", features = ["extension-module", "abi3-py38"] } -pyo3-asyncio = { version = "0.19", features = ["tokio-runtime"] } -pythonize = "0.19" +pyo3 = { version = "0.20", features = ["extension-module", "abi3-py38", "anyhow"] } +pyo3-asyncio = { version = "0.20", features = ["tokio-runtime"] } +pythonize = "0.20" temporal-client = { version = "0.1.0", path = "./sdk-core/client" } temporal-sdk-core = { version = "0.1.0", path = "./sdk-core/core", features = ["ephemeral-server"] } temporal-sdk-core-api = { version = "0.1.0", path = "./sdk-core/core-api" } diff --git a/temporalio/bridge/src/worker.rs b/temporalio/bridge/src/worker.rs index e955619fd..b600cbf47 100644 --- a/temporalio/bridge/src/worker.rs +++ b/temporalio/bridge/src/worker.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use prost::Message; use pyo3::exceptions::{PyException, PyRuntimeError, PyValueError}; use pyo3::prelude::*; @@ -59,15 +60,21 @@ pub struct TunerHolder { #[derive(FromPyObject)] pub enum SlotSupplier { - FixedSize(usize), + FixedSize(FixedSizeSlotSupplier), ResourceBased(ResourceBasedSlotSupplier), } +#[derive(FromPyObject)] +pub struct FixedSizeSlotSupplier { + num_slots: usize, +} + #[derive(FromPyObject)] pub struct ResourceBasedSlotSupplier { minimum_slots: usize, maximum_slots: usize, - ramp_throttle: Duration, + // Need pyo3 0.21+ for this to be std Duration + ramp_throttle_ms: u64, tuner_options: ResourceBasedTunerOptions, } @@ -98,7 +105,7 @@ pub fn new_worker( config, client.retry_client.clone().into_inner(), ) - .map_err(|err| PyValueError::new_err(format!("Failed creating worker: {}", err)))?; + .context("Failed creating worker")?; Ok(WorkerRef { worker: Some(Arc::new(worker)), runtime: runtime_ref.runtime.clone(), @@ -132,9 +139,11 @@ impl WorkerRef { fn validate<'p>(&self, py: Python<'p>) -> PyResult<&'p PyAny> { let worker = self.worker.as_ref().unwrap().clone(); self.runtime.future_into_py(py, async move { - worker.validate().await.map_err(|err| { - PyRuntimeError::new_err(format!("Worker validation failed: {}", err)) - }) + worker + .validate() + .await + .context("Worker validation failed") + .map_err(Into::into) }) } @@ -176,10 +185,8 @@ impl WorkerRef { worker .complete_workflow_activation(completion) .await - .map_err(|err| { - // TODO(cretz): More error types - PyRuntimeError::new_err(format!("Completion failure: {}", err)) - }) + .context("Completion failure") + .map_err(Into::into) }) } @@ -191,10 +198,8 @@ impl WorkerRef { worker .complete_activity_task(completion) .await - .map_err(|err| { - // TODO(cretz): More error types - PyRuntimeError::new_err(format!("Completion failure: {}", err)) - }) + .context("Completion failure") + .map_err(Into::into) }) } @@ -359,7 +364,7 @@ impl TryFrom for temporal_sdk_core::TunerHolder { .build() .map_err(|e| PyValueError::new_err(format!("Invalid tuner holder options: {}", e)))? .build_tuner_holder() - .map_err(|e| PyRuntimeError::new_err(format!("Failed to build tuner holder: {}", e)))?) + .context("Failed building tuner holder")?) } } @@ -368,15 +373,15 @@ impl TryFrom for temporal_sdk_core::SlotSupplierOptions { fn try_from(supplier: SlotSupplier) -> PyResult { Ok(match supplier { - SlotSupplier::FixedSize(size) => { - temporal_sdk_core::SlotSupplierOptions::FixedSize { slots: size } - } + SlotSupplier::FixedSize(fs) => temporal_sdk_core::SlotSupplierOptions::FixedSize { + slots: fs.num_slots, + }, SlotSupplier::ResourceBased(ss) => { temporal_sdk_core::SlotSupplierOptions::ResourceBased( temporal_sdk_core::ResourceSlotOptions::new( ss.minimum_slots, ss.maximum_slots, - ss.ramp_throttle, + Duration::from_millis(ss.ramp_throttle_ms), ), ) } diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index 068b4eb40..f291213cd 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -1,7 +1,9 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import timedelta -from typing import Literal, Optional, TypeAlias, Union +from typing import Literal, Optional, Union + +from typing_extensions import TypeAlias import temporalio.bridge.worker From 80e41eea85b3afbed6ccca96118fe415675e87cc Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 24 Jun 2024 15:20:17 -0700 Subject: [PATCH 03/10] Added basic tests --- temporalio/bridge/worker.py | 2 +- temporalio/worker/__init__.py | 16 ++++++++- temporalio/worker/_tuning.py | 2 +- tests/worker/test_worker.py | 66 ++++++++++++++++++++++++++++++++++- 4 files changed, 82 insertions(+), 4 deletions(-) diff --git a/temporalio/bridge/worker.py b/temporalio/bridge/worker.py index b6f44b6e7..38bf29cec 100644 --- a/temporalio/bridge/worker.py +++ b/temporalio/bridge/worker.py @@ -71,7 +71,7 @@ class ResourceBasedTunerOptions: class ResourceBasedSlotSupplier: minimum_slots: int maximum_slots: int - ramp_throttle: timedelta + ramp_throttle_ms: int tuner_options: ResourceBasedTunerOptions diff --git a/temporalio/worker/__init__.py b/temporalio/worker/__init__.py index b5b94bb66..5da35c114 100644 --- a/temporalio/worker/__init__.py +++ b/temporalio/worker/__init__.py @@ -26,7 +26,15 @@ WorkflowReplayResult, WorkflowReplayResults, ) -from ._tuning import WorkerTuner +from ._tuning import ( + CompositeTuner, + FixedSizeSlotSupplier, + ResourceBasedSlotOptions, + ResourceBasedSlotSupplier, + ResourceBasedTuner, + ResourceBasedTunerOptions, + WorkerTuner, +) from ._worker import Worker, WorkerConfig from ._workflow_instance import ( UnsandboxedWorkflowRunner, @@ -72,4 +80,10 @@ "UnsandboxedWorkflowRunner", # Tuning types "WorkerTuner", + "ResourceBasedTuner", + "CompositeTuner", + "FixedSizeSlotSupplier", + "ResourceBasedSlotSupplier", + "ResourceBasedTunerOptions", + "ResourceBasedSlotOptions", ] diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index f291213cd..0d8cf3317 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -60,7 +60,7 @@ def _to_bridge_slot_supplier( return temporalio.bridge.worker.ResourceBasedSlotSupplier( min_slots, max_slots, - ramp_throttle, + int(ramp_throttle / timedelta(milliseconds=1)), temporalio.bridge.worker.ResourceBasedTunerOptions( slot_supplier.tuner_options.target_memory_usage, slot_supplier.tuner_options.target_cpu_usage, diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index 8389435b4..7077075cc 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -11,7 +11,15 @@ from temporalio import activity, workflow from temporalio.client import BuildIdOpAddNewDefault, Client, TaskReachabilityType from temporalio.testing import WorkflowEnvironment -from temporalio.worker import Worker +from temporalio.worker import ( + ResourceBasedSlotOptions, + ResourceBasedTuner, + ResourceBasedTunerOptions, + Worker, + CompositeTuner, + ResourceBasedSlotSupplier, + FixedSizeSlotSupplier, +) from temporalio.workflow import VersioningIntent from tests.helpers import new_worker, worker_versioning_enabled @@ -228,6 +236,62 @@ async def test_worker_validate_fail(client: Client, env: WorkflowEnvironment): assert str(err.value).startswith("Worker validation failed") +async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnvironment): + tuner = ResourceBasedTuner(ResourceBasedTunerOptions(0.5, 0.5)) + tuner.set_workflow_task_options( + ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)) + ) + async with new_worker( + client, + WaitOnSignalWorkflow, + activities=[say_hello], + tuner=tuner, + ) as w: + wf1 = await client.start_workflow( + WaitOnSignalWorkflow.run, + id=f"resource-based-{uuid.uuid4()}", + task_queue=w.task_queue, + ) + await wf1.signal(WaitOnSignalWorkflow.my_signal, "finish") + await wf1.result() + + +async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvironment): + resource_based_options = ResourceBasedTunerOptions(0.5, 0.5) + tuner = CompositeTuner( + FixedSizeSlotSupplier(5), + ResourceBasedSlotSupplier( + ResourceBasedSlotOptions( + minimum_slots=1, + maximum_slots=20, + ramp_throttle=timedelta(milliseconds=60), + ), + resource_based_options, + ), + ResourceBasedSlotSupplier( + ResourceBasedSlotOptions( + minimum_slots=1, + maximum_slots=20, + ramp_throttle=timedelta(milliseconds=60), + ), + resource_based_options, + ), + ) + async with new_worker( + client, + WaitOnSignalWorkflow, + activities=[say_hello], + tuner=tuner, + ) as w: + wf1 = await client.start_workflow( + WaitOnSignalWorkflow.run, + id=f"composite-tuner-{uuid.uuid4()}", + task_queue=w.task_queue, + ) + await wf1.signal(WaitOnSignalWorkflow.my_signal, "finish") + await wf1.result() + + def create_worker( client: Client, on_fatal_error: Optional[Callable[[BaseException], Awaitable[None]]] = None, From 3e2a000b004a2d55d851da0557744e57c29517a3 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 24 Jun 2024 15:44:35 -0700 Subject: [PATCH 04/10] Add docstrings --- temporalio/bridge/worker.py | 8 +++++ temporalio/worker/_tuning.py | 62 ++++++++++++++++++++++++++++++------ temporalio/worker/_worker.py | 26 +++++++++------ tests/worker/test_worker.py | 25 ++++++++++++--- 4 files changed, 98 insertions(+), 23 deletions(-) diff --git a/temporalio/bridge/worker.py b/temporalio/bridge/worker.py index 38bf29cec..3b11a132b 100644 --- a/temporalio/bridge/worker.py +++ b/temporalio/bridge/worker.py @@ -63,12 +63,16 @@ class WorkerConfig: @dataclass class ResourceBasedTunerOptions: + """Python representation of the Rust struct for configuring a resource-based tuner.""" + target_memory_usage: float target_cpu_usage: float @dataclass class ResourceBasedSlotSupplier: + """Python representation of the Rust struct for a resource-based slot supplier.""" + minimum_slots: int maximum_slots: int ramp_throttle_ms: int @@ -77,6 +81,8 @@ class ResourceBasedSlotSupplier: @dataclass(frozen=True) class FixedSizeSlotSupplier: + """Python representation of the Rust struct for a fixed-size slot supplier.""" + num_slots: int @@ -85,6 +91,8 @@ class FixedSizeSlotSupplier: @dataclass class TunerHolder: + """Python representation of the Rust struct for a tuner holder.""" + workflow_slot_supplier: SlotSupplier activity_slot_supplier: SlotSupplier local_activity_slot_supplier: SlotSupplier diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index 0d8cf3317..941f174fc 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -10,11 +10,20 @@ @dataclass(frozen=True) class FixedSizeSlotSupplier: + """A fixed-size slot supplier that will never issue more than a fixed number of slots.""" + num_slots: int + """The maximum number of slots that can be issued""" @dataclass(frozen=True) class ResourceBasedTunerOptions: + """Options for a :py:class:`ResourceBasedTuner` or a :py:class:`ResourceBasedSlotSupplier`. + + .. warning:: + The resource based tuner is currently experimental. + """ + target_memory_usage: float """A value between 0 and 1 that represents the target (system) memory usage. It's not recommended to set this higher than 0.8, since how much memory a workflow may use is not predictable, and @@ -26,15 +35,34 @@ class ResourceBasedTunerOptions: @dataclass(frozen=True) class ResourceBasedSlotOptions: + """Options for a specific slot type being used with a :py:class:`ResourceBasedSlotSupplier`. + + .. warning:: + The resource based tuner is currently experimental. + """ + minimum_slots: Optional[int] + """Amount of slots that will be issued regardless of any other checks""" maximum_slots: Optional[int] + """Maximum amount of slots permitted""" ramp_throttle: Optional[timedelta] + """Minimum time we will wait (after passing the minimum slots number) between handing out new slots in milliseconds. + This value matters because how many resources a task will use cannot be determined ahead of time, and thus the + system should wait to see how much resources are used before issuing more slots.""" @dataclass(frozen=True) class ResourceBasedSlotSupplier: + """A slot supplier that will dynamically adjust the number of slots based on resource usage. + + .. warning:: + The resource based tuner is currently experimental. + """ + slot_options: ResourceBasedSlotOptions tuner_options: ResourceBasedTunerOptions + """Options for the tuner that will be used to adjust the number of slots. When used with a + :py:class:`CompositeTuner`, all resource-based slot suppliers must use the same tuner options.""" SlotSupplier: TypeAlias = Union[FixedSizeSlotSupplier, ResourceBasedSlotSupplier] @@ -74,47 +102,61 @@ class WorkerTuner(ABC): """WorkerTuners allow for the dynamic customization of some aspects of worker configuration""" @abstractmethod - def get_workflow_task_slot_supplier(self) -> SlotSupplier: + def _get_workflow_task_slot_supplier(self) -> SlotSupplier: raise NotImplementedError @abstractmethod - def get_activity_task_slot_supplier(self) -> SlotSupplier: + def _get_activity_task_slot_supplier(self) -> SlotSupplier: raise NotImplementedError @abstractmethod - def get_local_activity_task_slot_supplier(self) -> SlotSupplier: + def _get_local_activity_task_slot_supplier(self) -> SlotSupplier: raise NotImplementedError class ResourceBasedTuner(WorkerTuner): + """This tuner attempts to maintain certain levels of resource usage when under load. + + .. warning:: + The resource based tuner is currently experimental. + """ + def __init__(self, options: ResourceBasedTunerOptions): + """Create a new ResourceBasedTuner + + Args: + options: Specify the target resource usage levels for the tuner + """ self.options = options self.workflow_task_options: Optional[ResourceBasedSlotOptions] = None self.activity_task_options: Optional[ResourceBasedSlotOptions] = None self.local_activity_task_options: Optional[ResourceBasedSlotOptions] = None def set_workflow_task_options(self, options: ResourceBasedSlotOptions): + """Set options for the workflow task slot supplier""" self.workflow_task_options = options def set_activity_task_options(self, options: ResourceBasedSlotOptions): + """Set options for the activity task slot supplier""" self.activity_task_options = options def set_local_activity_task_options(self, options: ResourceBasedSlotOptions): + """Set options for the local activity task slot supplier""" self.local_activity_task_options = options - def get_workflow_task_slot_supplier(self) -> SlotSupplier: + def _get_workflow_task_slot_supplier(self) -> SlotSupplier: return ResourceBasedSlotSupplier( self.workflow_task_options or ResourceBasedSlotOptions(None, None, None), self.options, ) - def get_activity_task_slot_supplier(self) -> SlotSupplier: + def _get_activity_task_slot_supplier(self) -> SlotSupplier: return ResourceBasedSlotSupplier( self.activity_task_options or ResourceBasedSlotOptions(None, None, None), self.options, ) - def get_local_activity_task_slot_supplier(self) -> SlotSupplier: + def _get_local_activity_task_slot_supplier(self) -> SlotSupplier: return ResourceBasedSlotSupplier( self.local_activity_task_options or ResourceBasedSlotOptions(None, None, None), @@ -124,15 +166,17 @@ def get_local_activity_task_slot_supplier(self) -> SlotSupplier: @dataclass(frozen=True) class CompositeTuner(WorkerTuner): + """This tuner allows for different slot suppliers for different slot types.""" + workflow_slot_supplier: SlotSupplier activity_slot_supplier: SlotSupplier local_activity_slot_supplier: SlotSupplier - def get_workflow_task_slot_supplier(self) -> SlotSupplier: + def _get_workflow_task_slot_supplier(self) -> SlotSupplier: return self.workflow_slot_supplier - def get_activity_task_slot_supplier(self) -> SlotSupplier: + def _get_activity_task_slot_supplier(self) -> SlotSupplier: return self.activity_slot_supplier - def get_local_activity_task_slot_supplier(self) -> SlotSupplier: + def _get_local_activity_task_slot_supplier(self) -> SlotSupplier: return self.local_activity_slot_supplier diff --git a/temporalio/worker/_worker.py b/temporalio/worker/_worker.py index d746d2791..96e8d2856 100644 --- a/temporalio/worker/_worker.py +++ b/temporalio/worker/_worker.py @@ -128,12 +128,16 @@ def __init__( max_cached_workflows: If nonzero, workflows will be cached and sticky task queues will be used. max_concurrent_workflow_tasks: Maximum allowed number of workflow - tasks that will ever be given to this worker at one time. + tasks that will ever be given to this worker at one time. Mutually exclusive with ``tuner``. max_concurrent_activities: Maximum number of activity tasks that - will ever be given to this worker concurrently. + will ever be given to this worker concurrently. Mutually exclusive with ``tuner``. max_concurrent_local_activities: Maximum number of local activity - tasks that will ever be given to this worker concurrently. - tuner: TODO + tasks that will ever be given to this worker concurrently. Mutually exclusive with ``tuner``. + tuner: Provide a custom :py:class:`WorkerTuner`. Mutually exclusive with the + ``max_concurrent_workflow_tasks``, ``max_concurrent_activities``, and + ``max_concurrent_local_activities`` arguments. + + WARNING: This argument is experimental max_concurrent_workflow_task_polls: Maximum number of concurrent poll workflow task requests we will perform at a time on this worker's task queue. @@ -324,13 +328,13 @@ def __init__( if tuner is not None: workflow_slot_supplier = _to_bridge_slot_supplier( - tuner.get_workflow_task_slot_supplier(), "workflow" + tuner._get_workflow_task_slot_supplier(), "workflow" ) activity_slot_supplier = _to_bridge_slot_supplier( - tuner.get_activity_task_slot_supplier(), "activity" + tuner._get_activity_task_slot_supplier(), "activity" ) local_activity_slot_supplier = _to_bridge_slot_supplier( - tuner.get_local_activity_task_slot_supplier(), "local_activity" + tuner._get_local_activity_task_slot_supplier(), "local_activity" ) else: workflow_slot_supplier = temporalio.bridge.worker.FixedSizeSlotSupplier( @@ -394,9 +398,11 @@ def __init__( # per workflow type nondeterminism_as_workflow_fail=self._workflow_worker is not None and self._workflow_worker.nondeterminism_as_workflow_fail(), - nondeterminism_as_workflow_fail_for_types=self._workflow_worker.nondeterminism_as_workflow_fail_for_types() - if self._workflow_worker - else set(), + nondeterminism_as_workflow_fail_for_types=( + self._workflow_worker.nondeterminism_as_workflow_fail_for_types() + if self._workflow_worker + else set() + ), ), ) diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index 7077075cc..ad7011ac6 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -12,13 +12,13 @@ from temporalio.client import BuildIdOpAddNewDefault, Client, TaskReachabilityType from temporalio.testing import WorkflowEnvironment from temporalio.worker import ( + CompositeTuner, + FixedSizeSlotSupplier, ResourceBasedSlotOptions, + ResourceBasedSlotSupplier, ResourceBasedTuner, ResourceBasedTunerOptions, Worker, - CompositeTuner, - ResourceBasedSlotSupplier, - FixedSizeSlotSupplier, ) from temporalio.workflow import VersioningIntent from tests.helpers import new_worker, worker_versioning_enabled @@ -271,7 +271,7 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir ResourceBasedSlotSupplier( ResourceBasedSlotOptions( minimum_slots=1, - maximum_slots=20, + maximum_slots=5, ramp_throttle=timedelta(milliseconds=60), ), resource_based_options, @@ -292,6 +292,23 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir await wf1.result() +async def test_cant_specify_max_concurrent_and_tuner( + client: Client, env: WorkflowEnvironment +): + tuner = ResourceBasedTuner(ResourceBasedTunerOptions(0.5, 0.5)) + tuner.set_workflow_task_options( + ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)) + ) + async with new_worker( + client, + WaitOnSignalWorkflow, + activities=[say_hello], + tuner=tuner, + max_concurrent_workflow_tasks=10, + ): + pass + + def create_worker( client: Client, on_fatal_error: Optional[Callable[[BaseException], Awaitable[None]]] = None, From cf2593315e7d679492313271c8aee67558fda9df Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 24 Jun 2024 16:02:10 -0700 Subject: [PATCH 05/10] Fix the mutual exclusion test --- temporalio/worker/_worker.py | 10 ++++++++++ tests/worker/test_worker.py | 20 ++++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/temporalio/worker/_worker.py b/temporalio/worker/_worker.py index 96e8d2856..feae289ea 100644 --- a/temporalio/worker/_worker.py +++ b/temporalio/worker/_worker.py @@ -327,6 +327,16 @@ def __init__( local_activity_slot_supplier: temporalio.bridge.worker.SlotSupplier if tuner is not None: + if ( + max_concurrent_workflow_tasks + or max_concurrent_activities + or max_concurrent_local_activities + ): + raise ValueError( + "Cannot specify max_concurrent_workflow_tasks, max_concurrent_activities, " + "or max_concurrent_local_activities when also specifying tuner" + ) + workflow_slot_supplier = _to_bridge_slot_supplier( tuner._get_workflow_task_slot_supplier(), "workflow" ) diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index ad7011ac6..f2f563cbc 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -299,14 +299,18 @@ async def test_cant_specify_max_concurrent_and_tuner( tuner.set_workflow_task_options( ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)) ) - async with new_worker( - client, - WaitOnSignalWorkflow, - activities=[say_hello], - tuner=tuner, - max_concurrent_workflow_tasks=10, - ): - pass + + with pytest.raises(ValueError) as err: + async with new_worker( + client, + WaitOnSignalWorkflow, + activities=[say_hello], + tuner=tuner, + max_concurrent_workflow_tasks=10, + ): + pass + assert "Cannot specify " in str(err.value) + assert "when also specifying tuner" in str(err.value) def create_worker( From bcd826240d65ac4fc6d714f8fc6674185cd1d2a8 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Mon, 24 Jun 2024 16:06:01 -0700 Subject: [PATCH 06/10] Proper default specification for slot opions --- temporalio/worker/_tuning.py | 13 ++++++++----- tests/worker/test_worker.py | 2 ++ 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index 941f174fc..6c7d28188 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -41,12 +41,15 @@ class ResourceBasedSlotOptions: The resource based tuner is currently experimental. """ - minimum_slots: Optional[int] - """Amount of slots that will be issued regardless of any other checks""" - maximum_slots: Optional[int] - """Maximum amount of slots permitted""" - ramp_throttle: Optional[timedelta] + minimum_slots: Optional[int] = None + """Amount of slots that will be issued regardless of any other checks. Defaults to 5 for workflows and 1 for + activities.""" + maximum_slots: Optional[int] = None + """Maximum amount of slots permitted. Defaults to 500.""" + ramp_throttle: Optional[timedelta] = None """Minimum time we will wait (after passing the minimum slots number) between handing out new slots in milliseconds. + Defaults to 0 for workflows and 50ms for activities. + This value matters because how many resources a task will use cannot be determined ahead of time, and thus the system should wait to see how much resources are used before issuing more slots.""" diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index f2f563cbc..eb79a5e5d 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -241,6 +241,8 @@ async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnviro tuner.set_workflow_task_options( ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)) ) + # Ensure we can assume defaults when specifying only some options + tuner.set_activity_task_options(ResourceBasedSlotOptions(minimum_slots=1)) async with new_worker( client, WaitOnSignalWorkflow, From cd18bb2cad58cc66761022323a24015a92592ed7 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 25 Jun 2024 09:55:30 -0700 Subject: [PATCH 07/10] Rename options->config --- temporalio/bridge/src/runtime.rs | 1 - temporalio/bridge/src/worker.rs | 10 +++++----- temporalio/bridge/worker.py | 4 ++-- temporalio/worker/__init__.py | 4 ++-- temporalio/worker/_tuning.py | 12 ++++++------ tests/worker/test_worker.py | 8 ++++---- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/temporalio/bridge/src/runtime.rs b/temporalio/bridge/src/runtime.rs index 949afe7af..aec8e8d7f 100644 --- a/temporalio/bridge/src/runtime.rs +++ b/temporalio/bridge/src/runtime.rs @@ -1,7 +1,6 @@ use futures::channel::mpsc::Receiver; use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::prelude::*; -use pyo3::AsPyPointer; use pythonize::pythonize; use std::collections::HashMap; use std::future::Future; diff --git a/temporalio/bridge/src/worker.rs b/temporalio/bridge/src/worker.rs index b600cbf47..c9afec734 100644 --- a/temporalio/bridge/src/worker.rs +++ b/temporalio/bridge/src/worker.rs @@ -75,11 +75,11 @@ pub struct ResourceBasedSlotSupplier { maximum_slots: usize, // Need pyo3 0.21+ for this to be std Duration ramp_throttle_ms: u64, - tuner_options: ResourceBasedTunerOptions, + tuner_config: ResourceBasedTunerConfig, } #[derive(FromPyObject, Clone, Copy, PartialEq)] -pub struct ResourceBasedTunerOptions { +pub struct ResourceBasedTunerConfig { target_memory_usage: f64, target_cpu_usage: f64, } @@ -312,19 +312,19 @@ impl TryFrom for temporal_sdk_core::TunerHolder { // Verify all resource-based options are the same if any are set let maybe_wf_resource_opts = if let SlotSupplier::ResourceBased(ref ss) = holder.workflow_slot_supplier { - Some(&ss.tuner_options) + Some(&ss.tuner_config) } else { None }; let maybe_act_resource_opts = if let SlotSupplier::ResourceBased(ref ss) = holder.activity_slot_supplier { - Some(&ss.tuner_options) + Some(&ss.tuner_config) } else { None }; let maybe_local_act_resource_opts = if let SlotSupplier::ResourceBased(ref ss) = holder.local_activity_slot_supplier { - Some(&ss.tuner_options) + Some(&ss.tuner_config) } else { None }; diff --git a/temporalio/bridge/worker.py b/temporalio/bridge/worker.py index 3b11a132b..a8171441b 100644 --- a/temporalio/bridge/worker.py +++ b/temporalio/bridge/worker.py @@ -62,7 +62,7 @@ class WorkerConfig: @dataclass -class ResourceBasedTunerOptions: +class ResourceBasedTunerConfig: """Python representation of the Rust struct for configuring a resource-based tuner.""" target_memory_usage: float @@ -76,7 +76,7 @@ class ResourceBasedSlotSupplier: minimum_slots: int maximum_slots: int ramp_throttle_ms: int - tuner_options: ResourceBasedTunerOptions + tuner_config: ResourceBasedTunerConfig @dataclass(frozen=True) diff --git a/temporalio/worker/__init__.py b/temporalio/worker/__init__.py index 5da35c114..98a72b427 100644 --- a/temporalio/worker/__init__.py +++ b/temporalio/worker/__init__.py @@ -32,7 +32,7 @@ ResourceBasedSlotOptions, ResourceBasedSlotSupplier, ResourceBasedTuner, - ResourceBasedTunerOptions, + ResourceBasedTunerConfig, WorkerTuner, ) from ._worker import Worker, WorkerConfig @@ -84,6 +84,6 @@ "CompositeTuner", "FixedSizeSlotSupplier", "ResourceBasedSlotSupplier", - "ResourceBasedTunerOptions", + "ResourceBasedTunerConfig", "ResourceBasedSlotOptions", ] diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index 6c7d28188..2fc607d02 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -17,7 +17,7 @@ class FixedSizeSlotSupplier: @dataclass(frozen=True) -class ResourceBasedTunerOptions: +class ResourceBasedTunerConfig: """Options for a :py:class:`ResourceBasedTuner` or a :py:class:`ResourceBasedSlotSupplier`. .. warning:: @@ -63,7 +63,7 @@ class ResourceBasedSlotSupplier: """ slot_options: ResourceBasedSlotOptions - tuner_options: ResourceBasedTunerOptions + tuner_config: ResourceBasedTunerConfig """Options for the tuner that will be used to adjust the number of slots. When used with a :py:class:`CompositeTuner`, all resource-based slot suppliers must use the same tuner options.""" @@ -92,9 +92,9 @@ def _to_bridge_slot_supplier( min_slots, max_slots, int(ramp_throttle / timedelta(milliseconds=1)), - temporalio.bridge.worker.ResourceBasedTunerOptions( - slot_supplier.tuner_options.target_memory_usage, - slot_supplier.tuner_options.target_cpu_usage, + temporalio.bridge.worker.ResourceBasedTunerConfig( + slot_supplier.tuner_config.target_memory_usage, + slot_supplier.tuner_config.target_cpu_usage, ), ) else: @@ -124,7 +124,7 @@ class ResourceBasedTuner(WorkerTuner): The resource based tuner is currently experimental. """ - def __init__(self, options: ResourceBasedTunerOptions): + def __init__(self, options: ResourceBasedTunerConfig): """Create a new ResourceBasedTuner Args: diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index eb79a5e5d..eb545bc2e 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -17,7 +17,7 @@ ResourceBasedSlotOptions, ResourceBasedSlotSupplier, ResourceBasedTuner, - ResourceBasedTunerOptions, + ResourceBasedTunerConfig, Worker, ) from temporalio.workflow import VersioningIntent @@ -237,7 +237,7 @@ async def test_worker_validate_fail(client: Client, env: WorkflowEnvironment): async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnvironment): - tuner = ResourceBasedTuner(ResourceBasedTunerOptions(0.5, 0.5)) + tuner = ResourceBasedTuner(ResourceBasedTunerConfig(0.5, 0.5)) tuner.set_workflow_task_options( ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)) ) @@ -259,7 +259,7 @@ async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnviro async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvironment): - resource_based_options = ResourceBasedTunerOptions(0.5, 0.5) + resource_based_options = ResourceBasedTunerConfig(0.5, 0.5) tuner = CompositeTuner( FixedSizeSlotSupplier(5), ResourceBasedSlotSupplier( @@ -297,7 +297,7 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir async def test_cant_specify_max_concurrent_and_tuner( client: Client, env: WorkflowEnvironment ): - tuner = ResourceBasedTuner(ResourceBasedTunerOptions(0.5, 0.5)) + tuner = ResourceBasedTuner(ResourceBasedTunerConfig(0.5, 0.5)) tuner.set_workflow_task_options( ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)) ) From 0c5cedf900d90c7d2e92d7d43a71e09c3ce6ceba Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 25 Jun 2024 10:31:24 -0700 Subject: [PATCH 08/10] Expose fewer things --- temporalio/worker/__init__.py | 4 -- temporalio/worker/_tuning.py | 117 ++++++++++++++++++++-------------- temporalio/worker/_worker.py | 30 ++------- tests/worker/test_worker.py | 30 ++++----- 4 files changed, 88 insertions(+), 93 deletions(-) diff --git a/temporalio/worker/__init__.py b/temporalio/worker/__init__.py index 98a72b427..7f6ab2753 100644 --- a/temporalio/worker/__init__.py +++ b/temporalio/worker/__init__.py @@ -27,11 +27,9 @@ WorkflowReplayResults, ) from ._tuning import ( - CompositeTuner, FixedSizeSlotSupplier, ResourceBasedSlotOptions, ResourceBasedSlotSupplier, - ResourceBasedTuner, ResourceBasedTunerConfig, WorkerTuner, ) @@ -80,8 +78,6 @@ "UnsandboxedWorkflowRunner", # Tuning types "WorkerTuner", - "ResourceBasedTuner", - "CompositeTuner", "FixedSizeSlotSupplier", "ResourceBasedSlotSupplier", "ResourceBasedTunerConfig", diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index 2fc607d02..139479d06 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -104,6 +104,62 @@ def _to_bridge_slot_supplier( class WorkerTuner(ABC): """WorkerTuners allow for the dynamic customization of some aspects of worker configuration""" + @staticmethod + def create_resource_based( + *, + target_memory_usage: float, + target_cpu_usage: float, + workflow_config: Optional[ResourceBasedSlotOptions] = None, + activity_config: Optional[ResourceBasedSlotOptions] = None, + local_activity_config: Optional[ResourceBasedSlotOptions] = None, + ) -> "WorkerTuner": + """Create a resource-based tuner with the provided options.""" + resource_cfg = ResourceBasedTunerConfig(target_memory_usage, target_cpu_usage) + wf = ResourceBasedSlotSupplier( + workflow_config or ResourceBasedSlotOptions(), resource_cfg + ) + act = ResourceBasedSlotSupplier( + activity_config or ResourceBasedSlotOptions(), resource_cfg + ) + local_act = ResourceBasedSlotSupplier( + local_activity_config or ResourceBasedSlotOptions(), resource_cfg + ) + return _CompositeTuner( + wf, + act, + local_act, + ) + + @staticmethod + def create_fixed( + *, + workflow_slots: Optional[int], + activity_slots: Optional[int], + local_activity_slots: Optional[int], + ) -> "WorkerTuner": + """Create a fixed-size tuner with the provided number of slots. Any unspecified slots will default to 100.""" + return _CompositeTuner( + FixedSizeSlotSupplier(workflow_slots if workflow_slots else 100), + FixedSizeSlotSupplier(activity_slots if activity_slots else 100), + FixedSizeSlotSupplier( + local_activity_slots if local_activity_slots else 100 + ), + ) + + @staticmethod + def create_composite( + *, + workflow_supplier: SlotSupplier, + activity_supplier: SlotSupplier, + local_activity_supplier: SlotSupplier, + ) -> "WorkerTuner": + """Create a tuner composed of the provided slot suppliers.""" + return _CompositeTuner( + workflow_supplier, + activity_supplier, + local_activity_supplier, + ) + @abstractmethod def _get_workflow_task_slot_supplier(self) -> SlotSupplier: raise NotImplementedError @@ -116,59 +172,22 @@ def _get_activity_task_slot_supplier(self) -> SlotSupplier: def _get_local_activity_task_slot_supplier(self) -> SlotSupplier: raise NotImplementedError - -class ResourceBasedTuner(WorkerTuner): - """This tuner attempts to maintain certain levels of resource usage when under load. - - .. warning:: - The resource based tuner is currently experimental. - """ - - def __init__(self, options: ResourceBasedTunerConfig): - """Create a new ResourceBasedTuner - - Args: - options: Specify the target resource usage levels for the tuner - """ - self.options = options - self.workflow_task_options: Optional[ResourceBasedSlotOptions] = None - self.activity_task_options: Optional[ResourceBasedSlotOptions] = None - self.local_activity_task_options: Optional[ResourceBasedSlotOptions] = None - - def set_workflow_task_options(self, options: ResourceBasedSlotOptions): - """Set options for the workflow task slot supplier""" - self.workflow_task_options = options - - def set_activity_task_options(self, options: ResourceBasedSlotOptions): - """Set options for the activity task slot supplier""" - self.activity_task_options = options - - def set_local_activity_task_options(self, options: ResourceBasedSlotOptions): - """Set options for the local activity task slot supplier""" - self.local_activity_task_options = options - - def _get_workflow_task_slot_supplier(self) -> SlotSupplier: - return ResourceBasedSlotSupplier( - self.workflow_task_options or ResourceBasedSlotOptions(None, None, None), - self.options, - ) - - def _get_activity_task_slot_supplier(self) -> SlotSupplier: - return ResourceBasedSlotSupplier( - self.activity_task_options or ResourceBasedSlotOptions(None, None, None), - self.options, - ) - - def _get_local_activity_task_slot_supplier(self) -> SlotSupplier: - return ResourceBasedSlotSupplier( - self.local_activity_task_options - or ResourceBasedSlotOptions(None, None, None), - self.options, + def _to_bridge_tuner(self) -> temporalio.bridge.worker.TunerHolder: + return temporalio.bridge.worker.TunerHolder( + _to_bridge_slot_supplier( + self._get_workflow_task_slot_supplier(), "workflow" + ), + _to_bridge_slot_supplier( + self._get_activity_task_slot_supplier(), "activity" + ), + _to_bridge_slot_supplier( + self._get_local_activity_task_slot_supplier(), "local_activity" + ), ) @dataclass(frozen=True) -class CompositeTuner(WorkerTuner): +class _CompositeTuner(WorkerTuner): """This tuner allows for different slot suppliers for different slot types.""" workflow_slot_supplier: SlotSupplier diff --git a/temporalio/worker/_worker.py b/temporalio/worker/_worker.py index feae289ea..949ea0d69 100644 --- a/temporalio/worker/_worker.py +++ b/temporalio/worker/_worker.py @@ -336,34 +336,14 @@ def __init__( "Cannot specify max_concurrent_workflow_tasks, max_concurrent_activities, " "or max_concurrent_local_activities when also specifying tuner" ) - - workflow_slot_supplier = _to_bridge_slot_supplier( - tuner._get_workflow_task_slot_supplier(), "workflow" - ) - activity_slot_supplier = _to_bridge_slot_supplier( - tuner._get_activity_task_slot_supplier(), "activity" - ) - local_activity_slot_supplier = _to_bridge_slot_supplier( - tuner._get_local_activity_task_slot_supplier(), "local_activity" - ) else: - workflow_slot_supplier = temporalio.bridge.worker.FixedSizeSlotSupplier( - max_concurrent_workflow_tasks if max_concurrent_workflow_tasks else 100 - ) - activity_slot_supplier = temporalio.bridge.worker.FixedSizeSlotSupplier( - max_concurrent_activities if max_concurrent_activities else 100 - ) - local_activity_slot_supplier = ( - temporalio.bridge.worker.FixedSizeSlotSupplier( - max_concurrent_local_activities - if max_concurrent_local_activities - else 100 - ) + tuner = WorkerTuner.create_fixed( + workflow_slots=max_concurrent_workflow_tasks, + activity_slots=max_concurrent_activities, + local_activity_slots=max_concurrent_local_activities, ) - bridge_tuner = temporalio.bridge.worker.TunerHolder( - workflow_slot_supplier, activity_slot_supplier, local_activity_slot_supplier - ) + bridge_tuner = tuner._to_bridge_tuner() # Create bridge worker last. We have empirically observed that if it is # created before an error is raised from the activity worker diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index eb545bc2e..60ea896ad 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -12,13 +12,12 @@ from temporalio.client import BuildIdOpAddNewDefault, Client, TaskReachabilityType from temporalio.testing import WorkflowEnvironment from temporalio.worker import ( - CompositeTuner, FixedSizeSlotSupplier, ResourceBasedSlotOptions, ResourceBasedSlotSupplier, - ResourceBasedTuner, ResourceBasedTunerConfig, Worker, + WorkerTuner, ) from temporalio.workflow import VersioningIntent from tests.helpers import new_worker, worker_versioning_enabled @@ -237,12 +236,13 @@ async def test_worker_validate_fail(client: Client, env: WorkflowEnvironment): async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnvironment): - tuner = ResourceBasedTuner(ResourceBasedTunerConfig(0.5, 0.5)) - tuner.set_workflow_task_options( - ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)) + tuner = WorkerTuner.create_resource_based( + target_memory_usage=0.5, + target_cpu_usage=0.5, + workflow_config=ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)), + # Ensure we can assume defaults when specifying only some options + activity_config=ResourceBasedSlotOptions(minimum_slots=1), ) - # Ensure we can assume defaults when specifying only some options - tuner.set_activity_task_options(ResourceBasedSlotOptions(minimum_slots=1)) async with new_worker( client, WaitOnSignalWorkflow, @@ -260,9 +260,9 @@ async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnviro async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvironment): resource_based_options = ResourceBasedTunerConfig(0.5, 0.5) - tuner = CompositeTuner( - FixedSizeSlotSupplier(5), - ResourceBasedSlotSupplier( + tuner = WorkerTuner.create_composite( + workflow_supplier=FixedSizeSlotSupplier(5), + activity_supplier=ResourceBasedSlotSupplier( ResourceBasedSlotOptions( minimum_slots=1, maximum_slots=20, @@ -270,7 +270,7 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir ), resource_based_options, ), - ResourceBasedSlotSupplier( + local_activity_supplier=ResourceBasedSlotSupplier( ResourceBasedSlotOptions( minimum_slots=1, maximum_slots=5, @@ -297,11 +297,11 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir async def test_cant_specify_max_concurrent_and_tuner( client: Client, env: WorkflowEnvironment ): - tuner = ResourceBasedTuner(ResourceBasedTunerConfig(0.5, 0.5)) - tuner.set_workflow_task_options( - ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)) + tuner = WorkerTuner.create_resource_based( + target_memory_usage=0.5, + target_cpu_usage=0.5, + workflow_config=ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)), ) - with pytest.raises(ValueError) as err: async with new_worker( client, From fc4a86d6089975d1fda5d35ac5b03cc40deada26 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 25 Jun 2024 10:56:53 -0700 Subject: [PATCH 09/10] Augment check for max workers --- temporalio/worker/_tuning.py | 12 +++++++++++- temporalio/worker/_worker.py | 7 +++++-- tests/worker/test_worker.py | 21 +++++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index 139479d06..d12207245 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -7,6 +7,8 @@ import temporalio.bridge.worker +_DEFAULT_RESOURCE_ACTIVITY_MAX = 500 + @dataclass(frozen=True) class FixedSizeSlotSupplier: @@ -78,7 +80,7 @@ def _to_bridge_slot_supplier( return temporalio.bridge.worker.FixedSizeSlotSupplier(slot_supplier.num_slots) elif isinstance(slot_supplier, ResourceBasedSlotSupplier): min_slots = 5 if kind == "workflow" else 1 - max_slots = 500 + max_slots = _DEFAULT_RESOURCE_ACTIVITY_MAX ramp_throttle = ( timedelta(seconds=0) if kind == "workflow" else timedelta(milliseconds=50) ) @@ -185,6 +187,14 @@ def _to_bridge_tuner(self) -> temporalio.bridge.worker.TunerHolder: ), ) + def _get_activities_max(self) -> Optional[int]: + ss = self._get_activity_task_slot_supplier() + if isinstance(ss, FixedSizeSlotSupplier): + return ss.num_slots + elif isinstance(ss, ResourceBasedSlotSupplier): + return ss.slot_options.maximum_slots or _DEFAULT_RESOURCE_ACTIVITY_MAX + return None + @dataclass(frozen=True) class _CompositeTuner(WorkerTuner): diff --git a/temporalio/worker/_worker.py b/temporalio/worker/_worker.py index 949ea0d69..30c0e5212 100644 --- a/temporalio/worker/_worker.py +++ b/temporalio/worker/_worker.py @@ -283,11 +283,14 @@ def __init__( # concurrent activities. We do this here instead of in # _ActivityWorker so the stack level is predictable. max_workers = getattr(activity_executor, "_max_workers", None) + concurrent_activities = max_concurrent_activities + if tuner and tuner._get_activities_max(): + concurrent_activities = tuner._get_activities_max() if isinstance(max_workers, int) and max_workers < ( - max_concurrent_activities or 0 + concurrent_activities or 0 ): warnings.warn( - f"Worker max_concurrent_activities is {max_concurrent_activities} " + f"Worker max_concurrent_activities is {concurrent_activities} " + f"but activity_executor's max_workers is only {max_workers}", stacklevel=2, ) diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index 60ea896ad..c2620fd58 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -1,6 +1,7 @@ from __future__ import annotations import asyncio +import concurrent.futures import uuid from datetime import timedelta from typing import Any, Awaitable, Callable, Optional @@ -315,6 +316,26 @@ async def test_cant_specify_max_concurrent_and_tuner( assert "when also specifying tuner" in str(err.value) +async def test_warns_when_workers_too_lot(client: Client, env: WorkflowEnvironment): + tuner = WorkerTuner.create_resource_based( + target_memory_usage=0.5, + target_cpu_usage=0.5, + ) + with concurrent.futures.ThreadPoolExecutor() as executor: + with pytest.warns( + UserWarning, + match=f"Worker max_concurrent_activities is 500 but activity_executor's max_workers is only", + ): + async with new_worker( + client, + WaitOnSignalWorkflow, + activities=[say_hello], + tuner=tuner, + activity_executor=executor, + ): + pass + + def create_worker( client: Client, on_fatal_error: Optional[Callable[[BaseException], Awaitable[None]]] = None, From b20cacde84aab436284604662b5810710f9e2c82 Mon Sep 17 00:00:00 2001 From: Spencer Judge Date: Tue, 25 Jun 2024 11:17:34 -0700 Subject: [PATCH 10/10] One last rename --- temporalio/worker/__init__.py | 4 ++-- temporalio/worker/_tuning.py | 30 +++++++++++++++--------------- tests/worker/test_worker.py | 12 ++++++------ 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/temporalio/worker/__init__.py b/temporalio/worker/__init__.py index 7f6ab2753..48fd1a1a3 100644 --- a/temporalio/worker/__init__.py +++ b/temporalio/worker/__init__.py @@ -28,7 +28,7 @@ ) from ._tuning import ( FixedSizeSlotSupplier, - ResourceBasedSlotOptions, + ResourceBasedSlotConfig, ResourceBasedSlotSupplier, ResourceBasedTunerConfig, WorkerTuner, @@ -81,5 +81,5 @@ "FixedSizeSlotSupplier", "ResourceBasedSlotSupplier", "ResourceBasedTunerConfig", - "ResourceBasedSlotOptions", + "ResourceBasedSlotConfig", ] diff --git a/temporalio/worker/_tuning.py b/temporalio/worker/_tuning.py index d12207245..a015e0eab 100644 --- a/temporalio/worker/_tuning.py +++ b/temporalio/worker/_tuning.py @@ -36,7 +36,7 @@ class ResourceBasedTunerConfig: @dataclass(frozen=True) -class ResourceBasedSlotOptions: +class ResourceBasedSlotConfig: """Options for a specific slot type being used with a :py:class:`ResourceBasedSlotSupplier`. .. warning:: @@ -64,7 +64,7 @@ class ResourceBasedSlotSupplier: The resource based tuner is currently experimental. """ - slot_options: ResourceBasedSlotOptions + slot_config: ResourceBasedSlotConfig tuner_config: ResourceBasedTunerConfig """Options for the tuner that will be used to adjust the number of slots. When used with a :py:class:`CompositeTuner`, all resource-based slot suppliers must use the same tuner options.""" @@ -84,12 +84,12 @@ def _to_bridge_slot_supplier( ramp_throttle = ( timedelta(seconds=0) if kind == "workflow" else timedelta(milliseconds=50) ) - if slot_supplier.slot_options.minimum_slots is not None: - min_slots = slot_supplier.slot_options.minimum_slots - if slot_supplier.slot_options.maximum_slots is not None: - max_slots = slot_supplier.slot_options.maximum_slots - if slot_supplier.slot_options.ramp_throttle is not None: - ramp_throttle = slot_supplier.slot_options.ramp_throttle + if slot_supplier.slot_config.minimum_slots is not None: + min_slots = slot_supplier.slot_config.minimum_slots + if slot_supplier.slot_config.maximum_slots is not None: + max_slots = slot_supplier.slot_config.maximum_slots + if slot_supplier.slot_config.ramp_throttle is not None: + ramp_throttle = slot_supplier.slot_config.ramp_throttle return temporalio.bridge.worker.ResourceBasedSlotSupplier( min_slots, max_slots, @@ -111,20 +111,20 @@ def create_resource_based( *, target_memory_usage: float, target_cpu_usage: float, - workflow_config: Optional[ResourceBasedSlotOptions] = None, - activity_config: Optional[ResourceBasedSlotOptions] = None, - local_activity_config: Optional[ResourceBasedSlotOptions] = None, + workflow_config: Optional[ResourceBasedSlotConfig] = None, + activity_config: Optional[ResourceBasedSlotConfig] = None, + local_activity_config: Optional[ResourceBasedSlotConfig] = None, ) -> "WorkerTuner": """Create a resource-based tuner with the provided options.""" resource_cfg = ResourceBasedTunerConfig(target_memory_usage, target_cpu_usage) wf = ResourceBasedSlotSupplier( - workflow_config or ResourceBasedSlotOptions(), resource_cfg + workflow_config or ResourceBasedSlotConfig(), resource_cfg ) act = ResourceBasedSlotSupplier( - activity_config or ResourceBasedSlotOptions(), resource_cfg + activity_config or ResourceBasedSlotConfig(), resource_cfg ) local_act = ResourceBasedSlotSupplier( - local_activity_config or ResourceBasedSlotOptions(), resource_cfg + local_activity_config or ResourceBasedSlotConfig(), resource_cfg ) return _CompositeTuner( wf, @@ -192,7 +192,7 @@ def _get_activities_max(self) -> Optional[int]: if isinstance(ss, FixedSizeSlotSupplier): return ss.num_slots elif isinstance(ss, ResourceBasedSlotSupplier): - return ss.slot_options.maximum_slots or _DEFAULT_RESOURCE_ACTIVITY_MAX + return ss.slot_config.maximum_slots or _DEFAULT_RESOURCE_ACTIVITY_MAX return None diff --git a/tests/worker/test_worker.py b/tests/worker/test_worker.py index c2620fd58..6af180594 100644 --- a/tests/worker/test_worker.py +++ b/tests/worker/test_worker.py @@ -14,7 +14,7 @@ from temporalio.testing import WorkflowEnvironment from temporalio.worker import ( FixedSizeSlotSupplier, - ResourceBasedSlotOptions, + ResourceBasedSlotConfig, ResourceBasedSlotSupplier, ResourceBasedTunerConfig, Worker, @@ -240,9 +240,9 @@ async def test_can_run_resource_based_worker(client: Client, env: WorkflowEnviro tuner = WorkerTuner.create_resource_based( target_memory_usage=0.5, target_cpu_usage=0.5, - workflow_config=ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)), + workflow_config=ResourceBasedSlotConfig(5, 20, timedelta(seconds=0)), # Ensure we can assume defaults when specifying only some options - activity_config=ResourceBasedSlotOptions(minimum_slots=1), + activity_config=ResourceBasedSlotConfig(minimum_slots=1), ) async with new_worker( client, @@ -264,7 +264,7 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir tuner = WorkerTuner.create_composite( workflow_supplier=FixedSizeSlotSupplier(5), activity_supplier=ResourceBasedSlotSupplier( - ResourceBasedSlotOptions( + ResourceBasedSlotConfig( minimum_slots=1, maximum_slots=20, ramp_throttle=timedelta(milliseconds=60), @@ -272,7 +272,7 @@ async def test_can_run_composite_tuner_worker(client: Client, env: WorkflowEnvir resource_based_options, ), local_activity_supplier=ResourceBasedSlotSupplier( - ResourceBasedSlotOptions( + ResourceBasedSlotConfig( minimum_slots=1, maximum_slots=5, ramp_throttle=timedelta(milliseconds=60), @@ -301,7 +301,7 @@ async def test_cant_specify_max_concurrent_and_tuner( tuner = WorkerTuner.create_resource_based( target_memory_usage=0.5, target_cpu_usage=0.5, - workflow_config=ResourceBasedSlotOptions(5, 20, timedelta(seconds=0)), + workflow_config=ResourceBasedSlotConfig(5, 20, timedelta(seconds=0)), ) with pytest.raises(ValueError) as err: async with new_worker(