From dd91bde3da2674d243019630fa4587e4670244f0 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Sat, 23 Aug 2025 00:37:05 -0400 Subject: [PATCH 01/31] envconfig impl --- temporalio/ext/Cargo.toml | 2 +- temporalio/ext/src/envconfig.rs | 215 ++++++ temporalio/ext/src/lib.rs | 2 + temporalio/lib/temporalio.rb | 1 + temporalio/lib/temporalio/cancellation.rb | 4 +- temporalio/lib/temporalio/envconfig.rb | 360 +++++++++ temporalio/test/envconfig_test.rb | 901 ++++++++++++++++++++++ 7 files changed, 1482 insertions(+), 3 deletions(-) create mode 100644 temporalio/ext/src/envconfig.rs create mode 100644 temporalio/lib/temporalio/envconfig.rb create mode 100644 temporalio/test/envconfig_test.rb diff --git a/temporalio/ext/Cargo.toml b/temporalio/ext/Cargo.toml index cd729168..73eb3475 100644 --- a/temporalio/ext/Cargo.toml +++ b/temporalio/ext/Cargo.toml @@ -17,7 +17,7 @@ prost = "0.13" rb-sys = "0.9" 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" } +temporal-sdk-core-api = { version = "0.1.0", path = "./sdk-core/core-api", features = ["envconfig"] } temporal-sdk-core-protos = { version = "0.1.0", path = "./sdk-core/sdk-core-protos" } tokio = "1.37" tokio-stream = "0.1" diff --git a/temporalio/ext/src/envconfig.rs b/temporalio/ext/src/envconfig.rs new file mode 100644 index 00000000..0e63d9d8 --- /dev/null +++ b/temporalio/ext/src/envconfig.rs @@ -0,0 +1,215 @@ +use std::collections::HashMap; + +use magnus::{Error, RHash, Ruby, function, prelude::*, scan_args, class}; +use temporal_sdk_core_api::envconfig::{ + load_client_config as core_load_client_config, + load_client_config_profile as core_load_client_config_profile, + ClientConfig as CoreClientConfig, ClientConfigCodec, ClientConfigProfile as CoreClientConfigProfile, + ClientConfigTLS as CoreClientConfigTLS, DataSource, LoadClientConfigOptions, + LoadClientConfigProfileOptions, +}; + +use crate::{ROOT_MOD, error}; + +pub fn init(ruby: &Ruby) -> Result<(), Error> { + let root_mod = ruby.get_inner(&ROOT_MOD); + + let class = root_mod.define_class("EnvConfig", class::object())?; + class.define_singleton_method("load_client_config", function!(load_client_config, -1))?; + class.define_singleton_method("load_client_connect_config", function!(load_client_connect_config, -1))?; + + Ok(()) +} + +fn data_source_to_hash(ruby: &Ruby, ds: &DataSource) -> Result { + let hash = RHash::new(); + match ds { + DataSource::Path(p) => { + hash.aset("path", ruby.str_new(p))?; + } + DataSource::Data(d) => { + hash.aset("data", ruby.str_from_slice(d))?; + } + } + Ok(hash) +} + +fn tls_to_hash(ruby: &Ruby, tls: &CoreClientConfigTLS) -> Result { + let hash = RHash::new(); + hash.aset("disabled", tls.disabled)?; + + if let Some(v) = &tls.client_cert { + hash.aset("client_cert", data_source_to_hash(ruby, v)?)?; + } + if let Some(v) = &tls.client_key { + hash.aset("client_key", data_source_to_hash(ruby, v)?)?; + } + if let Some(v) = &tls.server_ca_cert { + hash.aset("server_ca_cert", data_source_to_hash(ruby, v)?)?; + } + if let Some(v) = &tls.server_name { + hash.aset("server_name", ruby.str_new(v))?; + } + hash.aset("disable_host_verification", tls.disable_host_verification)?; + + Ok(hash) +} + +fn codec_to_hash(ruby: &Ruby, codec: &ClientConfigCodec) -> Result { + let hash = RHash::new(); + if let Some(v) = &codec.endpoint { + hash.aset("endpoint", ruby.str_new(v))?; + } + if let Some(v) = &codec.auth { + hash.aset("auth", ruby.str_new(v))?; + } + Ok(hash) +} + +fn profile_to_hash(ruby: &Ruby, profile: &CoreClientConfigProfile) -> Result { + let hash = RHash::new(); + + if let Some(v) = &profile.address { + hash.aset("address", ruby.str_new(v))?; + } + if let Some(v) = &profile.namespace { + hash.aset("namespace", ruby.str_new(v))?; + } + if let Some(v) = &profile.api_key { + hash.aset("api_key", ruby.str_new(v))?; + } + if let Some(tls) = &profile.tls { + hash.aset("tls", tls_to_hash(ruby, tls)?)?; + } + if let Some(codec) = &profile.codec { + hash.aset("codec", codec_to_hash(ruby, codec)?)?; + } + if !profile.grpc_meta.is_empty() { + let grpc_meta_hash = RHash::new(); + for (k, v) in &profile.grpc_meta { + grpc_meta_hash.aset(ruby.str_new(k), ruby.str_new(v))?; + } + hash.aset("grpc_meta", grpc_meta_hash)?; + } + + Ok(hash) +} + +fn core_config_to_hash(ruby: &Ruby, core_config: &CoreClientConfig) -> Result { + let profiles_hash = RHash::new(); + for (name, profile) in &core_config.profiles { + let profile_hash = profile_to_hash(ruby, profile)?; + profiles_hash.aset(ruby.str_new(name), profile_hash)?; + } + Ok(profiles_hash) +} + +fn load_client_config_inner( + ruby: &Ruby, + config_source: Option, + config_file_strict: bool, + disable_file: bool, + env_vars: Option>, +) -> Result { + let core_config = if disable_file { + CoreClientConfig::default() + } else { + let options = LoadClientConfigOptions { + config_source, + config_file_strict, + }; + core_load_client_config(options, env_vars.as_ref()) + .map_err(|e| error!("EnvConfig error: {}", e))? + }; + + core_config_to_hash(ruby, &core_config) +} + +fn load_client_connect_config_inner( + ruby: &Ruby, + config_source: Option, + profile: Option, + disable_file: bool, + disable_env: bool, + config_file_strict: bool, + env_vars: Option>, +) -> Result { + let options = LoadClientConfigProfileOptions { + config_source, + config_file_profile: profile, + config_file_strict, + disable_file, + disable_env, + }; + + let profile = core_load_client_config_profile(options, env_vars.as_ref()) + .map_err(|e| error!("EnvConfig error: {}", e))?; + + profile_to_hash(ruby, &profile) +} + +// load_client_config(path: String|nil, data: String|nil, disable_file: bool, config_file_strict: bool, env_vars: Hash|nil) +fn load_client_config(args: &[magnus::Value]) -> Result { + let ruby = Ruby::get().expect("Not in Ruby thread"); + let args = scan_args::scan_args::< + (Option, Option>, bool, bool), + (Option>,), + (), + (), + (), + (), + >(args)?; + let (path, data, disable_file, config_file_strict) = args.required; + let (env_vars,) = args.optional; + + let config_source = match (path, data) { + (Some(p), None) => Some(DataSource::Path(p)), + (None, Some(d)) => Some(DataSource::Data(d)), + (None, None) => None, + (Some(_), Some(_)) => { + return Err(error!("Cannot specify both path and data for config source")); + } + }; + + load_client_config_inner( + &ruby, + config_source, + config_file_strict, + disable_file, + env_vars, + ) +} + +// load_client_connect_config(profile: String|nil, path: String|nil, data: String|nil, disable_file: bool, disable_env: bool, config_file_strict: bool, env_vars: Hash|nil) +fn load_client_connect_config(args: &[magnus::Value]) -> Result { + let ruby = Ruby::get().expect("Not in Ruby thread"); + let args = scan_args::scan_args::< + (Option, Option, Option>, bool, bool, bool), + (Option>,), + (), + (), + (), + (), + >(args)?; + let (profile, path, data, disable_file, disable_env, config_file_strict) = args.required; + let (env_vars,) = args.optional; + + let config_source = match (path, data) { + (Some(p), None) => Some(DataSource::Path(p)), + (None, Some(d)) => Some(DataSource::Data(d)), + (None, None) => None, + (Some(_), Some(_)) => { + return Err(error!("Cannot specify both path and data for config source")); + } + }; + + load_client_connect_config_inner( + &ruby, + config_source, + profile, + disable_file, + disable_env, + config_file_strict, + env_vars, + ) +} \ No newline at end of file diff --git a/temporalio/ext/src/lib.rs b/temporalio/ext/src/lib.rs index 9a7f692c..9979acc1 100644 --- a/temporalio/ext/src/lib.rs +++ b/temporalio/ext/src/lib.rs @@ -2,6 +2,7 @@ use magnus::{Error, ExceptionClass, RModule, Ruby, prelude::*, value::Lazy}; mod client; mod client_rpc_generated; +mod envconfig; mod metric; mod runtime; mod testing; @@ -50,6 +51,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { Lazy::force(&ROOT_ERR, ruby); client::init(ruby)?; + envconfig::init(ruby)?; metric::init(ruby)?; runtime::init(ruby)?; testing::init(ruby)?; diff --git a/temporalio/lib/temporalio.rb b/temporalio/lib/temporalio.rb index 3bef9dcb..e5e19f3e 100644 --- a/temporalio/lib/temporalio.rb +++ b/temporalio/lib/temporalio.rb @@ -2,6 +2,7 @@ require 'temporalio/version' require 'temporalio/versioning_override' +require 'temporalio/envconfig' # Temporal Ruby SDK. See the README at https://github.com/temporalio/sdk-ruby. module Temporalio diff --git a/temporalio/lib/temporalio/cancellation.rb b/temporalio/lib/temporalio/cancellation.rb index 9b33b204..f1e12a46 100644 --- a/temporalio/lib/temporalio/cancellation.rb +++ b/temporalio/lib/temporalio/cancellation.rb @@ -167,8 +167,8 @@ def prepare_cancel(reason:) to_return.values end - def canceled_mutex_synchronize(&) - Workflow::Unsafe.illegal_call_tracing_disabled { @canceled_mutex.synchronize(&) } + def canceled_mutex_synchronize(&block) + Workflow::Unsafe.illegal_call_tracing_disabled { @canceled_mutex.synchronize(&block) } end end end diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb new file mode 100644 index 00000000..c972f3df --- /dev/null +++ b/temporalio/lib/temporalio/envconfig.rb @@ -0,0 +1,360 @@ +# frozen_string_literal: true + +require 'pathname' +require 'temporalio/internal/bridge' + +module Temporalio + module EnvConfig + # This module provides utilities to load Temporal client configuration from TOML files + # and environment variables. + # + # DataSource types: + # - Pathname: Path to a configuration file + # - String: TOML configuration content + # - nil: No configuration source + + # Convert a data source to path and data parameters for the bridge + # @param source [Pathname, String, nil] Configuration source + # @return [Array?>] Tuple of [path, data_bytes] + def self.source_to_path_and_data(source) + case source + when Pathname + [source.to_s, nil] + when String + [nil, source.encode('UTF-8').bytes] + when nil + [nil, nil] + else + raise TypeError, "config_source must be Pathname, String, or nil, got #{source.class}" + end + end + + # TLS configuration as specified as part of client configuration + # + # @!attribute [r] disabled + # @return [Boolean] If true, TLS is explicitly disabled + # @!attribute [r] server_name + # @return [String, nil] SNI override + # @!attribute [r] server_root_ca_cert + # @return [Pathname, String, nil] Server CA certificate source + # @!attribute [r] client_cert + # @return [Pathname, String, nil] Client certificate source + # @!attribute [r] client_private_key + # @return [Pathname, String, nil] Client key source + class ClientConfigTLS + attr_reader :disabled, :server_name, :server_root_ca_cert, :client_cert, :client_private_key + + # Create a ClientConfigTLS from a hash + # @param hash [Hash, nil] Hash representation + # @return [ClientConfigTLS, nil] The TLS configuration or nil if hash is nil/empty + def self.from_hash(hash) + return nil if hash.nil? || hash.empty? + + new( + disabled: hash[:disabled] || hash['disabled'] || false, + server_name: hash[:server_name] || hash['server_name'], + server_root_ca_cert: hash_to_source(hash[:server_ca_cert] || hash['server_ca_cert']), + client_cert: hash_to_source(hash[:client_cert] || hash['client_cert']), + client_private_key: hash_to_source(hash[:client_key] || hash['client_key']) + ) + end + + # Convert a hash representation to a data source + # @param hash [Hash, nil] Hash with :path or :data key + # @return [Pathname, String, nil] Data source + def self.hash_to_source(hash) + return nil if hash.nil? + + # Always expect a hash with path or data + if hash[:path] || hash['path'] + # Return path as string to match old behavior + hash[:path] || hash['path'] + elsif hash[:data] || hash['data'] + hash[:data] || hash['data'] + else + nil + end + end + + # @param disabled [Boolean] If true, TLS is explicitly disabled + # @param server_name [String, nil] SNI override + # @param server_root_ca_cert [Pathname, String, nil] Server CA certificate source + # @param client_cert [Pathname, String, nil] Client certificate source + # @param client_private_key [Pathname, String, nil] Client key source + def initialize( + disabled: false, + server_name: nil, + server_root_ca_cert: nil, + client_cert: nil, + client_private_key: nil + ) + @disabled = disabled + @server_name = server_name + @server_root_ca_cert = server_root_ca_cert + @client_cert = client_cert + @client_private_key = client_private_key + end + + # Convert to a hash that can be used for TOML serialization + # @return [Hash] Dictionary representation + def to_hash + hash = {} + hash[:disabled] = @disabled if @disabled + hash[:server_name] = @server_name if @server_name + hash[:server_ca_cert] = source_to_hash(@server_root_ca_cert) if @server_root_ca_cert + hash[:client_cert] = source_to_hash(@client_cert) if @client_cert + hash[:client_key] = source_to_hash(@client_private_key) if @client_private_key + hash + end + + # Create a TLS configuration for use with connections + # @return [Hash, false] A TLS config hash or false if disabled + def to_connect_tls_config + return false if @disabled + + config = {} + config[:domain] = @server_name if @server_name + config[:server_root_ca_cert] = read_source(@server_root_ca_cert) if @server_root_ca_cert + config[:client_cert] = read_source(@client_cert) if @client_cert + config[:client_private_key] = read_source(@client_private_key) if @client_private_key + config + end + + private + + def source_to_hash(source) + case source + when Pathname + { path: source.to_s } + when String + # String is always treated as data content + { data: source } + when nil + nil + else + raise TypeError, "Source must be Pathname, String, or nil, got #{source.class}" + end + end + + def read_source(source) + case source + when Pathname + File.read(source.to_s) + when String + # If it's a string path (from TOML), read the file + # Otherwise return as content + if File.exist?(source) + File.read(source) + else + source + end + when nil + nil + else + raise TypeError, "Source must be Pathname, String, or nil, got #{source.class}" + end + end + end + + # Represents a client configuration profile. + # + # This class holds the configuration as loaded from a file or environment. + # See #to_client_connect_config to transform the profile to a connect config hash. + # + # @!attribute [r] address + # @return [String, nil] Client address + # @!attribute [r] namespace + # @return [String, nil] Client namespace + # @!attribute [r] api_key + # @return [String, nil] Client API key + # @!attribute [r] tls + # @return [ClientConfigTLS, nil] TLS configuration + # @!attribute [r] grpc_meta + # @return [Hash] gRPC metadata + class ClientConfigProfile + attr_reader :address, :namespace, :api_key, :tls, :grpc_meta + + # Create a ClientConfigProfile from a hash + # @param hash [Hash] Hash representation + # @return [ClientConfigProfile] The client profile + def self.from_hash(hash) + new( + address: hash[:address] || hash['address'], + namespace: hash[:namespace] || hash['namespace'], + api_key: hash[:api_key] || hash['api_key'], + tls: ClientConfigTLS.from_hash(hash[:tls] || hash['tls']), + grpc_meta: hash[:grpc_meta] || hash['grpc_meta'] || {} + ) + end + + # Load a single client profile from given sources, applying env overrides. + # + # @param profile [String, nil] Profile to load from the config + # @param config_source [Pathname, String, nil] Configuration source - + # Pathname for file path, String for TOML content + # @param disable_file [Boolean] If true, file loading is disabled + # @param disable_env [Boolean] If true, environment variable loading and overriding is disabled + # @param config_file_strict [Boolean] If true, will error on unrecognized keys + # @param override_env_vars [Hash, nil] Environment variables to use for loading and overrides + # @return [ClientConfigProfile] The client configuration profile + def self.load( + profile: nil, + config_source: nil, + disable_file: false, + disable_env: false, + config_file_strict: false, + override_env_vars: nil + ) + path, data = Temporalio::EnvConfig.source_to_path_and_data(config_source) + + raw_profile = Temporalio::Internal::Bridge::EnvConfig.load_client_connect_config( + profile, + path, + data, + disable_file, + disable_env, + config_file_strict, + override_env_vars || {} + ) + + from_hash(raw_profile) + end + + # @param address [String, nil] Client address + # @param namespace [String, nil] Client namespace + # @param api_key [String, nil] Client API key + # @param tls [ClientConfigTLS, nil] TLS configuration + # @param grpc_meta [Hash] gRPC metadata + def initialize( + address: nil, + namespace: nil, + api_key: nil, + tls: nil, + grpc_meta: {} + ) + @address = address + @namespace = namespace + @api_key = api_key + @tls = tls + @grpc_meta = grpc_meta || {} + end + + # Convert to a hash that can be used for TOML serialization + # @return [Hash] Dictionary representation + def to_hash + hash = {} + hash[:address] = @address if @address + hash[:namespace] = @namespace if @namespace + hash[:api_key] = @api_key if @api_key + hash[:tls] = @tls.to_hash if @tls && !@tls.to_hash.empty? + hash[:grpc_meta] = @grpc_meta if @grpc_meta && !@grpc_meta.empty? + hash + end + + # Create a client connect config from this profile + # @return [Hash] Arguments that can be passed to Client.connect + def to_client_connect_config + config = {} + config[:target_host] = @address if @address + config[:namespace] = @namespace if @namespace + config[:api_key] = @api_key if @api_key + config[:tls] = @tls.to_connect_tls_config if @tls + config[:rpc_metadata] = @grpc_meta if @grpc_meta && !@grpc_meta.empty? + config + end + end + + # Client configuration loaded from TOML and environment variables. + # + # This contains a mapping of profile names to client profiles. + # + # @!attribute [r] profiles + # @return [Hash] Map of profile name to its corresponding ClientConfigProfile + class ClientConfig + attr_reader :profiles + + # Create a ClientConfig from a hash + # @param hash [Hash] Hash representation + # @return [ClientConfig] The client configuration + def self.from_hash(hash) + profiles = hash.transform_values do |profile_hash| + ClientConfigProfile.from_hash(profile_hash) + end + new(profiles) + end + + # Load all client profiles from given sources. + # + # This does not apply environment variable overrides to the profiles, it + # only uses an environment variable to find the default config file path + # (TEMPORAL_CONFIG_FILE). + # + # @param config_source [Pathname, String, nil] Configuration source + # @param disable_file [Boolean] If true, file loading is disabled + # @param config_file_strict [Boolean] If true, will error on unrecognized keys + # @param override_env_vars [Hash, nil] Environment variables to use + # @return [ClientConfig] The client configuration + def self.load( + config_source: nil, + disable_file: false, + config_file_strict: false, + override_env_vars: nil + ) + path, data = Temporalio::EnvConfig.source_to_path_and_data(config_source) + + loaded_profiles = Temporalio::Internal::Bridge::EnvConfig.load_client_config( + path, + data, + disable_file, + config_file_strict, + override_env_vars || {} + ) + + from_hash(loaded_profiles) + end + + # Load a single client profile and convert to connect config + # + # This is a convenience function that combines loading a profile and + # converting it to a connect config hash. + # + # @param profile [String, nil] The profile to load from the config + # @param config_source [Pathname, String, nil] Configuration source + # @param disable_file [Boolean] If true, file loading is disabled + # @param disable_env [Boolean] If true, environment variable loading and overriding is disabled + # @param config_file_strict [Boolean] If true, will error on unrecognized keys + # @param override_env_vars [Hash, nil] Environment variables to use for loading and overrides + # @return [Hash] Hash of keyword arguments for Client.connect + def self.load_client_connect_config( + profile: nil, + config_source: nil, + disable_file: false, + disable_env: false, + config_file_strict: false, + override_env_vars: nil + ) + + prof = ClientConfigProfile.load( + profile: profile, + config_source: config_source, + disable_file: disable_file, + disable_env: disable_env, + config_file_strict: config_file_strict, + override_env_vars: override_env_vars + ) + prof.to_client_connect_config + end + + # @param profiles [Hash] Map of profile name to ClientConfigProfile + def initialize(profiles) + @profiles = profiles || {} + end + + # Convert to a hash that can be used for TOML serialization + # @return [Hash] Dictionary representation + def to_hash + @profiles.transform_values(&:to_hash) + end + end + end +end diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb new file mode 100644 index 00000000..1536a4f2 --- /dev/null +++ b/temporalio/test/envconfig_test.rb @@ -0,0 +1,901 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'pathname' +require 'temporalio/client' +require 'temporalio/envconfig' +require_relative 'test' + +class EnvConfigTest < Test + # A base TOML config with a default and a custom profile + TOML_CONFIG_BASE = <<~TOML + [profile.default] + address = "default-address" + namespace = "default-namespace" + + [profile.custom] + address = "custom-address" + namespace = "custom-namespace" + api_key = "custom-api-key" + [profile.custom.tls] + server_name = "custom-server-name" + [profile.custom.grpc_meta] + custom-header = "custom-value" + TOML + + # A TOML config with an unrecognized key for strict testing + TOML_CONFIG_STRICT_FAIL = <<~TOML + [profile.default] + address = "default-address" + unrecognized_field = "should-fail" + TOML + + # Malformed TOML for error testing + TOML_CONFIG_MALFORMED = 'this is not valid toml' + + # A TOML config for testing detailed TLS options + TOML_CONFIG_TLS_DETAILED = <<~TOML + [profile.tls_disabled] + address = "localhost:1234" + [profile.tls_disabled.tls] + disabled = true + server_name = "should-be-ignored" + + [profile.tls_with_certs] + address = "localhost:5678" + [profile.tls_with_certs.tls] + server_name = "custom-server" + server_ca_cert_data = "ca-pem-data" + client_cert_data = "client-crt-data" + client_key_data = "client-key-data" + TOML + + # TOML config for metadata normalization testing + TOML_CONFIG_GRPC_META = <<~TOML + [profile.default] + address = "localhost:7233" + namespace = "default" + + [profile.default.grpc_meta] + "Custom-Header" = "custom-value" + "ANOTHER_HEADER_KEY" = "another-value" + "mixed_Case-header" = "mixed-value" + TOML + + # ============================================================================= + # PROFILE LOADING TESTS (6 tests) + # ============================================================================= + + def test_load_profile_from_file_default + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file)) + assert_equal 'default-address', profile.address + assert_equal 'default-namespace', profile.namespace + assert_nil profile.tls + refute_includes profile.grpc_meta, 'custom-header' + + config = profile.to_client_connect_config + assert_equal 'default-address', config[:target_host] + assert_nil config[:tls] + rpc_meta = config[:rpc_metadata] + if rpc_meta.nil? + assert_nil rpc_meta + else + refute_includes rpc_meta, 'custom-header' + end + end + end + + def test_load_profile_from_file_custom + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), profile: 'custom') + assert_equal 'custom-address', profile.address + assert_equal 'custom-namespace', profile.namespace + refute_nil profile.tls + assert_equal 'custom-server-name', profile.tls.server_name + assert_equal 'custom-value', profile.grpc_meta['custom-header'] + + config = profile.to_client_connect_config + assert_equal 'custom-address', config[:target_host] + tls_config = config[:tls] + assert_instance_of Hash, tls_config + assert_equal 'custom-server-name', tls_config[:domain] + rpc_metadata = config[:rpc_metadata] + refute_nil rpc_metadata + assert_equal 'custom-value', rpc_metadata['custom-header'] + end + end + + def test_load_profile_from_data_default + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: TOML_CONFIG_BASE) + assert_equal 'default-address', profile.address + assert_equal 'default-namespace', profile.namespace + assert_nil profile.tls + + config = profile.to_client_connect_config + assert_equal 'default-address', config[:target_host] + assert_nil config[:tls] + end + + def test_load_profile_from_data_custom + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: TOML_CONFIG_BASE, profile: 'custom') + assert_equal 'custom-address', profile.address + assert_equal 'custom-namespace', profile.namespace + refute_nil profile.tls + assert_equal 'custom-server-name', profile.tls.server_name + assert_equal 'custom-value', profile.grpc_meta['custom-header'] + + config = profile.to_client_connect_config + assert_equal 'custom-address', config[:target_host] + tls_config = config[:tls] + assert_instance_of Hash, tls_config + assert_equal 'custom-server-name', tls_config[:domain] + rpc_metadata = config[:rpc_metadata] + refute_nil rpc_metadata + assert_equal 'custom-value', rpc_metadata['custom-header'] + end + + def test_load_profile_from_data_env_overrides + env = { + 'TEMPORAL_ADDRESS' => 'env-address', + 'TEMPORAL_NAMESPACE' => 'env-namespace' + } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: TOML_CONFIG_BASE, profile: 'custom', override_env_vars: env + ) + assert_equal 'env-address', profile.address + assert_equal 'env-namespace', profile.namespace + + config = profile.to_client_connect_config + assert_equal 'env-address', config[:target_host] + end + + def test_load_profile_env_overrides + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { + 'TEMPORAL_ADDRESS' => 'env-address', + 'TEMPORAL_NAMESPACE' => 'env-namespace', + 'TEMPORAL_API_KEY' => 'env-api-key', + 'TEMPORAL_TLS_SERVER_NAME' => 'env-server-name' + } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: Pathname.new(config_file), profile: 'custom', override_env_vars: env + ) + assert_equal 'env-address', profile.address + assert_equal 'env-namespace', profile.namespace + assert_equal 'env-api-key', profile.api_key + refute_nil profile.tls + assert_equal 'env-server-name', profile.tls.server_name + + config = profile.to_client_connect_config + assert_equal 'env-address', config[:target_host] + assert_equal 'env-api-key', config[:api_key] + tls_config = config[:tls] + assert_instance_of Hash, tls_config + assert_equal 'env-server-name', tls_config[:domain] + end + end + + # ============================================================================= + # ENVIRONMENT VARIABLES TESTS (4 tests) + # ============================================================================= + + def test_load_profile_grpc_meta_env_overrides + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { + # This should override the value in the file + 'TEMPORAL_GRPC_META_CUSTOM_HEADER' => 'env-value', + # This should add a new header + 'TEMPORAL_GRPC_META_ANOTHER_HEADER' => 'another-value' + } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: Pathname.new(config_file), profile: 'custom', override_env_vars: env + ) + assert_equal 'env-value', profile.grpc_meta['custom-header'] + assert_equal 'another-value', profile.grpc_meta['another-header'] + + config = profile.to_client_connect_config + rpc_metadata = config[:rpc_metadata] + refute_nil rpc_metadata + assert_equal 'env-value', rpc_metadata['custom-header'] + assert_equal 'another-value', rpc_metadata['another-header'] + end + end + + def test_grpc_metadata_normalization_from_toml + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: TOML_CONFIG_GRPC_META) + + # Keys should be normalized: uppercase -> lowercase, underscores -> hyphens + assert_equal 'custom-value', profile.grpc_meta['custom-header'] + assert_equal 'another-value', profile.grpc_meta['another-header-key'] + assert_equal 'mixed-value', profile.grpc_meta['mixed-case-header'] + + # Original case variations should not exist + refute_includes profile.grpc_meta, 'Custom-Header' + refute_includes profile.grpc_meta, 'ANOTHER_HEADER_KEY' + refute_includes profile.grpc_meta, 'mixed_Case-header' + + config = profile.to_client_connect_config + rpc_metadata = config[:rpc_metadata] + refute_nil rpc_metadata + assert_equal 'custom-value', rpc_metadata['custom-header'] + assert_equal 'another-value', rpc_metadata['another-header-key'] + end + + def test_grpc_metadata_deletion_via_empty_env_value + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { + # Empty value should remove the header + 'TEMPORAL_GRPC_META_CUSTOM_HEADER' => '', + # Non-empty value should set the header + 'TEMPORAL_GRPC_META_NEW_HEADER' => 'new-value' + } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: Pathname.new(config_file), profile: 'custom', override_env_vars: env + ) + + # custom-header should be removed by empty env value + refute_includes profile.grpc_meta, 'custom-header' + # new-header should be added + assert_equal 'new-value', profile.grpc_meta['new-header'] + + config = profile.to_client_connect_config + rpc_metadata = config[:rpc_metadata] + if rpc_metadata && !rpc_metadata.empty? + refute_includes rpc_metadata, 'custom-header' + assert_equal 'new-value', rpc_metadata['new-header'] + end + end + end + + def test_load_profile_disable_env + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { 'TEMPORAL_ADDRESS' => 'env-address' } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: Pathname.new(config_file), override_env_vars: env, disable_env: true + ) + assert_equal 'default-address', profile.address + + config = profile.to_client_connect_config + assert_equal 'default-address', config[:target_host] + end + end + + # ============================================================================= + # CONTROL FLAGS TESTS (3 tests) + # ============================================================================= + + def test_load_profile_disable_file + env = { 'TEMPORAL_ADDRESS' => 'env-address' } + profile = Temporalio::EnvConfig::ClientConfigProfile.load(disable_file: true, override_env_vars: env) + assert_equal 'env-address', profile.address + + config = profile.to_client_connect_config + assert_equal 'env-address', config[:target_host] + end + + def test_load_profiles_no_env_override + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { + 'TEMPORAL_CONFIG_FILE' => config_file, + 'TEMPORAL_ADDRESS' => 'env-address' # This should be ignored for profiles loading + } + client_config = Temporalio::EnvConfig::ClientConfig.load(override_env_vars: env) + connect_config = client_config.profiles['default'].to_client_connect_config + assert_equal 'default-address', connect_config[:target_host] + end + end + + def test_disables_raise_error + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(disable_file: true, disable_env: true) + end + end + + # ============================================================================= + # CONFIG DISCOVERY TESTS (6 tests) + # ============================================================================= + + def test_load_profiles_from_file_all + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + client_config = Temporalio::EnvConfig::ClientConfig.load(config_source: Pathname.new(config_file)) + assert_equal 2, client_config.profiles.size + assert_includes client_config.profiles, 'default' + assert_includes client_config.profiles, 'custom' + # Check that we can convert to a connect config + connect_config = client_config.profiles['default'].to_client_connect_config + assert_equal 'default-address', connect_config[:target_host] + end + end + + def test_load_profiles_from_data_all + client_config = Temporalio::EnvConfig::ClientConfig.load(config_source: TOML_CONFIG_BASE) + assert_equal 2, client_config.profiles.size + connect_config = client_config.profiles['custom'].to_client_connect_config + assert_equal 'custom-address', connect_config[:target_host] + end + + def test_load_profiles_no_config_file + client_config = Temporalio::EnvConfig::ClientConfig.load(override_env_vars: {}) + assert_empty client_config.profiles + end + + def test_load_profiles_discovery + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { 'TEMPORAL_CONFIG_FILE' => config_file } + client_config = Temporalio::EnvConfig::ClientConfig.load(override_env_vars: env) + assert_includes client_config.profiles, 'default' + end + end + + def test_load_profiles_disable_file + # With no env vars, should be empty + client_config = Temporalio::EnvConfig::ClientConfig.load(disable_file: true, override_env_vars: {}) + assert_empty client_config.profiles + end + + def test_default_profile_not_found_returns_empty_profile + toml = <<~TOML + [profile.existing] + address = "my-address" + TOML + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: toml) + assert_nil profile.address + assert_nil profile.namespace + assert_nil profile.api_key + assert_empty profile.grpc_meta + assert_nil profile.tls + end + + # ============================================================================= + # TLS CONFIGURATION TESTS (7 tests) + # ============================================================================= + + def test_load_profile_api_key_enables_tls + config_toml = "[profile.default]\naddress = 'some-host:1234'\napi_key = 'my-key'" + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: config_toml) + assert_equal 'my-key', profile.api_key + refute_nil profile.tls + + config = profile.to_client_connect_config + refute_nil config[:tls] + assert_equal 'my-key', config[:api_key] + end + + def test_load_profile_tls_options + # Test with TLS disabled + profile_disabled = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: TOML_CONFIG_TLS_DETAILED, profile: 'tls_disabled' + ) + refute_nil profile_disabled.tls + assert profile_disabled.tls.disabled + + config_disabled = profile_disabled.to_client_connect_config + assert_equal false, config_disabled[:tls] + + # Test with TLS certs + profile_certs = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: TOML_CONFIG_TLS_DETAILED, profile: 'tls_with_certs' + ) + refute_nil profile_certs.tls + assert_equal 'custom-server', profile_certs.tls.server_name + refute_nil profile_certs.tls.server_root_ca_cert + assert_equal 'ca-pem-data', profile_certs.tls.server_root_ca_cert + refute_nil profile_certs.tls.client_cert + assert_equal 'client-crt-data', profile_certs.tls.client_cert + refute_nil profile_certs.tls.client_private_key + assert_equal 'client-key-data', profile_certs.tls.client_private_key + + config_certs = profile_certs.to_client_connect_config + tls_config_certs = config_certs[:tls] + assert_instance_of Hash, tls_config_certs + assert_equal 'custom-server', tls_config_certs[:domain] + assert_equal 'ca-pem-data', tls_config_certs[:server_root_ca_cert] + assert_equal 'client-crt-data', tls_config_certs[:client_cert] + assert_equal 'client-key-data', tls_config_certs[:client_private_key] + end + + def test_load_profile_tls_from_paths + Dir.mktmpdir do |tmpdir| + # Create dummy cert files + ca_pem_path = File.join(tmpdir, 'ca.pem') + client_crt_path = File.join(tmpdir, 'client.crt') + client_key_path = File.join(tmpdir, 'client.key') + + File.write(ca_pem_path, 'ca-pem-data') + File.write(client_crt_path, 'client-crt-data') + File.write(client_key_path, 'client-key-data') + + toml_config = <<~TOML + [profile.default] + address = "localhost:5678" + [profile.default.tls] + server_name = "custom-server" + server_ca_cert_path = "#{ca_pem_path}" + client_cert_path = "#{client_crt_path}" + client_key_path = "#{client_key_path}" + TOML + + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: toml_config) + refute_nil profile.tls + assert_equal 'custom-server', profile.tls.server_name + refute_nil profile.tls.server_root_ca_cert + assert_equal ca_pem_path, profile.tls.server_root_ca_cert + refute_nil profile.tls.client_cert + assert_equal client_crt_path, profile.tls.client_cert + refute_nil profile.tls.client_private_key + assert_equal client_key_path, profile.tls.client_private_key + + config = profile.to_client_connect_config + tls_config = config[:tls] + assert_instance_of Hash, tls_config + assert_equal 'custom-server', tls_config[:domain] + assert_equal 'ca-pem-data', tls_config[:server_root_ca_cert] + assert_equal 'client-crt-data', tls_config[:client_cert] + assert_equal 'client-key-data', tls_config[:client_private_key] + end + end + + def test_load_profile_conflicting_cert_source_fails + toml_config = <<~TOML + [profile.default] + address = "localhost:5678" + [profile.default.tls] + client_cert_path = "/path/to/cert" + client_cert_data = "cert-data" + TOML + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: toml_config) + end + end + + def test_tls_conflict_across_sources_path_in_toml_data_in_env + toml_config = <<~TOML + [profile.default] + address = "localhost:7233" + [profile.default.tls] + client_cert_path = "/path/to/cert" + TOML + + env = { + 'TEMPORAL_TLS_CLIENT_CERT_DATA' => 'cert-data-from-env' + } + + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: toml_config, + override_env_vars: env + ) + end + end + + def test_tls_conflict_across_sources_data_in_toml_path_in_env + toml_config = <<~TOML + [profile.default] + address = "localhost:7233" + [profile.default.tls] + client_cert_data = "cert-data-from-toml" + TOML + + env = { + 'TEMPORAL_TLS_CLIENT_CERT_PATH' => '/path/from/env' + } + + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: toml_config, + override_env_vars: env + ) + end + end + + # ============================================================================= + # ERROR HANDLING TESTS (4 tests) + # ============================================================================= + + def test_load_profile_not_found + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), profile: 'nonexistent') + end + end + end + + def test_load_profiles_strict_mode_fail + with_temp_config_file(TOML_CONFIG_STRICT_FAIL) do |config_file| + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfig.load(config_source: Pathname.new(config_file), config_file_strict: true) + end + end + end + + def test_load_profile_strict_mode_fail + with_temp_config_file(TOML_CONFIG_STRICT_FAIL) do |config_file| + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), config_file_strict: true) + end + end + end + + def test_load_profiles_from_data_malformed + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfig.load(config_source: TOML_CONFIG_MALFORMED) + end + end + + # ============================================================================= + # SERIALIZATION TESTS (3 tests) + # ============================================================================= + + def test_client_config_profile_to_from_dict + # Profile with all fields + profile = Temporalio::EnvConfig::ClientConfigProfile.new( + address: 'some-address', + namespace: 'some-namespace', + api_key: 'some-api-key', + tls: Temporalio::EnvConfig::ClientConfigTLS.new( + disabled: false, + server_name: 'some-server-name', + server_root_ca_cert: 'ca-cert-data', + client_cert: Pathname.new('/path/to/client.crt'), + client_private_key: 'client-key-data' + ), + grpc_meta: { 'some-header' => 'some-value' } + ) + + profile_hash = profile.to_hash + + # Check hash representation. Note that disabled=false is not in the hash. + expected_hash = { + address: 'some-address', + namespace: 'some-namespace', + api_key: 'some-api-key', + tls: { + server_name: 'some-server-name', + server_ca_cert: { data: 'ca-cert-data' }, + client_cert: { path: '/path/to/client.crt' }, + client_key: { data: 'client-key-data' } + }, + grpc_meta: { 'some-header' => 'some-value' } + } + assert_equal expected_hash, profile_hash + + # Convert back to profile + new_profile = Temporalio::EnvConfig::ClientConfigProfile.from_hash(profile_hash) + + # We expect the new profile to be the same + assert_equal profile.address, new_profile.address + assert_equal profile.namespace, new_profile.namespace + assert_equal profile.api_key, new_profile.api_key + assert_equal profile.grpc_meta, new_profile.grpc_meta + + # Test with minimal profile + profile_minimal = Temporalio::EnvConfig::ClientConfigProfile.new + profile_minimal_hash = profile_minimal.to_hash + assert_empty profile_minimal_hash + new_profile_minimal = Temporalio::EnvConfig::ClientConfigProfile.from_hash(profile_minimal_hash) + assert_nil profile_minimal.address + assert_nil new_profile_minimal.address + end + + def test_client_config_to_from_dict + # Config with multiple profiles + profile1 = Temporalio::EnvConfig::ClientConfigProfile.new( + address: 'some-address', + namespace: 'some-namespace' + ) + profile2 = Temporalio::EnvConfig::ClientConfigProfile.new( + address: 'another-address', + tls: Temporalio::EnvConfig::ClientConfigTLS.new(server_name: 'some-server-name'), + grpc_meta: { 'some-header' => 'some-value' } + ) + config = Temporalio::EnvConfig::ClientConfig.new( + 'default' => profile1, + 'custom' => profile2 + ) + + config_hash = config.to_hash + + expected_hash = { + 'default' => { + address: 'some-address', + namespace: 'some-namespace' + }, + 'custom' => { + address: 'another-address', + tls: { server_name: 'some-server-name' }, + grpc_meta: { 'some-header' => 'some-value' } + } + } + assert_equal expected_hash, config_hash + + # Convert back to config + new_config = Temporalio::EnvConfig::ClientConfig.from_hash(config_hash) + assert_equal config.profiles.keys, new_config.profiles.keys + + # Test empty config + empty_config = Temporalio::EnvConfig::ClientConfig.new({}) + empty_config_hash = empty_config.to_hash + assert_empty empty_config_hash + new_empty_config = Temporalio::EnvConfig::ClientConfig.from_hash(empty_config_hash) + assert_empty new_empty_config.profiles + end + + def test_read_source_from_string_content + # Test that read_source correctly handles string content + profile = Temporalio::EnvConfig::ClientConfigProfile.new( + address: 'localhost:1234', + tls: Temporalio::EnvConfig::ClientConfigTLS.new(client_cert: 'string-as-cert-content') + ) + config = profile.to_client_connect_config + tls_config = config[:tls] + assert_instance_of Hash, tls_config + assert_equal 'string-as-cert-content', tls_config[:client_cert] + end + + # ============================================================================= + # INTEGRATION/E2E TESTS (2 tests) + # ============================================================================= + + def test_load_client_connect_config_convenience_api + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + # Test default profile with file + connect_config = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + config_source: Pathname.new(config_file) + ) + assert_equal 'default-address', connect_config[:target_host] + assert_equal 'default-namespace', connect_config[:namespace] + + # Test with environment overrides + env = { 'TEMPORAL_NAMESPACE' => 'env-override-namespace' } + connect_config_with_env = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + config_source: Pathname.new(config_file), + override_env_vars: env + ) + assert_equal 'default-address', connect_config_with_env[:target_host] + assert_equal 'env-override-namespace', connect_config_with_env[:namespace] + + # Test with specific profile + connect_config_custom = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + profile: 'custom', + config_source: Pathname.new(config_file) + ) + assert_equal 'custom-address', connect_config_custom[:target_host] + assert_equal 'custom-namespace', connect_config_custom[:namespace] + assert_equal 'custom-api-key', connect_config_custom[:api_key] + end + end + + def test_load_client_connect_config_e2e_validation + # Test comprehensive end-to-end configuration loading with all features + toml_content = <<~TOML + [profile.production] + address = "prod.temporal.com:443" + namespace = "production-ns" + api_key = "prod-api-key" + + [profile.production.tls] + server_name = "prod.temporal.com" + server_ca_cert_data = "prod-ca-cert" + + [profile.production.grpc_meta] + authorization = "Bearer prod-token" + "x-custom-header" = "prod-value" + TOML + + env_overrides = { + 'TEMPORAL_GRPC_META_X_ENVIRONMENT' => 'production', + 'TEMPORAL_TLS_SERVER_NAME' => 'override.temporal.com' + } + + connect_config = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + profile: 'production', + config_source: toml_content, + override_env_vars: env_overrides + ) + + # Validate all configuration aspects + assert_equal 'prod.temporal.com:443', connect_config[:target_host] + assert_equal 'production-ns', connect_config[:namespace] + assert_equal 'prod-api-key', connect_config[:api_key] + + # TLS configuration (API key should auto-enable TLS) + refute_nil connect_config[:tls] + tls_config = connect_config[:tls] + assert_equal 'override.temporal.com', tls_config[:domain] # Env override + assert_equal 'prod-ca-cert', tls_config[:server_root_ca_cert] + + # gRPC metadata with normalization and env overrides + refute_nil connect_config[:rpc_metadata] + rpc_metadata = connect_config[:rpc_metadata] + assert_equal 'Bearer prod-token', rpc_metadata['authorization'] + assert_equal 'prod-value', rpc_metadata['x-custom-header'] + assert_equal 'production', rpc_metadata['x-environment'] # From env + end + + # ============================================================================= + # END-TO-END CLIENT CONNECTION TESTS (4 tests) + # ============================================================================= + + def test_e2e_basic_development_profile_client_connection + toml_content = <<~TOML + [profile.development] + address = "localhost:7233" + namespace = "dev-namespace" + + [profile.development.grpc_meta] + "x-test-source" = "envconfig-ruby-dev" + TOML + + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'development', + config_source: toml_content + ) + + connect_config = profile.to_client_connect_config + + # Create actual Temporal client using envconfig + client = Temporalio::Client.connect( + connect_config[:target_host], + connect_config[:namespace], + api_key: connect_config[:api_key], + tls: connect_config[:tls], + rpc_metadata: profile.grpc_meta, + lazy_connect: true + ) + + # Verify client configuration matches envconfig + assert_equal "localhost:7233", client.connection.target_host + assert_equal "dev-namespace", client.namespace + assert_equal "envconfig-ruby-dev", client.connection.options.rpc_metadata["x-test-source"] + end + + def test_e2e_production_tls_api_key_client_connection + toml_content = <<~TOML + [profile.production] + address = "prod.tmprl.cloud:443" + namespace = "production-namespace" + api_key = "prod-api-key-123" + + [profile.production.tls] + server_name = "prod.tmprl.cloud" + + [profile.production.grpc_meta] + authorization = "Bearer prod-token" + "x-environment" = "production" + TOML + + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'production', + config_source: toml_content + ) + + connect_config = profile.to_client_connect_config + + # Create TLS-enabled client with API key + client = Temporalio::Client.connect( + connect_config[:target_host], + connect_config[:namespace], + api_key: connect_config[:api_key], + tls: connect_config[:tls], + rpc_metadata: profile.grpc_meta, + lazy_connect: true + ) + + # Verify production configuration + assert_equal "prod.tmprl.cloud:443", client.connection.target_host + assert_equal "production-namespace", client.namespace + assert_equal "prod-api-key-123", client.connection.options.api_key + refute_nil client.connection.options.tls # TLS should be enabled with API key + assert_equal "Bearer prod-token", client.connection.options.rpc_metadata["authorization"] + assert_equal "production", client.connection.options.rpc_metadata["x-environment"] + end + + def test_e2e_environment_overrides_client_connection + toml_content = <<~TOML + [profile.staging] + address = "staging.temporal.com:443" + namespace = "staging-namespace" + + [profile.staging.grpc_meta] + "x-deployment" = "staging" + authorization = "Bearer staging-token" + TOML + + env_overrides = { + 'TEMPORAL_ADDRESS' => 'override.temporal.com:443', + 'TEMPORAL_NAMESPACE' => 'override-namespace', + 'TEMPORAL_GRPC_META_X_DEPLOYMENT' => 'canary', + 'TEMPORAL_GRPC_META_AUTHORIZATION' => 'Bearer override-token' + } + + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'staging', + config_source: toml_content, + override_env_vars: env_overrides + ) + + connect_config = profile.to_client_connect_config + + # Create client with environment overrides + client = Temporalio::Client.connect( + connect_config[:target_host], + connect_config[:namespace], + rpc_metadata: profile.grpc_meta, + lazy_connect: true + ) + + # Verify environment overrides took effect + assert_equal "override.temporal.com:443", client.connection.target_host + assert_equal "override-namespace", client.namespace + assert_equal "canary", client.connection.options.rpc_metadata["x-deployment"] + assert_equal "Bearer override-token", client.connection.options.rpc_metadata["authorization"] + end + + def test_e2e_multi_profile_different_client_connections + toml_content = <<~TOML + [profile.development] + address = "localhost:7233" + namespace = "dev" + + [profile.production] + address = "prod.tmprl.cloud:443" + namespace = "prod" + api_key = "prod-key" + + [profile.production.tls] + server_name = "prod.tmprl.cloud" + TOML + + # Load and create development client + dev_profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'development', + config_source: toml_content + ) + + dev_config = dev_profile.to_client_connect_config + dev_client = Temporalio::Client.connect( + dev_config[:target_host], + dev_config[:namespace], + api_key: dev_config[:api_key], + tls: dev_config[:tls], + lazy_connect: true + ) + + # Load and create production client + prod_profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'production', + config_source: toml_content + ) + + prod_config = prod_profile.to_client_connect_config + prod_client = Temporalio::Client.connect( + prod_config[:target_host], + prod_config[:namespace], + api_key: prod_config[:api_key], + tls: prod_config[:tls], + lazy_connect: true + ) + + # Verify different configurations for each client + assert_equal "localhost:7233", dev_client.connection.target_host + assert_equal "dev", dev_client.namespace + assert_nil dev_client.connection.options.api_key + assert_nil dev_client.connection.options.tls + + assert_equal "prod.tmprl.cloud:443", prod_client.connection.target_host + assert_equal "prod", prod_client.namespace + assert_equal "prod-key", prod_client.connection.options.api_key + refute_nil prod_client.connection.options.tls # TLS enabled with API key + end + + private + + def with_temp_config_file(content) + Dir.mktmpdir do |tmpdir| + config_file = File.join(tmpdir, 'config.toml') + File.write(config_file, content) + yield config_file + end + end +end \ No newline at end of file From 3504fad23af4d3811861931faefd6f3e769109e6 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 25 Aug 2025 16:12:02 -0400 Subject: [PATCH 02/31] linting --- temporalio/lib/temporalio.rb | 2 +- temporalio/lib/temporalio/envconfig.rb | 10 +- temporalio/test/envconfig_test.rb | 135 +++++++++++++------------ 3 files changed, 74 insertions(+), 73 deletions(-) diff --git a/temporalio/lib/temporalio.rb b/temporalio/lib/temporalio.rb index e5e19f3e..e8e72687 100644 --- a/temporalio/lib/temporalio.rb +++ b/temporalio/lib/temporalio.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true +require 'temporalio/envconfig' require 'temporalio/version' require 'temporalio/versioning_override' -require 'temporalio/envconfig' # Temporal Ruby SDK. See the README at https://github.com/temporalio/sdk-ruby. module Temporalio diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index c972f3df..946bb5a1 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -4,6 +4,7 @@ require 'temporalio/internal/bridge' module Temporalio + # Environment and file-based configuration for Temporal clients module EnvConfig # This module provides utilities to load Temporal client configuration from TOML files # and environment variables. @@ -12,7 +13,7 @@ module EnvConfig # - Pathname: Path to a configuration file # - String: TOML configuration content # - nil: No configuration source - + # Convert a data source to path and data parameters for the bridge # @param source [Pathname, String, nil] Configuration source # @return [Array?>] Tuple of [path, data_bytes] @@ -64,15 +65,13 @@ def self.from_hash(hash) # @return [Pathname, String, nil] Data source def self.hash_to_source(hash) return nil if hash.nil? - + # Always expect a hash with path or data if hash[:path] || hash['path'] # Return path as string to match old behavior hash[:path] || hash['path'] elsif hash[:data] || hash['data'] hash[:data] || hash['data'] - else - nil end end @@ -190,7 +189,7 @@ def self.from_hash(hash) # Load a single client profile from given sources, applying env overrides. # # @param profile [String, nil] Profile to load from the config - # @param config_source [Pathname, String, nil] Configuration source - + # @param config_source [Pathname, String, nil] Configuration source - # Pathname for file path, String for TOML content # @param disable_file [Boolean] If true, file loading is disabled # @param disable_env [Boolean] If true, environment variable loading and overriding is disabled @@ -333,7 +332,6 @@ def self.load_client_connect_config( config_file_strict: false, override_env_vars: nil ) - prof = ClientConfigProfile.load( profile: profile, config_source: config_source, diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index 1536a4f2..c702d5b8 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -6,7 +6,7 @@ require 'temporalio/envconfig' require_relative 'test' -class EnvConfigTest < Test +class EnvConfigTest < Test # A base TOML config with a default and a custom profile TOML_CONFIG_BASE = <<~TOML [profile.default] @@ -55,7 +55,7 @@ class EnvConfigTest < Test [profile.default] address = "localhost:7233" namespace = "default" - + [profile.default.grpc_meta] "Custom-Header" = "custom-value" "ANOTHER_HEADER_KEY" = "another-value" @@ -88,7 +88,8 @@ def test_load_profile_from_file_default def test_load_profile_from_file_custom with_temp_config_file(TOML_CONFIG_BASE) do |config_file| - profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), profile: 'custom') + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), + profile: 'custom') assert_equal 'custom-address', profile.address assert_equal 'custom-namespace', profile.namespace refute_nil profile.tls @@ -204,12 +205,12 @@ def test_load_profile_grpc_meta_env_overrides def test_grpc_metadata_normalization_from_toml profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: TOML_CONFIG_GRPC_META) - + # Keys should be normalized: uppercase -> lowercase, underscores -> hyphens assert_equal 'custom-value', profile.grpc_meta['custom-header'] - assert_equal 'another-value', profile.grpc_meta['another-header-key'] + assert_equal 'another-value', profile.grpc_meta['another-header-key'] assert_equal 'mixed-value', profile.grpc_meta['mixed-case-header'] - + # Original case variations should not exist refute_includes profile.grpc_meta, 'Custom-Header' refute_includes profile.grpc_meta, 'ANOTHER_HEADER_KEY' @@ -233,7 +234,7 @@ def test_grpc_metadata_deletion_via_empty_env_value profile = Temporalio::EnvConfig::ClientConfigProfile.load( config_source: Pathname.new(config_file), profile: 'custom', override_env_vars: env ) - + # custom-header should be removed by empty env value refute_includes profile.grpc_meta, 'custom-header' # new-header should be added @@ -456,14 +457,14 @@ def test_tls_conflict_across_sources_path_in_toml_data_in_env [profile.default.tls] client_cert_path = "/path/to/cert" TOML - + env = { 'TEMPORAL_TLS_CLIENT_CERT_DATA' => 'cert-data-from-env' } - + assert_raises(Temporalio::Internal::Bridge::Error) do Temporalio::EnvConfig::ClientConfigProfile.load( - config_source: toml_config, + config_source: toml_config, override_env_vars: env ) end @@ -476,11 +477,11 @@ def test_tls_conflict_across_sources_data_in_toml_path_in_env [profile.default.tls] client_cert_data = "cert-data-from-toml" TOML - + env = { 'TEMPORAL_TLS_CLIENT_CERT_PATH' => '/path/from/env' } - + assert_raises(Temporalio::Internal::Bridge::Error) do Temporalio::EnvConfig::ClientConfigProfile.load( config_source: toml_config, @@ -496,7 +497,8 @@ def test_tls_conflict_across_sources_data_in_toml_path_in_env def test_load_profile_not_found with_temp_config_file(TOML_CONFIG_BASE) do |config_file| assert_raises(Temporalio::Internal::Bridge::Error) do - Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), profile: 'nonexistent') + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), + profile: 'nonexistent') end end end @@ -512,7 +514,8 @@ def test_load_profiles_strict_mode_fail def test_load_profile_strict_mode_fail with_temp_config_file(TOML_CONFIG_STRICT_FAIL) do |config_file| assert_raises(Temporalio::Internal::Bridge::Error) do - Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), config_file_strict: true) + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), + config_file_strict: true) end end end @@ -645,7 +648,7 @@ def test_load_client_connect_config_convenience_api ) assert_equal 'default-address', connect_config[:target_host] assert_equal 'default-namespace', connect_config[:namespace] - + # Test with environment overrides env = { 'TEMPORAL_NAMESPACE' => 'env-override-namespace' } connect_config_with_env = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( @@ -654,7 +657,7 @@ def test_load_client_connect_config_convenience_api ) assert_equal 'default-address', connect_config_with_env[:target_host] assert_equal 'env-override-namespace', connect_config_with_env[:namespace] - + # Test with specific profile connect_config_custom = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( profile: 'custom', @@ -673,38 +676,38 @@ def test_load_client_connect_config_e2e_validation address = "prod.temporal.com:443" namespace = "production-ns" api_key = "prod-api-key" - + [profile.production.tls] server_name = "prod.temporal.com" server_ca_cert_data = "prod-ca-cert" - + [profile.production.grpc_meta] authorization = "Bearer prod-token" "x-custom-header" = "prod-value" TOML - + env_overrides = { 'TEMPORAL_GRPC_META_X_ENVIRONMENT' => 'production', 'TEMPORAL_TLS_SERVER_NAME' => 'override.temporal.com' } - + connect_config = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( profile: 'production', config_source: toml_content, override_env_vars: env_overrides ) - + # Validate all configuration aspects assert_equal 'prod.temporal.com:443', connect_config[:target_host] assert_equal 'production-ns', connect_config[:namespace] assert_equal 'prod-api-key', connect_config[:api_key] - + # TLS configuration (API key should auto-enable TLS) refute_nil connect_config[:tls] tls_config = connect_config[:tls] assert_equal 'override.temporal.com', tls_config[:domain] # Env override assert_equal 'prod-ca-cert', tls_config[:server_root_ca_cert] - + # gRPC metadata with normalization and env overrides refute_nil connect_config[:rpc_metadata] rpc_metadata = connect_config[:rpc_metadata] @@ -722,18 +725,18 @@ def test_e2e_basic_development_profile_client_connection [profile.development] address = "localhost:7233" namespace = "dev-namespace" - + [profile.development.grpc_meta] "x-test-source" = "envconfig-ruby-dev" TOML - + profile = Temporalio::EnvConfig::ClientConfigProfile.load( profile: 'development', config_source: toml_content ) - + connect_config = profile.to_client_connect_config - + # Create actual Temporal client using envconfig client = Temporalio::Client.connect( connect_config[:target_host], @@ -743,11 +746,11 @@ def test_e2e_basic_development_profile_client_connection rpc_metadata: profile.grpc_meta, lazy_connect: true ) - + # Verify client configuration matches envconfig - assert_equal "localhost:7233", client.connection.target_host - assert_equal "dev-namespace", client.namespace - assert_equal "envconfig-ruby-dev", client.connection.options.rpc_metadata["x-test-source"] + assert_equal 'localhost:7233', client.connection.target_host + assert_equal 'dev-namespace', client.namespace + assert_equal 'envconfig-ruby-dev', client.connection.options.rpc_metadata['x-test-source'] end def test_e2e_production_tls_api_key_client_connection @@ -756,22 +759,22 @@ def test_e2e_production_tls_api_key_client_connection address = "prod.tmprl.cloud:443" namespace = "production-namespace" api_key = "prod-api-key-123" - + [profile.production.tls] server_name = "prod.tmprl.cloud" - + [profile.production.grpc_meta] authorization = "Bearer prod-token" "x-environment" = "production" TOML - + profile = Temporalio::EnvConfig::ClientConfigProfile.load( profile: 'production', config_source: toml_content ) - + connect_config = profile.to_client_connect_config - + # Create TLS-enabled client with API key client = Temporalio::Client.connect( connect_config[:target_host], @@ -781,14 +784,14 @@ def test_e2e_production_tls_api_key_client_connection rpc_metadata: profile.grpc_meta, lazy_connect: true ) - + # Verify production configuration - assert_equal "prod.tmprl.cloud:443", client.connection.target_host - assert_equal "production-namespace", client.namespace - assert_equal "prod-api-key-123", client.connection.options.api_key + assert_equal 'prod.tmprl.cloud:443', client.connection.target_host + assert_equal 'production-namespace', client.namespace + assert_equal 'prod-api-key-123', client.connection.options.api_key refute_nil client.connection.options.tls # TLS should be enabled with API key - assert_equal "Bearer prod-token", client.connection.options.rpc_metadata["authorization"] - assert_equal "production", client.connection.options.rpc_metadata["x-environment"] + assert_equal 'Bearer prod-token', client.connection.options.rpc_metadata['authorization'] + assert_equal 'production', client.connection.options.rpc_metadata['x-environment'] end def test_e2e_environment_overrides_client_connection @@ -796,27 +799,27 @@ def test_e2e_environment_overrides_client_connection [profile.staging] address = "staging.temporal.com:443" namespace = "staging-namespace" - + [profile.staging.grpc_meta] "x-deployment" = "staging" authorization = "Bearer staging-token" TOML - + env_overrides = { 'TEMPORAL_ADDRESS' => 'override.temporal.com:443', 'TEMPORAL_NAMESPACE' => 'override-namespace', 'TEMPORAL_GRPC_META_X_DEPLOYMENT' => 'canary', 'TEMPORAL_GRPC_META_AUTHORIZATION' => 'Bearer override-token' } - + profile = Temporalio::EnvConfig::ClientConfigProfile.load( profile: 'staging', config_source: toml_content, override_env_vars: env_overrides ) - + connect_config = profile.to_client_connect_config - + # Create client with environment overrides client = Temporalio::Client.connect( connect_config[:target_host], @@ -824,12 +827,12 @@ def test_e2e_environment_overrides_client_connection rpc_metadata: profile.grpc_meta, lazy_connect: true ) - + # Verify environment overrides took effect - assert_equal "override.temporal.com:443", client.connection.target_host - assert_equal "override-namespace", client.namespace - assert_equal "canary", client.connection.options.rpc_metadata["x-deployment"] - assert_equal "Bearer override-token", client.connection.options.rpc_metadata["authorization"] + assert_equal 'override.temporal.com:443', client.connection.target_host + assert_equal 'override-namespace', client.namespace + assert_equal 'canary', client.connection.options.rpc_metadata['x-deployment'] + assert_equal 'Bearer override-token', client.connection.options.rpc_metadata['authorization'] end def test_e2e_multi_profile_different_client_connections @@ -837,22 +840,22 @@ def test_e2e_multi_profile_different_client_connections [profile.development] address = "localhost:7233" namespace = "dev" - + [profile.production] address = "prod.tmprl.cloud:443" namespace = "prod" api_key = "prod-key" - + [profile.production.tls] server_name = "prod.tmprl.cloud" TOML - + # Load and create development client dev_profile = Temporalio::EnvConfig::ClientConfigProfile.load( profile: 'development', config_source: toml_content ) - + dev_config = dev_profile.to_client_connect_config dev_client = Temporalio::Client.connect( dev_config[:target_host], @@ -861,13 +864,13 @@ def test_e2e_multi_profile_different_client_connections tls: dev_config[:tls], lazy_connect: true ) - + # Load and create production client prod_profile = Temporalio::EnvConfig::ClientConfigProfile.load( profile: 'production', config_source: toml_content ) - + prod_config = prod_profile.to_client_connect_config prod_client = Temporalio::Client.connect( prod_config[:target_host], @@ -876,16 +879,16 @@ def test_e2e_multi_profile_different_client_connections tls: prod_config[:tls], lazy_connect: true ) - + # Verify different configurations for each client - assert_equal "localhost:7233", dev_client.connection.target_host - assert_equal "dev", dev_client.namespace + assert_equal 'localhost:7233', dev_client.connection.target_host + assert_equal 'dev', dev_client.namespace assert_nil dev_client.connection.options.api_key assert_nil dev_client.connection.options.tls - - assert_equal "prod.tmprl.cloud:443", prod_client.connection.target_host - assert_equal "prod", prod_client.namespace - assert_equal "prod-key", prod_client.connection.options.api_key + + assert_equal 'prod.tmprl.cloud:443', prod_client.connection.target_host + assert_equal 'prod', prod_client.namespace + assert_equal 'prod-key', prod_client.connection.options.api_key refute_nil prod_client.connection.options.tls # TLS enabled with API key end @@ -898,4 +901,4 @@ def with_temp_config_file(content) yield config_file end end -end \ No newline at end of file +end From 4d7903628637521010ca5545a238ae4ce458fac8 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 25 Aug 2025 17:06:17 -0400 Subject: [PATCH 03/31] fix steep type checks --- temporalio/lib/temporalio/envconfig.rb | 7 +- temporalio/sig/temporalio/envconfig.rbs | 87 +++++++++++++++++++++++++ temporalio/test/envconfig_test.rb | 41 ++++++------ temporalio/test/sig/envconfig_test.rbs | 10 +++ 4 files changed, 123 insertions(+), 22 deletions(-) create mode 100644 temporalio/sig/temporalio/envconfig.rbs create mode 100644 temporalio/test/sig/envconfig_test.rbs diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index 946bb5a1..f4c6ac73 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -245,7 +245,10 @@ def to_hash hash[:address] = @address if @address hash[:namespace] = @namespace if @namespace hash[:api_key] = @api_key if @api_key - hash[:tls] = @tls.to_hash if @tls && !@tls.to_hash.empty? + if @tls + tls_hash = @tls.to_hash # steep:ignore + hash[:tls] = tls_hash unless tls_hash.empty? + end hash[:grpc_meta] = @grpc_meta if @grpc_meta && !@grpc_meta.empty? hash end @@ -257,7 +260,7 @@ def to_client_connect_config config[:target_host] = @address if @address config[:namespace] = @namespace if @namespace config[:api_key] = @api_key if @api_key - config[:tls] = @tls.to_connect_tls_config if @tls + config[:tls] = @tls.to_connect_tls_config if @tls # steep:ignore config[:rpc_metadata] = @grpc_meta if @grpc_meta && !@grpc_meta.empty? config end diff --git a/temporalio/sig/temporalio/envconfig.rbs b/temporalio/sig/temporalio/envconfig.rbs new file mode 100644 index 00000000..484688cc --- /dev/null +++ b/temporalio/sig/temporalio/envconfig.rbs @@ -0,0 +1,87 @@ +module Temporalio + module EnvConfig + def self.source_to_path_and_data: (untyped source) -> [String?, Array[Integer]?] + + class ClientConfigTLS + attr_reader disabled: bool + attr_reader server_name: String? + attr_reader server_root_ca_cert: (Pathname | String)? + attr_reader client_cert: (Pathname | String)? + attr_reader client_private_key: (Pathname | String)? + + def self.from_hash: (Hash[untyped, untyped]? hash) -> ClientConfigTLS? + def self.hash_to_source: (Hash[untyped, untyped]? hash) -> (Pathname | String)? + + def initialize: ( + ?disabled: bool, + ?server_name: String?, + ?server_root_ca_cert: (Pathname | String)?, + ?client_cert: (Pathname | String)?, + ?client_private_key: (Pathname | String)? + ) -> void + + def to_hash: -> Hash[Symbol, untyped] + def to_connect_tls_config: -> (Hash[Symbol, untyped] | false) + + private + + def source_to_hash: (untyped source) -> Hash[Symbol, String]? + def read_source: (untyped source) -> String? + end + + class ClientConfigProfile + attr_reader address: String? + attr_reader namespace: String? + attr_reader api_key: String? + attr_reader tls: ClientConfigTLS? + attr_reader grpc_meta: Hash[untyped, untyped] + + def self.from_hash: (Hash[untyped, untyped] hash) -> ClientConfigProfile + + def self.load: ( + ?profile: String?, + ?config_source: (Pathname | String)?, + ?disable_file: bool, + ?disable_env: bool, + ?config_file_strict: bool, + ?override_env_vars: Hash[String, String]? + ) -> ClientConfigProfile + + def initialize: ( + ?address: String?, + ?namespace: String?, + ?api_key: String?, + ?tls: ClientConfigTLS?, + ?grpc_meta: Hash[untyped, untyped] + ) -> void + + def to_hash: -> Hash[Symbol, untyped] + def to_client_connect_config: -> Hash[Symbol, untyped] + end + + class ClientConfig + attr_reader profiles: Hash[String, ClientConfigProfile] + + def self.from_hash: (Hash[untyped, untyped] hash) -> ClientConfig + + def self.load: ( + ?config_source: (Pathname | String)?, + ?disable_file: bool, + ?config_file_strict: bool, + ?override_env_vars: Hash[String, String]? + ) -> ClientConfig + + def self.load_client_connect_config: ( + ?profile: String?, + ?config_source: (Pathname | String)?, + ?disable_file: bool, + ?disable_env: bool, + ?config_file_strict: bool, + ?override_env_vars: Hash[String, String]? + ) -> Hash[Symbol, untyped] + + def initialize: (Hash[String, ClientConfigProfile] profiles) -> void + def to_hash: -> Hash[String, Hash[Symbol, untyped]] + end + end +end \ No newline at end of file diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index c702d5b8..65a58d5a 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -4,6 +4,7 @@ require 'pathname' require 'temporalio/client' require 'temporalio/envconfig' +require 'tmpdir' require_relative 'test' class EnvConfigTest < Test @@ -93,7 +94,7 @@ def test_load_profile_from_file_custom assert_equal 'custom-address', profile.address assert_equal 'custom-namespace', profile.namespace refute_nil profile.tls - assert_equal 'custom-server-name', profile.tls.server_name + assert_equal 'custom-server-name', profile.tls.server_name # steep:ignore assert_equal 'custom-value', profile.grpc_meta['custom-header'] config = profile.to_client_connect_config @@ -123,7 +124,7 @@ def test_load_profile_from_data_custom assert_equal 'custom-address', profile.address assert_equal 'custom-namespace', profile.namespace refute_nil profile.tls - assert_equal 'custom-server-name', profile.tls.server_name + assert_equal 'custom-server-name', profile.tls.server_name # steep:ignore assert_equal 'custom-value', profile.grpc_meta['custom-header'] config = profile.to_client_connect_config @@ -166,7 +167,7 @@ def test_load_profile_env_overrides assert_equal 'env-namespace', profile.namespace assert_equal 'env-api-key', profile.api_key refute_nil profile.tls - assert_equal 'env-server-name', profile.tls.server_name + assert_equal 'env-server-name', profile.tls.server_name # steep:ignore config = profile.to_client_connect_config assert_equal 'env-address', config[:target_host] @@ -369,7 +370,7 @@ def test_load_profile_tls_options config_source: TOML_CONFIG_TLS_DETAILED, profile: 'tls_disabled' ) refute_nil profile_disabled.tls - assert profile_disabled.tls.disabled + assert profile_disabled.tls.disabled # steep:ignore config_disabled = profile_disabled.to_client_connect_config assert_equal false, config_disabled[:tls] @@ -379,13 +380,13 @@ def test_load_profile_tls_options config_source: TOML_CONFIG_TLS_DETAILED, profile: 'tls_with_certs' ) refute_nil profile_certs.tls - assert_equal 'custom-server', profile_certs.tls.server_name - refute_nil profile_certs.tls.server_root_ca_cert - assert_equal 'ca-pem-data', profile_certs.tls.server_root_ca_cert - refute_nil profile_certs.tls.client_cert - assert_equal 'client-crt-data', profile_certs.tls.client_cert - refute_nil profile_certs.tls.client_private_key - assert_equal 'client-key-data', profile_certs.tls.client_private_key + assert_equal 'custom-server', profile_certs.tls.server_name # steep:ignore + refute_nil profile_certs.tls.server_root_ca_cert # steep:ignore + assert_equal 'ca-pem-data', profile_certs.tls.server_root_ca_cert # steep:ignore + refute_nil profile_certs.tls.client_cert # steep:ignore + assert_equal 'client-crt-data', profile_certs.tls.client_cert # steep:ignore + refute_nil profile_certs.tls.client_private_key # steep:ignore + assert_equal 'client-key-data', profile_certs.tls.client_private_key # steep:ignore config_certs = profile_certs.to_client_connect_config tls_config_certs = config_certs[:tls] @@ -397,7 +398,7 @@ def test_load_profile_tls_options end def test_load_profile_tls_from_paths - Dir.mktmpdir do |tmpdir| + Dir.mktmpdir do |tmpdir| # steep:ignore # Create dummy cert files ca_pem_path = File.join(tmpdir, 'ca.pem') client_crt_path = File.join(tmpdir, 'client.crt') @@ -419,13 +420,13 @@ def test_load_profile_tls_from_paths profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: toml_config) refute_nil profile.tls - assert_equal 'custom-server', profile.tls.server_name - refute_nil profile.tls.server_root_ca_cert - assert_equal ca_pem_path, profile.tls.server_root_ca_cert - refute_nil profile.tls.client_cert - assert_equal client_crt_path, profile.tls.client_cert - refute_nil profile.tls.client_private_key - assert_equal client_key_path, profile.tls.client_private_key + assert_equal 'custom-server', profile.tls.server_name # steep:ignore + refute_nil profile.tls.server_root_ca_cert # steep:ignore + assert_equal ca_pem_path, profile.tls.server_root_ca_cert # steep:ignore + refute_nil profile.tls.client_cert # steep:ignore + assert_equal client_crt_path, profile.tls.client_cert # steep:ignore + refute_nil profile.tls.client_private_key # steep:ignore + assert_equal client_key_path, profile.tls.client_private_key # steep:ignore config = profile.to_client_connect_config tls_config = config[:tls] @@ -895,7 +896,7 @@ def test_e2e_multi_profile_different_client_connections private def with_temp_config_file(content) - Dir.mktmpdir do |tmpdir| + Dir.mktmpdir do |tmpdir| # steep:ignore config_file = File.join(tmpdir, 'config.toml') File.write(config_file, content) yield config_file diff --git a/temporalio/test/sig/envconfig_test.rbs b/temporalio/test/sig/envconfig_test.rbs new file mode 100644 index 00000000..4fac79fa --- /dev/null +++ b/temporalio/test/sig/envconfig_test.rbs @@ -0,0 +1,10 @@ +class EnvConfigTest < Test + TOML_CONFIG_BASE: String + TOML_CONFIG_STRICT_FAIL: String + TOML_CONFIG_MALFORMED: String + TOML_CONFIG_TLS_DETAILED: String + + private + + def with_temp_config_file: [T] (String content) { (String config_file) -> T } -> T +end \ No newline at end of file From a79438c2b7762a02f253bd7f6ee5676b0ce857e5 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 25 Aug 2025 17:17:02 -0400 Subject: [PATCH 04/31] formatting --- temporalio/ext/src/envconfig.rs | 40 ++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/temporalio/ext/src/envconfig.rs b/temporalio/ext/src/envconfig.rs index 0e63d9d8..0e7d31e4 100644 --- a/temporalio/ext/src/envconfig.rs +++ b/temporalio/ext/src/envconfig.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; -use magnus::{Error, RHash, Ruby, function, prelude::*, scan_args, class}; +use magnus::{Error, RHash, Ruby, class, function, prelude::*, scan_args}; use temporal_sdk_core_api::envconfig::{ + ClientConfig as CoreClientConfig, ClientConfigCodec, + ClientConfigProfile as CoreClientConfigProfile, ClientConfigTLS as CoreClientConfigTLS, + DataSource, LoadClientConfigOptions, LoadClientConfigProfileOptions, load_client_config as core_load_client_config, load_client_config_profile as core_load_client_config_profile, - ClientConfig as CoreClientConfig, ClientConfigCodec, ClientConfigProfile as CoreClientConfigProfile, - ClientConfigTLS as CoreClientConfigTLS, DataSource, LoadClientConfigOptions, - LoadClientConfigProfileOptions, }; use crate::{ROOT_MOD, error}; @@ -16,7 +16,10 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> { let class = root_mod.define_class("EnvConfig", class::object())?; class.define_singleton_method("load_client_config", function!(load_client_config, -1))?; - class.define_singleton_method("load_client_connect_config", function!(load_client_connect_config, -1))?; + class.define_singleton_method( + "load_client_connect_config", + function!(load_client_connect_config, -1), + )?; Ok(()) } @@ -37,7 +40,7 @@ fn data_source_to_hash(ruby: &Ruby, ds: &DataSource) -> Result { fn tls_to_hash(ruby: &Ruby, tls: &CoreClientConfigTLS) -> Result { let hash = RHash::new(); hash.aset("disabled", tls.disabled)?; - + if let Some(v) = &tls.client_cert { hash.aset("client_cert", data_source_to_hash(ruby, v)?)?; } @@ -51,7 +54,7 @@ fn tls_to_hash(ruby: &Ruby, tls: &CoreClientConfigTLS) -> Result { hash.aset("server_name", ruby.str_new(v))?; } hash.aset("disable_host_verification", tls.disable_host_verification)?; - + Ok(hash) } @@ -68,7 +71,7 @@ fn codec_to_hash(ruby: &Ruby, codec: &ClientConfigCodec) -> Result fn profile_to_hash(ruby: &Ruby, profile: &CoreClientConfigProfile) -> Result { let hash = RHash::new(); - + if let Some(v) = &profile.address { hash.aset("address", ruby.str_new(v))?; } @@ -91,7 +94,7 @@ fn profile_to_hash(ruby: &Ruby, profile: &CoreClientConfigProfile) -> Result Result { (None, Some(d)) => Some(DataSource::Data(d)), (None, None) => None, (Some(_), Some(_)) => { - return Err(error!("Cannot specify both path and data for config source")); + return Err(error!( + "Cannot specify both path and data for config source" + )); } }; @@ -184,7 +189,14 @@ fn load_client_config(args: &[magnus::Value]) -> Result { fn load_client_connect_config(args: &[magnus::Value]) -> Result { let ruby = Ruby::get().expect("Not in Ruby thread"); let args = scan_args::scan_args::< - (Option, Option, Option>, bool, bool, bool), + ( + Option, + Option, + Option>, + bool, + bool, + bool, + ), (Option>,), (), (), @@ -199,7 +211,9 @@ fn load_client_connect_config(args: &[magnus::Value]) -> Result { (None, Some(d)) => Some(DataSource::Data(d)), (None, None) => None, (Some(_), Some(_)) => { - return Err(error!("Cannot specify both path and data for config source")); + return Err(error!( + "Cannot specify both path and data for config source" + )); } }; @@ -212,4 +226,4 @@ fn load_client_connect_config(args: &[magnus::Value]) -> Result { config_file_strict, env_vars, ) -} \ No newline at end of file +} From 6dfaca35f9748c9431c0c87a95b7e3ebaeb8c421 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Fri, 5 Sep 2025 11:29:49 -0400 Subject: [PATCH 05/31] Refactor envconfig classes to use Data.define for immutability - Refactor ClientConfigTLS, ClientConfigProfile, and ClientConfig to use Data.define --- temporalio/lib/temporalio/envconfig.rb | 107 +++++++++---------------- temporalio/test/envconfig_test.rb | 6 +- 2 files changed, 43 insertions(+), 70 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index f4c6ac73..665f8aac 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -42,8 +42,13 @@ def self.source_to_path_and_data(source) # @return [Pathname, String, nil] Client certificate source # @!attribute [r] client_private_key # @return [Pathname, String, nil] Client key source + ClientConfigTLS = Data.define(:disabled, :server_name, :server_root_ca_cert, :client_cert, :client_private_key) + class ClientConfigTLS - attr_reader :disabled, :server_name, :server_root_ca_cert, :client_cert, :client_private_key + # Set default values + def initialize(disabled: false, server_name: nil, server_root_ca_cert: nil, client_cert: nil, client_private_key: nil) + super + end # Create a ClientConfigTLS from a hash # @param hash [Hash, nil] Hash representation @@ -75,47 +80,28 @@ def self.hash_to_source(hash) end end - # @param disabled [Boolean] If true, TLS is explicitly disabled - # @param server_name [String, nil] SNI override - # @param server_root_ca_cert [Pathname, String, nil] Server CA certificate source - # @param client_cert [Pathname, String, nil] Client certificate source - # @param client_private_key [Pathname, String, nil] Client key source - def initialize( - disabled: false, - server_name: nil, - server_root_ca_cert: nil, - client_cert: nil, - client_private_key: nil - ) - @disabled = disabled - @server_name = server_name - @server_root_ca_cert = server_root_ca_cert - @client_cert = client_cert - @client_private_key = client_private_key - end - # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_hash hash = {} - hash[:disabled] = @disabled if @disabled - hash[:server_name] = @server_name if @server_name - hash[:server_ca_cert] = source_to_hash(@server_root_ca_cert) if @server_root_ca_cert - hash[:client_cert] = source_to_hash(@client_cert) if @client_cert - hash[:client_key] = source_to_hash(@client_private_key) if @client_private_key + hash[:disabled] = disabled if disabled + hash[:server_name] = server_name if server_name + hash[:server_ca_cert] = source_to_hash(server_root_ca_cert) if server_root_ca_cert + hash[:client_cert] = source_to_hash(client_cert) if client_cert + hash[:client_key] = source_to_hash(client_private_key) if client_private_key hash end # Create a TLS configuration for use with connections # @return [Hash, false] A TLS config hash or false if disabled def to_connect_tls_config - return false if @disabled + return false if disabled config = {} - config[:domain] = @server_name if @server_name - config[:server_root_ca_cert] = read_source(@server_root_ca_cert) if @server_root_ca_cert - config[:client_cert] = read_source(@client_cert) if @client_cert - config[:client_private_key] = read_source(@client_private_key) if @client_private_key + config[:domain] = server_name if server_name + config[:server_root_ca_cert] = read_source(server_root_ca_cert) if server_root_ca_cert + config[:client_cert] = read_source(client_cert) if client_cert + config[:client_private_key] = read_source(client_private_key) if client_private_key config end @@ -170,8 +156,13 @@ def read_source(source) # @return [ClientConfigTLS, nil] TLS configuration # @!attribute [r] grpc_meta # @return [Hash] gRPC metadata + ClientConfigProfile = Data.define(:address, :namespace, :api_key, :tls, :grpc_meta) + class ClientConfigProfile - attr_reader :address, :namespace, :api_key, :tls, :grpc_meta + # Create a ClientConfigProfile instance with defaults + def initialize(address: nil, namespace: nil, api_key: nil, tls: nil, grpc_meta: {}) + super + end # Create a ClientConfigProfile from a hash # @param hash [Hash] Hash representation @@ -219,37 +210,19 @@ def self.load( from_hash(raw_profile) end - # @param address [String, nil] Client address - # @param namespace [String, nil] Client namespace - # @param api_key [String, nil] Client API key - # @param tls [ClientConfigTLS, nil] TLS configuration - # @param grpc_meta [Hash] gRPC metadata - def initialize( - address: nil, - namespace: nil, - api_key: nil, - tls: nil, - grpc_meta: {} - ) - @address = address - @namespace = namespace - @api_key = api_key - @tls = tls - @grpc_meta = grpc_meta || {} - end # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_hash hash = {} - hash[:address] = @address if @address - hash[:namespace] = @namespace if @namespace - hash[:api_key] = @api_key if @api_key - if @tls - tls_hash = @tls.to_hash # steep:ignore + hash[:address] = address if address + hash[:namespace] = namespace if namespace + hash[:api_key] = api_key if api_key + if tls + tls_hash = tls.to_hash # steep:ignore hash[:tls] = tls_hash unless tls_hash.empty? end - hash[:grpc_meta] = @grpc_meta if @grpc_meta && !@grpc_meta.empty? + hash[:grpc_meta] = grpc_meta if grpc_meta && !grpc_meta.empty? hash end @@ -257,11 +230,11 @@ def to_hash # @return [Hash] Arguments that can be passed to Client.connect def to_client_connect_config config = {} - config[:target_host] = @address if @address - config[:namespace] = @namespace if @namespace - config[:api_key] = @api_key if @api_key - config[:tls] = @tls.to_connect_tls_config if @tls # steep:ignore - config[:rpc_metadata] = @grpc_meta if @grpc_meta && !@grpc_meta.empty? + config[:target_host] = address if address + config[:namespace] = namespace if namespace + config[:api_key] = api_key if api_key + config[:tls] = tls.to_connect_tls_config if tls # steep:ignore + config[:rpc_metadata] = grpc_meta if grpc_meta && !grpc_meta.empty? config end end @@ -272,8 +245,13 @@ def to_client_connect_config # # @!attribute [r] profiles # @return [Hash] Map of profile name to its corresponding ClientConfigProfile + ClientConfig = Data.define(:profiles) + class ClientConfig - attr_reader :profiles + # Create a ClientConfig instance with defaults + def initialize(profiles: {}) + super + end # Create a ClientConfig from a hash # @param hash [Hash] Hash representation @@ -346,15 +324,10 @@ def self.load_client_connect_config( prof.to_client_connect_config end - # @param profiles [Hash] Map of profile name to ClientConfigProfile - def initialize(profiles) - @profiles = profiles || {} - end - # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_hash - @profiles.transform_values(&:to_hash) + profiles.transform_values(&:to_hash) end end end diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index 65a58d5a..ff9b7376 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -593,10 +593,10 @@ def test_client_config_to_from_dict tls: Temporalio::EnvConfig::ClientConfigTLS.new(server_name: 'some-server-name'), grpc_meta: { 'some-header' => 'some-value' } ) - config = Temporalio::EnvConfig::ClientConfig.new( + config = Temporalio::EnvConfig::ClientConfig.new(profiles: { 'default' => profile1, 'custom' => profile2 - ) + }) config_hash = config.to_hash @@ -618,7 +618,7 @@ def test_client_config_to_from_dict assert_equal config.profiles.keys, new_config.profiles.keys # Test empty config - empty_config = Temporalio::EnvConfig::ClientConfig.new({}) + empty_config = Temporalio::EnvConfig::ClientConfig.new(profiles: {}) empty_config_hash = empty_config.to_hash assert_empty empty_config_hash new_empty_config = Temporalio::EnvConfig::ClientConfig.from_hash(empty_config_hash) From 3fb3b512a007cfa73b309f4c3ac70cbd3fdcb21c Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Fri, 5 Sep 2025 11:40:53 -0400 Subject: [PATCH 06/31] Rename hash methods to use idiomatic Ruby naming MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename from_hash → from_h for all envconfig classes - Rename to_hash → to_h for all envconfig classes --- temporalio/lib/temporalio/envconfig.rb | 24 ++++++++++++------------ temporalio/test/envconfig_test.rb | 16 ++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index 665f8aac..fecbed74 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -53,7 +53,7 @@ def initialize(disabled: false, server_name: nil, server_root_ca_cert: nil, clie # Create a ClientConfigTLS from a hash # @param hash [Hash, nil] Hash representation # @return [ClientConfigTLS, nil] The TLS configuration or nil if hash is nil/empty - def self.from_hash(hash) + def self.from_h(hash) return nil if hash.nil? || hash.empty? new( @@ -82,7 +82,7 @@ def self.hash_to_source(hash) # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation - def to_hash + def to_h hash = {} hash[:disabled] = disabled if disabled hash[:server_name] = server_name if server_name @@ -167,12 +167,12 @@ def initialize(address: nil, namespace: nil, api_key: nil, tls: nil, grpc_meta: # Create a ClientConfigProfile from a hash # @param hash [Hash] Hash representation # @return [ClientConfigProfile] The client profile - def self.from_hash(hash) + def self.from_h(hash) new( address: hash[:address] || hash['address'], namespace: hash[:namespace] || hash['namespace'], api_key: hash[:api_key] || hash['api_key'], - tls: ClientConfigTLS.from_hash(hash[:tls] || hash['tls']), + tls: ClientConfigTLS.from_h(hash[:tls] || hash['tls']), grpc_meta: hash[:grpc_meta] || hash['grpc_meta'] || {} ) end @@ -207,19 +207,19 @@ def self.load( override_env_vars || {} ) - from_hash(raw_profile) + from_h(raw_profile) end # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation - def to_hash + def to_h hash = {} hash[:address] = address if address hash[:namespace] = namespace if namespace hash[:api_key] = api_key if api_key if tls - tls_hash = tls.to_hash # steep:ignore + tls_hash = tls.to_h # steep:ignore hash[:tls] = tls_hash unless tls_hash.empty? end hash[:grpc_meta] = grpc_meta if grpc_meta && !grpc_meta.empty? @@ -256,9 +256,9 @@ def initialize(profiles: {}) # Create a ClientConfig from a hash # @param hash [Hash] Hash representation # @return [ClientConfig] The client configuration - def self.from_hash(hash) + def self.from_h(hash) profiles = hash.transform_values do |profile_hash| - ClientConfigProfile.from_hash(profile_hash) + ClientConfigProfile.from_h(profile_hash) end new(profiles) end @@ -290,7 +290,7 @@ def self.load( override_env_vars || {} ) - from_hash(loaded_profiles) + from_h(loaded_profiles) end # Load a single client profile and convert to connect config @@ -326,8 +326,8 @@ def self.load_client_connect_config( # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation - def to_hash - profiles.transform_values(&:to_hash) + def to_h + profiles.transform_values(&:to_h) end end end diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index ff9b7376..8abf2996 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -547,7 +547,7 @@ def test_client_config_profile_to_from_dict grpc_meta: { 'some-header' => 'some-value' } ) - profile_hash = profile.to_hash + profile_hash = profile.to_h # Check hash representation. Note that disabled=false is not in the hash. expected_hash = { @@ -565,7 +565,7 @@ def test_client_config_profile_to_from_dict assert_equal expected_hash, profile_hash # Convert back to profile - new_profile = Temporalio::EnvConfig::ClientConfigProfile.from_hash(profile_hash) + new_profile = Temporalio::EnvConfig::ClientConfigProfile.from_h(profile_hash) # We expect the new profile to be the same assert_equal profile.address, new_profile.address @@ -575,9 +575,9 @@ def test_client_config_profile_to_from_dict # Test with minimal profile profile_minimal = Temporalio::EnvConfig::ClientConfigProfile.new - profile_minimal_hash = profile_minimal.to_hash + profile_minimal_hash = profile_minimal.to_h assert_empty profile_minimal_hash - new_profile_minimal = Temporalio::EnvConfig::ClientConfigProfile.from_hash(profile_minimal_hash) + new_profile_minimal = Temporalio::EnvConfig::ClientConfigProfile.from_h(profile_minimal_hash) assert_nil profile_minimal.address assert_nil new_profile_minimal.address end @@ -598,7 +598,7 @@ def test_client_config_to_from_dict 'custom' => profile2 }) - config_hash = config.to_hash + config_hash = config.to_h expected_hash = { 'default' => { @@ -614,14 +614,14 @@ def test_client_config_to_from_dict assert_equal expected_hash, config_hash # Convert back to config - new_config = Temporalio::EnvConfig::ClientConfig.from_hash(config_hash) + new_config = Temporalio::EnvConfig::ClientConfig.from_h(config_hash) assert_equal config.profiles.keys, new_config.profiles.keys # Test empty config empty_config = Temporalio::EnvConfig::ClientConfig.new(profiles: {}) - empty_config_hash = empty_config.to_hash + empty_config_hash = empty_config.to_h assert_empty empty_config_hash - new_empty_config = Temporalio::EnvConfig::ClientConfig.from_hash(empty_config_hash) + new_empty_config = Temporalio::EnvConfig::ClientConfig.from_h(empty_config_hash) assert_empty new_empty_config.profiles end From 12d382489b8a0d0483319b9aa31239a1986dc5d0 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 8 Sep 2025 09:35:33 -0700 Subject: [PATCH 07/31] Replace TLS config hash with Connection::TLSOptions object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename `to_connect_tls_config` → `to_tls_options` - Return `Connection::TLSOptions` object instead of plain Hash --- temporalio/lib/temporalio/envconfig.rb | 24 ++++++++-------- temporalio/test/envconfig_test.rb | 40 +++++++++++++------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index fecbed74..2c038f2a 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -26,7 +26,7 @@ def self.source_to_path_and_data(source) when nil [nil, nil] else - raise TypeError, "config_source must be Pathname, String, or nil, got #{source.class}" + raise TypeError, "Must be Pathname, String, or nil, got #{source.class}" end end @@ -93,16 +93,16 @@ def to_h end # Create a TLS configuration for use with connections - # @return [Hash, false] A TLS config hash or false if disabled - def to_connect_tls_config + # @return [Connection::TLSOptions, false] TLS options or false if disabled + def to_tls_options return false if disabled - config = {} - config[:domain] = server_name if server_name - config[:server_root_ca_cert] = read_source(server_root_ca_cert) if server_root_ca_cert - config[:client_cert] = read_source(client_cert) if client_cert - config[:client_private_key] = read_source(client_private_key) if client_private_key - config + Temporalio::Client::Connection::TLSOptions.new( + domain: server_name, + server_root_ca_cert: read_source(server_root_ca_cert), + client_cert: read_source(client_cert), + client_private_key: read_source(client_private_key) + ) end private @@ -195,9 +195,9 @@ def self.load( config_file_strict: false, override_env_vars: nil ) - path, data = Temporalio::EnvConfig.source_to_path_and_data(config_source) + path, data = EnvConfig.source_to_path_and_data(config_source) - raw_profile = Temporalio::Internal::Bridge::EnvConfig.load_client_connect_config( + raw_profile = Internal::Bridge::EnvConfig.load_client_connect_config( profile, path, data, @@ -233,7 +233,7 @@ def to_client_connect_config config[:target_host] = address if address config[:namespace] = namespace if namespace config[:api_key] = api_key if api_key - config[:tls] = tls.to_connect_tls_config if tls # steep:ignore + config[:tls] = tls.to_tls_options if tls # steep:ignore config[:rpc_metadata] = grpc_meta if grpc_meta && !grpc_meta.empty? config end diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index 8abf2996..2c414b51 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -100,8 +100,8 @@ def test_load_profile_from_file_custom config = profile.to_client_connect_config assert_equal 'custom-address', config[:target_host] tls_config = config[:tls] - assert_instance_of Hash, tls_config - assert_equal 'custom-server-name', tls_config[:domain] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'custom-server-name', tls_config.domain rpc_metadata = config[:rpc_metadata] refute_nil rpc_metadata assert_equal 'custom-value', rpc_metadata['custom-header'] @@ -130,8 +130,8 @@ def test_load_profile_from_data_custom config = profile.to_client_connect_config assert_equal 'custom-address', config[:target_host] tls_config = config[:tls] - assert_instance_of Hash, tls_config - assert_equal 'custom-server-name', tls_config[:domain] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'custom-server-name', tls_config.domain rpc_metadata = config[:rpc_metadata] refute_nil rpc_metadata assert_equal 'custom-value', rpc_metadata['custom-header'] @@ -173,8 +173,8 @@ def test_load_profile_env_overrides assert_equal 'env-address', config[:target_host] assert_equal 'env-api-key', config[:api_key] tls_config = config[:tls] - assert_instance_of Hash, tls_config - assert_equal 'env-server-name', tls_config[:domain] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'env-server-name', tls_config.domain end end @@ -390,11 +390,11 @@ def test_load_profile_tls_options config_certs = profile_certs.to_client_connect_config tls_config_certs = config_certs[:tls] - assert_instance_of Hash, tls_config_certs - assert_equal 'custom-server', tls_config_certs[:domain] - assert_equal 'ca-pem-data', tls_config_certs[:server_root_ca_cert] - assert_equal 'client-crt-data', tls_config_certs[:client_cert] - assert_equal 'client-key-data', tls_config_certs[:client_private_key] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config_certs + assert_equal 'custom-server', tls_config_certs.domain + assert_equal 'ca-pem-data', tls_config_certs.server_root_ca_cert + assert_equal 'client-crt-data', tls_config_certs.client_cert + assert_equal 'client-key-data', tls_config_certs.client_private_key end def test_load_profile_tls_from_paths @@ -430,11 +430,11 @@ def test_load_profile_tls_from_paths config = profile.to_client_connect_config tls_config = config[:tls] - assert_instance_of Hash, tls_config - assert_equal 'custom-server', tls_config[:domain] - assert_equal 'ca-pem-data', tls_config[:server_root_ca_cert] - assert_equal 'client-crt-data', tls_config[:client_cert] - assert_equal 'client-key-data', tls_config[:client_private_key] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'custom-server', tls_config.domain + assert_equal 'ca-pem-data', tls_config.server_root_ca_cert + assert_equal 'client-crt-data', tls_config.client_cert + assert_equal 'client-key-data', tls_config.client_private_key end end @@ -633,8 +633,8 @@ def test_read_source_from_string_content ) config = profile.to_client_connect_config tls_config = config[:tls] - assert_instance_of Hash, tls_config - assert_equal 'string-as-cert-content', tls_config[:client_cert] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'string-as-cert-content', tls_config.client_cert end # ============================================================================= @@ -706,8 +706,8 @@ def test_load_client_connect_config_e2e_validation # TLS configuration (API key should auto-enable TLS) refute_nil connect_config[:tls] tls_config = connect_config[:tls] - assert_equal 'override.temporal.com', tls_config[:domain] # Env override - assert_equal 'prod-ca-cert', tls_config[:server_root_ca_cert] + assert_equal 'override.temporal.com', tls_config.domain # Env override + assert_equal 'prod-ca-cert', tls_config.server_root_ca_cert # gRPC metadata with normalization and env overrides refute_nil connect_config[:rpc_metadata] From a3696a0c61eb3fbd9265d57d4deb49e57a8d3395 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 8 Sep 2025 09:43:45 -0700 Subject: [PATCH 08/31] Refactor to use inline pattern instead of temporary variables --- temporalio/lib/temporalio/envconfig.rb | 45 ++++++++++++-------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index 2c038f2a..ed2ec9d7 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -83,13 +83,13 @@ def self.hash_to_source(hash) # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_h - hash = {} - hash[:disabled] = disabled if disabled - hash[:server_name] = server_name if server_name - hash[:server_ca_cert] = source_to_hash(server_root_ca_cert) if server_root_ca_cert - hash[:client_cert] = source_to_hash(client_cert) if client_cert - hash[:client_key] = source_to_hash(client_private_key) if client_private_key - hash + { + disabled: disabled ? disabled : nil, + server_name: server_name, + server_ca_cert: server_root_ca_cert ? source_to_hash(server_root_ca_cert) : nil, + client_cert: client_cert ? source_to_hash(client_cert) : nil, + client_key: client_private_key ? source_to_hash(client_private_key) : nil + }.compact end # Create a TLS configuration for use with connections @@ -214,28 +214,25 @@ def self.load( # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_h - hash = {} - hash[:address] = address if address - hash[:namespace] = namespace if namespace - hash[:api_key] = api_key if api_key - if tls - tls_hash = tls.to_h # steep:ignore - hash[:tls] = tls_hash unless tls_hash.empty? - end - hash[:grpc_meta] = grpc_meta if grpc_meta && !grpc_meta.empty? - hash + { + address: address, + namespace: namespace, + api_key: api_key, + tls: tls&.to_h&.then { |tls_hash| tls_hash.empty? ? nil : tls_hash }, # steep:ignore + grpc_meta: grpc_meta&.empty? ? nil : grpc_meta + }.compact end # Create a client connect config from this profile # @return [Hash] Arguments that can be passed to Client.connect def to_client_connect_config - config = {} - config[:target_host] = address if address - config[:namespace] = namespace if namespace - config[:api_key] = api_key if api_key - config[:tls] = tls.to_tls_options if tls # steep:ignore - config[:rpc_metadata] = grpc_meta if grpc_meta && !grpc_meta.empty? - config + { + target_host: address, + namespace: namespace, + api_key: api_key, + tls: tls&.to_tls_options, + rpc_metadata: (grpc_meta if grpc_meta && !grpc_meta.empty?) + }.compact end end From 027a1baf4b92c0bd8ad35256279cf7bf8175f6b7 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 8 Sep 2025 15:23:32 -0700 Subject: [PATCH 09/31] Refactor to_client_connect_options to return tuple for one-liner usage Changed `to_client_connect_options` to return `[positional_args, keyword_args]` tuple instead of a hash, enabling clean one-liner client connections: ```ruby args, kwargs = profile.to_client_connect_options client = Temporalio::Client.connect(*args, **kwargs) ``` --- temporalio/lib/temporalio/envconfig.rb | 15 +- temporalio/sig/temporalio/envconfig.rbs | 2 +- temporalio/test/envconfig_test.rb | 185 +++++++++++++----------- 3 files changed, 106 insertions(+), 96 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index ed2ec9d7..8f03e9be 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -144,7 +144,7 @@ def read_source(source) # Represents a client configuration profile. # # This class holds the configuration as loaded from a file or environment. - # See #to_client_connect_config to transform the profile to a connect config hash. + # See #to_client_connect_options to transform the profile to a connect config hash. # # @!attribute [r] address # @return [String, nil] Client address @@ -224,15 +224,16 @@ def to_h end # Create a client connect config from this profile - # @return [Hash] Arguments that can be passed to Client.connect - def to_client_connect_config - { - target_host: address, - namespace: namespace, + # @return [Array] Tuple of [positional_args, keyword_args] that can be splatted to Client.connect + def to_client_connect_options + positional_args = [address, namespace].compact + keyword_args = { api_key: api_key, tls: tls&.to_tls_options, rpc_metadata: (grpc_meta if grpc_meta && !grpc_meta.empty?) }.compact + + [positional_args, keyword_args] end end @@ -318,7 +319,7 @@ def self.load_client_connect_config( config_file_strict: config_file_strict, override_env_vars: override_env_vars ) - prof.to_client_connect_config + prof.to_client_connect_options end # Convert to a hash that can be used for TOML serialization diff --git a/temporalio/sig/temporalio/envconfig.rbs b/temporalio/sig/temporalio/envconfig.rbs index 484688cc..8fdf6f23 100644 --- a/temporalio/sig/temporalio/envconfig.rbs +++ b/temporalio/sig/temporalio/envconfig.rbs @@ -56,7 +56,7 @@ module Temporalio ) -> void def to_hash: -> Hash[Symbol, untyped] - def to_client_connect_config: -> Hash[Symbol, untyped] + def to_client_connect_options: -> Hash[Symbol, untyped] end class ClientConfig diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index 2c414b51..a37a204b 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -75,10 +75,11 @@ def test_load_profile_from_file_default assert_nil profile.tls refute_includes profile.grpc_meta, 'custom-header' - config = profile.to_client_connect_config - assert_equal 'default-address', config[:target_host] - assert_nil config[:tls] - rpc_meta = config[:rpc_metadata] + args, kwargs = profile.to_client_connect_options + assert_equal 'default-address', args[0] + assert_equal 'default-namespace', args[1] + assert_nil kwargs[:tls] + rpc_meta = kwargs[:rpc_metadata] if rpc_meta.nil? assert_nil rpc_meta else @@ -97,12 +98,13 @@ def test_load_profile_from_file_custom assert_equal 'custom-server-name', profile.tls.server_name # steep:ignore assert_equal 'custom-value', profile.grpc_meta['custom-header'] - config = profile.to_client_connect_config - assert_equal 'custom-address', config[:target_host] - tls_config = config[:tls] + args, kwargs = profile.to_client_connect_options + assert_equal 'custom-address', args[0] + assert_equal 'custom-namespace', args[1] + tls_config = kwargs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config assert_equal 'custom-server-name', tls_config.domain - rpc_metadata = config[:rpc_metadata] + rpc_metadata = kwargs[:rpc_metadata] refute_nil rpc_metadata assert_equal 'custom-value', rpc_metadata['custom-header'] end @@ -114,9 +116,10 @@ def test_load_profile_from_data_default assert_equal 'default-namespace', profile.namespace assert_nil profile.tls - config = profile.to_client_connect_config - assert_equal 'default-address', config[:target_host] - assert_nil config[:tls] + args, kwargs = profile.to_client_connect_options + assert_equal 'default-address', args[0] + assert_equal 'default-namespace', args[1] + assert_nil kwargs[:tls] end def test_load_profile_from_data_custom @@ -127,12 +130,13 @@ def test_load_profile_from_data_custom assert_equal 'custom-server-name', profile.tls.server_name # steep:ignore assert_equal 'custom-value', profile.grpc_meta['custom-header'] - config = profile.to_client_connect_config - assert_equal 'custom-address', config[:target_host] - tls_config = config[:tls] + args, kwargs = profile.to_client_connect_options + assert_equal 'custom-address', args[0] + assert_equal 'custom-namespace', args[1] + tls_config = kwargs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config assert_equal 'custom-server-name', tls_config.domain - rpc_metadata = config[:rpc_metadata] + rpc_metadata = kwargs[:rpc_metadata] refute_nil rpc_metadata assert_equal 'custom-value', rpc_metadata['custom-header'] end @@ -148,8 +152,9 @@ def test_load_profile_from_data_env_overrides assert_equal 'env-address', profile.address assert_equal 'env-namespace', profile.namespace - config = profile.to_client_connect_config - assert_equal 'env-address', config[:target_host] + args, kwargs = profile.to_client_connect_options + assert_equal 'env-address', args[0] + assert_equal 'env-namespace', args[1] end def test_load_profile_env_overrides @@ -169,10 +174,11 @@ def test_load_profile_env_overrides refute_nil profile.tls assert_equal 'env-server-name', profile.tls.server_name # steep:ignore - config = profile.to_client_connect_config - assert_equal 'env-address', config[:target_host] - assert_equal 'env-api-key', config[:api_key] - tls_config = config[:tls] + args, kwargs = profile.to_client_connect_options + assert_equal 'env-address', args[0] + assert_equal 'env-namespace', args[1] + assert_equal 'env-api-key', kwargs[:api_key] + tls_config = kwargs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config assert_equal 'env-server-name', tls_config.domain end @@ -196,8 +202,8 @@ def test_load_profile_grpc_meta_env_overrides assert_equal 'env-value', profile.grpc_meta['custom-header'] assert_equal 'another-value', profile.grpc_meta['another-header'] - config = profile.to_client_connect_config - rpc_metadata = config[:rpc_metadata] + args, kwargs = profile.to_client_connect_options + rpc_metadata = kwargs[:rpc_metadata] refute_nil rpc_metadata assert_equal 'env-value', rpc_metadata['custom-header'] assert_equal 'another-value', rpc_metadata['another-header'] @@ -217,8 +223,8 @@ def test_grpc_metadata_normalization_from_toml refute_includes profile.grpc_meta, 'ANOTHER_HEADER_KEY' refute_includes profile.grpc_meta, 'mixed_Case-header' - config = profile.to_client_connect_config - rpc_metadata = config[:rpc_metadata] + args, kwargs = profile.to_client_connect_options + rpc_metadata = kwargs[:rpc_metadata] refute_nil rpc_metadata assert_equal 'custom-value', rpc_metadata['custom-header'] assert_equal 'another-value', rpc_metadata['another-header-key'] @@ -241,8 +247,8 @@ def test_grpc_metadata_deletion_via_empty_env_value # new-header should be added assert_equal 'new-value', profile.grpc_meta['new-header'] - config = profile.to_client_connect_config - rpc_metadata = config[:rpc_metadata] + args, kwargs = profile.to_client_connect_options + rpc_metadata = kwargs[:rpc_metadata] if rpc_metadata && !rpc_metadata.empty? refute_includes rpc_metadata, 'custom-header' assert_equal 'new-value', rpc_metadata['new-header'] @@ -258,8 +264,8 @@ def test_load_profile_disable_env ) assert_equal 'default-address', profile.address - config = profile.to_client_connect_config - assert_equal 'default-address', config[:target_host] + args, kwargs = profile.to_client_connect_options + assert_equal 'default-address', args[0] end end @@ -272,8 +278,8 @@ def test_load_profile_disable_file profile = Temporalio::EnvConfig::ClientConfigProfile.load(disable_file: true, override_env_vars: env) assert_equal 'env-address', profile.address - config = profile.to_client_connect_config - assert_equal 'env-address', config[:target_host] + args, kwargs = profile.to_client_connect_options + assert_equal 'env-address', args[0] end def test_load_profiles_no_env_override @@ -283,8 +289,9 @@ def test_load_profiles_no_env_override 'TEMPORAL_ADDRESS' => 'env-address' # This should be ignored for profiles loading } client_config = Temporalio::EnvConfig::ClientConfig.load(override_env_vars: env) - connect_config = client_config.profiles['default'].to_client_connect_config - assert_equal 'default-address', connect_config[:target_host] + args, kwargs = client_config.profiles['default'].to_client_connect_options + connect_config = { target_host: args[0], namespace: args[1] }.compact.merge(kwargs) + assert_equal 'default-address', args[0] end end @@ -305,15 +312,17 @@ def test_load_profiles_from_file_all assert_includes client_config.profiles, 'default' assert_includes client_config.profiles, 'custom' # Check that we can convert to a connect config - connect_config = client_config.profiles['default'].to_client_connect_config - assert_equal 'default-address', connect_config[:target_host] + args, kwargs = client_config.profiles['default'].to_client_connect_options + connect_config = { target_host: args[0], namespace: args[1] }.compact.merge(kwargs) + assert_equal 'default-address', args[0] end end def test_load_profiles_from_data_all client_config = Temporalio::EnvConfig::ClientConfig.load(config_source: TOML_CONFIG_BASE) assert_equal 2, client_config.profiles.size - connect_config = client_config.profiles['custom'].to_client_connect_config + args, kwargs = client_config.profiles['custom'].to_client_connect_options + connect_config = { target_host: args[0], namespace: args[1] }.compact.merge(kwargs) assert_equal 'custom-address', connect_config[:target_host] end @@ -359,9 +368,9 @@ def test_load_profile_api_key_enables_tls assert_equal 'my-key', profile.api_key refute_nil profile.tls - config = profile.to_client_connect_config - refute_nil config[:tls] - assert_equal 'my-key', config[:api_key] + args, kwargs = profile.to_client_connect_options + refute_nil kwargs[:tls] + assert_equal 'my-key', kwargs[:api_key] end def test_load_profile_tls_options @@ -372,8 +381,8 @@ def test_load_profile_tls_options refute_nil profile_disabled.tls assert profile_disabled.tls.disabled # steep:ignore - config_disabled = profile_disabled.to_client_connect_config - assert_equal false, config_disabled[:tls] + args_disabled, kwargs_disabled = profile_disabled.to_client_connect_options + assert_equal false, kwargs_disabled[:tls] # Test with TLS certs profile_certs = Temporalio::EnvConfig::ClientConfigProfile.load( @@ -388,8 +397,8 @@ def test_load_profile_tls_options refute_nil profile_certs.tls.client_private_key # steep:ignore assert_equal 'client-key-data', profile_certs.tls.client_private_key # steep:ignore - config_certs = profile_certs.to_client_connect_config - tls_config_certs = config_certs[:tls] + args_certs, kwargs_certs = profile_certs.to_client_connect_options + tls_config_certs = kwargs_certs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config_certs assert_equal 'custom-server', tls_config_certs.domain assert_equal 'ca-pem-data', tls_config_certs.server_root_ca_cert @@ -428,8 +437,8 @@ def test_load_profile_tls_from_paths refute_nil profile.tls.client_private_key # steep:ignore assert_equal client_key_path, profile.tls.client_private_key # steep:ignore - config = profile.to_client_connect_config - tls_config = config[:tls] + args, kwargs = profile.to_client_connect_options + tls_config = kwargs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config assert_equal 'custom-server', tls_config.domain assert_equal 'ca-pem-data', tls_config.server_root_ca_cert @@ -631,8 +640,8 @@ def test_read_source_from_string_content address: 'localhost:1234', tls: Temporalio::EnvConfig::ClientConfigTLS.new(client_cert: 'string-as-cert-content') ) - config = profile.to_client_connect_config - tls_config = config[:tls] + args, kwargs = profile.to_client_connect_options + tls_config = kwargs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config assert_equal 'string-as-cert-content', tls_config.client_cert end @@ -644,29 +653,29 @@ def test_read_source_from_string_content def test_load_client_connect_config_convenience_api with_temp_config_file(TOML_CONFIG_BASE) do |config_file| # Test default profile with file - connect_config = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( config_source: Pathname.new(config_file) ) - assert_equal 'default-address', connect_config[:target_host] - assert_equal 'default-namespace', connect_config[:namespace] + assert_equal 'default-address', args[0] + assert_equal 'default-namespace', args[1] # Test with environment overrides env = { 'TEMPORAL_NAMESPACE' => 'env-override-namespace' } - connect_config_with_env = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args_with_env, kwargs_with_env = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( config_source: Pathname.new(config_file), override_env_vars: env ) - assert_equal 'default-address', connect_config_with_env[:target_host] - assert_equal 'env-override-namespace', connect_config_with_env[:namespace] + assert_equal 'default-address', args_with_env[0] + assert_equal 'env-override-namespace', args_with_env[1] # Test with specific profile - connect_config_custom = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args_custom, kwargs_custom = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( profile: 'custom', config_source: Pathname.new(config_file) ) - assert_equal 'custom-address', connect_config_custom[:target_host] - assert_equal 'custom-namespace', connect_config_custom[:namespace] - assert_equal 'custom-api-key', connect_config_custom[:api_key] + assert_equal 'custom-address', args_custom[0] + assert_equal 'custom-namespace', args_custom[1] + assert_equal 'custom-api-key', kwargs_custom[:api_key] end end @@ -692,26 +701,26 @@ def test_load_client_connect_config_e2e_validation 'TEMPORAL_TLS_SERVER_NAME' => 'override.temporal.com' } - connect_config = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( profile: 'production', config_source: toml_content, override_env_vars: env_overrides ) # Validate all configuration aspects - assert_equal 'prod.temporal.com:443', connect_config[:target_host] - assert_equal 'production-ns', connect_config[:namespace] - assert_equal 'prod-api-key', connect_config[:api_key] + assert_equal 'prod.temporal.com:443', args[0] + assert_equal 'production-ns', args[1] + assert_equal 'prod-api-key', kwargs[:api_key] # TLS configuration (API key should auto-enable TLS) - refute_nil connect_config[:tls] - tls_config = connect_config[:tls] + refute_nil kwargs[:tls] + tls_config = kwargs[:tls] assert_equal 'override.temporal.com', tls_config.domain # Env override assert_equal 'prod-ca-cert', tls_config.server_root_ca_cert # gRPC metadata with normalization and env overrides - refute_nil connect_config[:rpc_metadata] - rpc_metadata = connect_config[:rpc_metadata] + refute_nil kwargs[:rpc_metadata] + rpc_metadata = kwargs[:rpc_metadata] assert_equal 'Bearer prod-token', rpc_metadata['authorization'] assert_equal 'prod-value', rpc_metadata['x-custom-header'] assert_equal 'production', rpc_metadata['x-environment'] # From env @@ -736,14 +745,14 @@ def test_e2e_basic_development_profile_client_connection config_source: toml_content ) - connect_config = profile.to_client_connect_config + args, kwargs = profile.to_client_connect_options # Create actual Temporal client using envconfig client = Temporalio::Client.connect( - connect_config[:target_host], - connect_config[:namespace], - api_key: connect_config[:api_key], - tls: connect_config[:tls], + args[0], + args[1], + api_key: kwargs[:api_key], + tls: kwargs[:tls], rpc_metadata: profile.grpc_meta, lazy_connect: true ) @@ -774,14 +783,14 @@ def test_e2e_production_tls_api_key_client_connection config_source: toml_content ) - connect_config = profile.to_client_connect_config + args, kwargs = profile.to_client_connect_options # Create TLS-enabled client with API key client = Temporalio::Client.connect( - connect_config[:target_host], - connect_config[:namespace], - api_key: connect_config[:api_key], - tls: connect_config[:tls], + args[0], + args[1], + api_key: kwargs[:api_key], + tls: kwargs[:tls], rpc_metadata: profile.grpc_meta, lazy_connect: true ) @@ -819,12 +828,12 @@ def test_e2e_environment_overrides_client_connection override_env_vars: env_overrides ) - connect_config = profile.to_client_connect_config + args, kwargs = profile.to_client_connect_options # Create client with environment overrides client = Temporalio::Client.connect( - connect_config[:target_host], - connect_config[:namespace], + args[0], + args[1], rpc_metadata: profile.grpc_meta, lazy_connect: true ) @@ -857,12 +866,12 @@ def test_e2e_multi_profile_different_client_connections config_source: toml_content ) - dev_config = dev_profile.to_client_connect_config + args_dev, kwargs_dev = dev_profile.to_client_connect_options dev_client = Temporalio::Client.connect( - dev_config[:target_host], - dev_config[:namespace], - api_key: dev_config[:api_key], - tls: dev_config[:tls], + args_dev[0], + args_dev[1], + api_key: kwargs_dev[:api_key], + tls: kwargs_dev[:tls], lazy_connect: true ) @@ -872,12 +881,12 @@ def test_e2e_multi_profile_different_client_connections config_source: toml_content ) - prod_config = prod_profile.to_client_connect_config + args_prod, kwargs_prod = prod_profile.to_client_connect_options prod_client = Temporalio::Client.connect( - prod_config[:target_host], - prod_config[:namespace], - api_key: prod_config[:api_key], - tls: prod_config[:tls], + args_prod[0], + args_prod[1], + api_key: kwargs_prod[:api_key], + tls: kwargs_prod[:tls], lazy_connect: true ) From 902dbfe7d1373be0ca684a1f794b6a7d3a5f9cda Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 8 Sep 2025 17:15:19 -0700 Subject: [PATCH 10/31] Make hash methods private --- temporalio/lib/temporalio/envconfig.rb | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index 8f03e9be..5bcf66ec 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -65,20 +65,6 @@ def self.from_h(hash) ) end - # Convert a hash representation to a data source - # @param hash [Hash, nil] Hash with :path or :data key - # @return [Pathname, String, nil] Data source - def self.hash_to_source(hash) - return nil if hash.nil? - - # Always expect a hash with path or data - if hash[:path] || hash['path'] - # Return path as string to match old behavior - hash[:path] || hash['path'] - elsif hash[:data] || hash['data'] - hash[:data] || hash['data'] - end - end # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation @@ -107,6 +93,18 @@ def to_tls_options private + def self.hash_to_source(hash) + return nil if hash.nil? + + # Always expect a hash with path or data + if hash[:path] || hash['path'] + # Return path as string to match old behavior + hash[:path] || hash['path'] + elsif hash[:data] || hash['data'] + hash[:data] || hash['data'] + end + end + def source_to_hash(source) case source when Pathname From 4979a3d7013c74ce0186acceb2dd658e24a43eb5 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 8 Sep 2025 17:49:49 -0700 Subject: [PATCH 11/31] Remove File.exist? check by using type system for path vs data distinction - TOML paths (*_path fields) become Pathname objects - TOML data (*_data fields) remain String objects --- temporalio/lib/temporalio/envconfig.rb | 12 +++--------- temporalio/test/envconfig_test.rb | 6 +++--- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index 5bcf66ec..440f5ef1 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -98,8 +98,7 @@ def self.hash_to_source(hash) # Always expect a hash with path or data if hash[:path] || hash['path'] - # Return path as string to match old behavior - hash[:path] || hash['path'] + Pathname.new(hash[:path] || hash['path']) elsif hash[:data] || hash['data'] hash[:data] || hash['data'] end @@ -124,13 +123,8 @@ def read_source(source) when Pathname File.read(source.to_s) when String - # If it's a string path (from TOML), read the file - # Otherwise return as content - if File.exist?(source) - File.read(source) - else - source - end + # String is always treated as raw data content + source when nil nil else diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index a37a204b..d16edfb8 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -431,11 +431,11 @@ def test_load_profile_tls_from_paths refute_nil profile.tls assert_equal 'custom-server', profile.tls.server_name # steep:ignore refute_nil profile.tls.server_root_ca_cert # steep:ignore - assert_equal ca_pem_path, profile.tls.server_root_ca_cert # steep:ignore + assert_equal Pathname.new(ca_pem_path), profile.tls.server_root_ca_cert # steep:ignore refute_nil profile.tls.client_cert # steep:ignore - assert_equal client_crt_path, profile.tls.client_cert # steep:ignore + assert_equal Pathname.new(client_crt_path), profile.tls.client_cert # steep:ignore refute_nil profile.tls.client_private_key # steep:ignore - assert_equal client_key_path, profile.tls.client_private_key # steep:ignore + assert_equal Pathname.new(client_key_path), profile.tls.client_private_key # steep:ignore args, kwargs = profile.to_client_connect_options tls_config = kwargs[:tls] From 33eccab3cd58a27007c340a743fc146d924414a4 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 8 Sep 2025 18:03:54 -0700 Subject: [PATCH 12/31] Use symbol keys instead of string keys in Rust FFI bridge Changed all hash keys from strings to symbols in the Rust-to-Ruby bridge --- temporalio/ext/src/envconfig.rs | 32 +++++++++++++------------- temporalio/lib/temporalio/envconfig.rb | 28 +++++++++++----------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/temporalio/ext/src/envconfig.rs b/temporalio/ext/src/envconfig.rs index 0e7d31e4..8eff2181 100644 --- a/temporalio/ext/src/envconfig.rs +++ b/temporalio/ext/src/envconfig.rs @@ -28,10 +28,10 @@ fn data_source_to_hash(ruby: &Ruby, ds: &DataSource) -> Result { let hash = RHash::new(); match ds { DataSource::Path(p) => { - hash.aset("path", ruby.str_new(p))?; + hash.aset(ruby.sym_new("path"), ruby.str_new(p))?; } DataSource::Data(d) => { - hash.aset("data", ruby.str_from_slice(d))?; + hash.aset(ruby.sym_new("data"), ruby.str_from_slice(d))?; } } Ok(hash) @@ -39,21 +39,21 @@ fn data_source_to_hash(ruby: &Ruby, ds: &DataSource) -> Result { fn tls_to_hash(ruby: &Ruby, tls: &CoreClientConfigTLS) -> Result { let hash = RHash::new(); - hash.aset("disabled", tls.disabled)?; + hash.aset(ruby.sym_new("disabled"), tls.disabled)?; if let Some(v) = &tls.client_cert { - hash.aset("client_cert", data_source_to_hash(ruby, v)?)?; + hash.aset(ruby.sym_new("client_cert"), data_source_to_hash(ruby, v)?)?; } if let Some(v) = &tls.client_key { - hash.aset("client_key", data_source_to_hash(ruby, v)?)?; + hash.aset(ruby.sym_new("client_key"), data_source_to_hash(ruby, v)?)?; } if let Some(v) = &tls.server_ca_cert { - hash.aset("server_ca_cert", data_source_to_hash(ruby, v)?)?; + hash.aset(ruby.sym_new("server_ca_cert"), data_source_to_hash(ruby, v)?)?; } if let Some(v) = &tls.server_name { - hash.aset("server_name", ruby.str_new(v))?; + hash.aset(ruby.sym_new("server_name"), ruby.str_new(v))?; } - hash.aset("disable_host_verification", tls.disable_host_verification)?; + hash.aset(ruby.sym_new("disable_host_verification"), tls.disable_host_verification)?; Ok(hash) } @@ -61,10 +61,10 @@ fn tls_to_hash(ruby: &Ruby, tls: &CoreClientConfigTLS) -> Result { fn codec_to_hash(ruby: &Ruby, codec: &ClientConfigCodec) -> Result { let hash = RHash::new(); if let Some(v) = &codec.endpoint { - hash.aset("endpoint", ruby.str_new(v))?; + hash.aset(ruby.sym_new("endpoint"), ruby.str_new(v))?; } if let Some(v) = &codec.auth { - hash.aset("auth", ruby.str_new(v))?; + hash.aset(ruby.sym_new("auth"), ruby.str_new(v))?; } Ok(hash) } @@ -73,26 +73,26 @@ fn profile_to_hash(ruby: &Ruby, profile: &CoreClientConfigProfile) -> Result Date: Wed, 3 Sep 2025 12:31:28 -0500 Subject: [PATCH 13/31] Bump golang.org/x/net in /temporalio/test/golangworker (#303) --- temporalio/test/golangworker/go.mod | 12 ++++---- temporalio/test/golangworker/go.sum | 43 ++++++----------------------- 2 files changed, 14 insertions(+), 41 deletions(-) diff --git a/temporalio/test/golangworker/go.mod b/temporalio/test/golangworker/go.mod index 19800d75..ca902718 100644 --- a/temporalio/test/golangworker/go.mod +++ b/temporalio/test/golangworker/go.mod @@ -1,8 +1,6 @@ module github.com/cretz/temporal-sdk-ruby-poc/temporalio/test/golangworker -go 1.22.0 - -toolchain go1.22.4 +go 1.23.0 require go.temporal.io/sdk v1.29.1 @@ -22,10 +20,10 @@ require ( github.com/stretchr/testify v1.9.0 // indirect go.temporal.io/api v1.39.0 // indirect golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect - golang.org/x/net v0.30.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/net v0.38.0 // indirect + golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect golang.org/x/time v0.7.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect diff --git a/temporalio/test/golangworker/go.sum b/temporalio/test/golangworker/go.sum index b47b3f95..fdfeac25 100644 --- a/temporalio/test/golangworker/go.sum +++ b/temporalio/test/golangworker/go.sum @@ -26,8 +26,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -36,8 +35,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -77,12 +74,8 @@ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.temporal.io/api v1.34.0 h1:RBQtYF+jJa252uruscL0TULgdFNqUkhk5R7Bj8PT2ko= -go.temporal.io/api v1.34.0/go.mod h1:YN5Ty/DSp7uAdJxLxup+Y3aQLM00q+7cZuOEGFJ2Ob8= go.temporal.io/api v1.39.0 h1:pbhcfvNDB7mllb8lIBqPcg+m6LMG/IhTpdiFxe+0mYk= go.temporal.io/api v1.39.0/go.mod h1:1WwYUMo6lao8yl0371xWUm13paHExN5ATYT/B7QtFis= -go.temporal.io/sdk v1.27.0 h1:C5oOE/IRyLcZaFoB13kEHsjvSHEnGcwT6bNys0HFFHk= -go.temporal.io/sdk v1.27.0/go.mod h1:PnOq5f3dWuU2NAbY+yczXkIeycsIIdBtoCO62ZE0aak= go.temporal.io/sdk v1.29.1 h1:y+sUMbUhTU9rj50mwIZAPmcXCtgUdOWS9xHDYRYSgZ0= go.temporal.io/sdk v1.29.1/go.mod h1:kp//DRvn3CqQVBCtjL51Oicp9wrZYB2s6row1UgzcKQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= @@ -93,8 +86,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= -golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY= golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -113,10 +104,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -124,8 +113,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -135,19 +124,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -169,12 +152,8 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e h1:SkdGTrROJl2jRGT/Fxv5QUf9jtdKCQh4KQJXbXVLAi0= -google.golang.org/genproto/googleapis/api v0.0.0-20240521202816-d264139d666e/go.mod h1:LweJcLbyVij6rCex8YunD8DYR5VDonap/jYl3ZRxcIU= google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53 h1:fVoAXEKA4+yufmbdVYv+SE73+cPZbbbe8paLsHfkK+U= google.golang.org/genproto/googleapis/api v0.0.0-20241015192408-796eee8c2d53/go.mod h1:riSXTwQ4+nqmPGtobMFyW5FqVAmIs0St6VPp4Ug7CE4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e h1:Elxv5MwEkCI9f5SkoL6afed6NTdxaGoAo39eANBwHL8= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= @@ -182,12 +161,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 0d16b46649a95a00003c7aded9e19598ba2c39f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:11:33 -0500 Subject: [PATCH 14/31] Bump tracing-subscriber from 0.3.19 to 0.3.20 in /temporalio (#331) --- temporalio/Cargo.lock | 46 +++++++++++-------------------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/temporalio/Cargo.lock b/temporalio/Cargo.lock index a6f180ec..1fc3849c 100644 --- a/temporalio/Cargo.lock +++ b/temporalio/Cargo.lock @@ -1789,11 +1789,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -1911,12 +1911,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -2049,12 +2048,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" version = "0.12.4" @@ -2636,17 +2629,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -2657,15 +2641,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -3869,15 +3847,15 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", "parking_lot", - "regex", + "regex-automata", "sharded-slab", "thread_local", "tracing", From 5c49d247907babbf6dafae337aceba554aca5eb3 Mon Sep 17 00:00:00 2001 From: Chad Retz Date: Mon, 8 Sep 2025 22:34:57 +0100 Subject: [PATCH 15/31] Reduce the scope of the illegal call tracer and other tracing fixes (#332) --- temporalio/Steepfile | 2 +- .../internal/worker/workflow_instance.rb | 156 +++++++------- .../workflow_instance/illegal_call_tracer.rb | 27 ++- .../internal/worker/workflow_instance.rbs | 4 - .../workflow_instance/illegal_call_tracer.rbs | 2 +- temporalio/test/gc_utils.rb | 193 ++++++++++++++++++ temporalio/test/sig/gc_utils.rbs | 14 ++ temporalio/test/worker_workflow_child_test.rb | 61 ++++++ temporalio/test/worker_workflow_test.rb | 47 ++++- 9 files changed, 406 insertions(+), 100 deletions(-) create mode 100644 temporalio/test/gc_utils.rb create mode 100644 temporalio/test/sig/gc_utils.rbs diff --git a/temporalio/Steepfile b/temporalio/Steepfile index 368288d5..3d19063c 100644 --- a/temporalio/Steepfile +++ b/temporalio/Steepfile @@ -9,7 +9,7 @@ target :lib do ignore 'lib/temporalio/api', 'lib/temporalio/internal/bridge/api' - library 'uri' + library 'uri', 'objspace' configure_code_diagnostics do |hash| # TODO(cretz): Fix as more protos are generated diff --git a/temporalio/lib/temporalio/internal/worker/workflow_instance.rb b/temporalio/lib/temporalio/internal/worker/workflow_instance.rb index 6ec101e4..2eba80cd 100644 --- a/temporalio/lib/temporalio/internal/worker/workflow_instance.rb +++ b/temporalio/lib/temporalio/internal/worker/workflow_instance.rb @@ -162,86 +162,9 @@ def initialize(details) end def activate(activation) - # Run inside of scheduler - run_in_scheduler { activate_internal(activation) } - end - - def add_command(command) - raise Workflow::InvalidWorkflowStateError, 'Cannot add commands in this context' if @context_frozen - - @commands << command - end - - def instance - @instance or raise 'Instance accessed before created' - end - - def search_attributes - # Lazy on first access - @search_attributes ||= SearchAttributes._from_proto( - @init_job.search_attributes, disable_mutations: true, never_nil: true - ) || raise - end - - def memo - # Lazy on first access - @memo ||= ExternallyImmutableHash.new(ProtoUtils.memo_from_proto(@init_job.memo, payload_converter) || {}) - end - - def now - # Create each time - ProtoUtils.timestamp_to_time(@now_timestamp) or raise 'Time unexpectedly not present' - end - - def illegal_call_tracing_disabled(&) - @tracer.disable(&) - end - - def patch(patch_id:, deprecated:) - # Use memoized result if present. If this is being deprecated, we can still use memoized result and skip the - # command. - patch_id = patch_id.to_s - @patches_memoized ||= {} - @patches_memoized.fetch(patch_id) do - patched = !replaying || @patches_notified.include?(patch_id) - @patches_memoized[patch_id] = patched - if patched - add_command( - Bridge::Api::WorkflowCommands::WorkflowCommand.new( - set_patch_marker: Bridge::Api::WorkflowCommands::SetPatchMarker.new(patch_id:, deprecated:) - ) - ) - end - patched - end - end - - def metric_meter - @metric_meter ||= ReplaySafeMetric::Meter.new( - @runtime_metric_meter.with_additional_attributes( - { - namespace: info.namespace, - task_queue: info.task_queue, - workflow_type: info.workflow_type - } - ) - ) - end - - private - - def run_in_scheduler(&) + # Run inside of scheduler (removed on ensure) Fiber.set_scheduler(@scheduler) - if @tracer - @tracer.enable(&) - else - yield - end - ensure - Fiber.set_scheduler(nil) - end - def activate_internal(activation) # Reset some activation state @commands = [] @current_activation_error = nil @@ -266,8 +189,12 @@ def activate_internal(activation) # the first activation) @primary_fiber ||= schedule(top_level: true) { run_workflow } - # Run the event loop - @scheduler.run_until_all_yielded + # Run the event loop in the tracer if it exists + if @tracer + @tracer.enable { @scheduler.run_until_all_yielded } + else + @scheduler.run_until_all_yielded + end rescue Exception => e # rubocop:disable Lint/RescueException on_top_level_exception(e) end @@ -306,8 +233,77 @@ def activate_internal(activation) ensure @commands = nil @current_activation_error = nil + Fiber.set_scheduler(nil) + end + + def add_command(command) + raise Workflow::InvalidWorkflowStateError, 'Cannot add commands in this context' if @context_frozen + + @commands << command + end + + def instance + @instance or raise 'Instance accessed before created' + end + + def search_attributes + # Lazy on first access + @search_attributes ||= SearchAttributes._from_proto( + @init_job.search_attributes, disable_mutations: true, never_nil: true + ) || raise + end + + def memo + # Lazy on first access + @memo ||= ExternallyImmutableHash.new(ProtoUtils.memo_from_proto(@init_job.memo, payload_converter) || {}) + end + + def now + # Create each time + ProtoUtils.timestamp_to_time(@now_timestamp) or raise 'Time unexpectedly not present' end + def illegal_call_tracing_disabled(&) + if @tracer + @tracer.disable_temporarily(&) + else + yield + end + end + + def patch(patch_id:, deprecated:) + # Use memoized result if present. If this is being deprecated, we can still use memoized result and skip the + # command. + patch_id = patch_id.to_s + @patches_memoized ||= {} + @patches_memoized.fetch(patch_id) do + patched = !replaying || @patches_notified.include?(patch_id) + @patches_memoized[patch_id] = patched + if patched + add_command( + Bridge::Api::WorkflowCommands::WorkflowCommand.new( + set_patch_marker: Bridge::Api::WorkflowCommands::SetPatchMarker.new(patch_id:, deprecated:) + ) + ) + end + patched + end + end + + def metric_meter + @metric_meter ||= ReplaySafeMetric::Meter.new( + @runtime_metric_meter.with_additional_attributes( + { + namespace: info.namespace, + task_queue: info.task_queue, + workflow_type: info.workflow_type + } + ) + ) + end + + private + def create_instance # Convert workflow arguments @workflow_arguments = convert_args(payload_array: @init_job.arguments, diff --git a/temporalio/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb b/temporalio/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb index 4661c995..54455c1f 100644 --- a/temporalio/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb +++ b/temporalio/lib/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rb @@ -84,7 +84,7 @@ def initialize(illegal_calls) when :all '' when Temporalio::Worker::IllegalWorkflowCallValidator - disable do + disable_temporarily do vals.block.call(Temporalio::Worker::IllegalWorkflowCallValidator::CallInfo.new( class_name:, method_name: tp.callee_id, trace_point: tp )) @@ -98,7 +98,7 @@ def initialize(illegal_calls) when true '' when Temporalio::Worker::IllegalWorkflowCallValidator - disable do + disable_temporarily do per_method.block.call(Temporalio::Worker::IllegalWorkflowCallValidator::CallInfo.new( class_name:, method_name: tp.callee_id, trace_point: tp )) @@ -118,8 +118,11 @@ def initialize(illegal_calls) end def enable(&block) - # We've seen leaking issues in Ruby 3.2 where the TracePoint inadvertently remains enabled even for threads - # that it was not started on. So we will check the thread ourselves. + # This is not reentrant and not expected to be called as such. We've seen leaking issues in Ruby 3.2 where + # the TracePoint inadvertently remains enabled even for threads that it was not started on. So we will check + # the thread ourselves. We also use the "enabled thread" concept for disabling checks too, see + # disable_temporarily for more details. + @enabled_thread = Thread.current @tracepoint.enable do block.call @@ -128,13 +131,17 @@ def enable(&block) end end - def disable(&block) + def disable_temporarily(&) + # An earlier version of this used @tracepoint.disable, but in some versions of Ruby, the observed behavior + # is confusingly not reentrant or at least not predictable. Therefore, instead of calling + # @tracepoint.disable, we are just unsetting the enabled thread. This means the tracer is still running, but + # no checks are performed. This is effectively a no-op if tracing was never enabled. + previous_thread = @enabled_thread - @tracepoint.disable do - block.call - ensure - @enabled_thread = previous_thread - end + @enabled_thread = nil + yield + ensure + @enabled_thread = previous_thread end end end diff --git a/temporalio/sig/temporalio/internal/worker/workflow_instance.rbs b/temporalio/sig/temporalio/internal/worker/workflow_instance.rbs index 63561199..e2d10cea 100644 --- a/temporalio/sig/temporalio/internal/worker/workflow_instance.rbs +++ b/temporalio/sig/temporalio/internal/worker/workflow_instance.rbs @@ -58,10 +58,6 @@ module Temporalio def metric_meter: -> Temporalio::Metric::Meter - def run_in_scheduler: [T] { -> T } -> T - - def activate_internal: (untyped activation) -> untyped - def create_instance: -> Temporalio::Workflow::Definition def apply: (untyped job) -> void diff --git a/temporalio/sig/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rbs b/temporalio/sig/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rbs index 4777f98b..4a2a4185 100644 --- a/temporalio/sig/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rbs +++ b/temporalio/sig/temporalio/internal/worker/workflow_instance/illegal_call_tracer.rbs @@ -12,7 +12,7 @@ module Temporalio ) -> void def enable: [T] { -> T } -> T - def disable: [T] { -> T } -> T + def disable_temporarily: [T] { -> T } -> T end end end diff --git a/temporalio/test/gc_utils.rb b/temporalio/test/gc_utils.rb new file mode 100644 index 00000000..a8f2aa4f --- /dev/null +++ b/temporalio/test/gc_utils.rb @@ -0,0 +1,193 @@ +# frozen_string_literal: true + +require 'objspace' + +module GCUtils + class << self + # Find one path from any GC root to the object with target_id. + # + # @return [Array, String] First value is array of path objects, second is root category. + def find_retaining_path_to(target_id, max_depth: 12, max_visits: 250_000, category_whitelist: nil) + roots = ObjectSpace.reachable_objects_from_root # {category_sym => [objs]} + queue = [] + seen = {} + parent = {} # child_id -> parent_id + root_of = {} # obj_id -> root_category + + roots.each do |category, objs| + next if category_whitelist && !category_whitelist.include?(category) + + objs.each do |o| + id = o.__id__ + next if seen[id] + + seen[id] = true + parent[id] = nil + root_of[id] = category + queue << o + end + end + + visits = 0 + depth = 0 + level_remaining = queue.length + next_level = 0 + + found_leaf = nil + + while !queue.empty? && visits < max_visits && depth <= max_depth + cur = queue.shift + level_remaining -= 1 + visits += 1 + + cid = cur.__id__ + if cid == target_id + found_leaf = cid + break + end + + children = begin + ObjectSpace.reachable_objects_from(cur) + rescue StandardError + nil + end + + children&.each do |child| + chid = child.__id__ + next if seen[chid] + + seen[chid] = true + parent[chid] = cid + root_of[chid] ||= root_of[cid] + queue << child + next_level += 1 + end + + next unless level_remaining.zero? + + depth += 1 + level_remaining = next_level + next_level = 0 + end + + return [[], ''] unless found_leaf + + # Reconstruct path + ids = [] + i = found_leaf + while i + ids << i + i = parent[i] + end + objs = ids.reverse.map do |id| + ObjectSpace._id2ref(id) + rescue StandardError + id + end + [objs, root_of[ids.first]] + end + + # Return string of annotated path + def annotated_path(path, root_category:) + lines = [] + lines << "Retaining path (len=#{path.length}) from ROOT[:#{root_category}] to target:" + return lines.join("\n") if path.empty? + + # First is the root + lines << " ROOT[:#{root_category}] #{describe_obj(path.first)}" + # Then edges with labels + (0...(path.length - 1)).each do |i| + parent = path[i] + child = path[i + 1] + labels = edge_labels(parent, child) + labels.each_with_index do |lab, j| + arrow = (j.zero? ? ' └─' : ' •') + lines << "#{arrow} via #{lab} → #{describe_obj(child)}" + end + end + lines.join("\n") + end + + private + + # Label HOW +parent+ holds a reference to +child+ (ivar name, constant, index, etc.). + def edge_labels(parent, child) + labels = [] + target = child + + # 1) Instance variables (works for Class/Module too – class ivars are ivars on the Class object) + if parent.respond_to?(:instance_variables) + parent.instance_variables.each do |ivar| + labels << "@#{ivar.to_s.delete('@')}" if parent.instance_variable_get(ivar).equal?(target) + rescue StandardError + # Ignore + end + end + + # 2) Class variables on Module/Class + if parent.is_a?(Module) + parent.class_variables.each do |cvar| + labels << cvar.to_s if parent.class_variable_get(cvar).equal?(target) + rescue NameError + # Ignore + end + end + + # 3) Constants on Module/Class (avoid triggering autoload) + if parent.is_a?(Module) + parent.constants(false).each do |c| + next if parent.respond_to?(:autoload?) && parent.autoload?(c) + + if parent.const_defined?(c, false) + v = parent.const_get(c, false) + labels << "::#{c}" if v.equal?(target) + end + rescue NameError, LoadError + # Ignore + end + end + + # 4) Array elements + if parent.is_a?(Array) + parent.each_with_index do |v, i| + labels << "[#{i}]" if v.equal?(target) + end + end + + # 5) Hash entries (key or value) + if parent.is_a?(Hash) + parent.each do |k, v| + labels << "{key #{k.inspect}}" if k.equal?(target) + labels << "{value for #{k.inspect}}" if v.equal?(target) + end + end + + # 6) Struct members + if parent.is_a?(Struct) + parent.members.each do |m| + labels << ".#{m}" if parent[m].equal?(target) + rescue StandardError + # Ignore + end + end + + # 7) Fallback for VM internals + if labels.empty? + begin + labels << '(internal)' if parent.is_a?(ObjectSpace::InternalObjectWrapper) + rescue StandardError + # Ignore + end + end + + labels.empty? ? ['(unknown edge)'] : labels + end + + def describe_obj(obj) + cls = (obj.is_a?(Module) ? obj : obj.class) + "#<#{cls} 0x#{obj.__id__.to_s(16)}>" + rescue StandardError + obj.inspect + end + end +end diff --git a/temporalio/test/sig/gc_utils.rbs b/temporalio/test/sig/gc_utils.rbs new file mode 100644 index 00000000..0c335596 --- /dev/null +++ b/temporalio/test/sig/gc_utils.rbs @@ -0,0 +1,14 @@ +module GCUtils + def self.find_retaining_path_to: ( + Integer target_id, + ?max_depth: Integer, + ?max_visits: Integer, + ?category_whitelist: Array[String]? + ) -> [Array[untyped], String] + + def self.print_annotated_path: (Array[untyped] path, root_category: String) -> void + + def self.edge_labels: (untyped parent, untyped child) -> Array[String] + + def self.describe_obj: (untyped obj) -> String +end \ No newline at end of file diff --git a/temporalio/test/worker_workflow_child_test.rb b/temporalio/test/worker_workflow_child_test.rb index f2d708c7..bbeabe6c 100644 --- a/temporalio/test/worker_workflow_child_test.rb +++ b/temporalio/test/worker_workflow_child_test.rb @@ -276,4 +276,65 @@ def test_search_attributes ) assert_equal({ ATTR_KEY_TEXT.name => 'changed-text', ATTR_KEY_KEYWORD.name => 'some-keyword' }, results) end + + class ManyChildrenActivity < Temporalio::Activity::Definition + def execute(name) + "Hello #{name}" + end + end + + class ManyChildrenChildWorkflow < Temporalio::Workflow::Definition + def execute(name) + Temporalio::Workflow.execute_activity( + ManyChildrenActivity, + name, + start_to_close_timeout: 30 + ) + end + end + + class ManyChildrenWorkflow < Temporalio::Workflow::Definition + COUNT = 500 + + def execute + futures = ManyChildrenWorkflow::COUNT.times.map do |i| + Temporalio::Workflow::Future.new do + Temporalio::Workflow.execute_child_workflow(ManyChildrenChildWorkflow, "Test #{i}") + end + end + + Temporalio::Workflow::Future.all_of(*futures).wait + + 'done' + end + end + + def test_many_children + worker = Temporalio::Worker.new( + client: env.client, + task_queue: "tq-#{SecureRandom.uuid}", + activities: [ManyChildrenActivity], + workflows: [ManyChildrenWorkflow, ManyChildrenChildWorkflow], + # This is a slow test, so we need to beef up the tuner and pollers + tuner: Temporalio::Worker::Tuner.create_fixed( + workflow_slots: ManyChildrenWorkflow::COUNT + 1, + activity_slots: ManyChildrenWorkflow::COUNT + ), + max_concurrent_workflow_task_polls: 60, + max_concurrent_activity_task_polls: 60 + ) + worker.run do + handle = env.client.start_workflow( + ManyChildrenWorkflow, + id: "wf-#{SecureRandom.uuid}", + task_queue: worker.task_queue + ) + assert_equal('done', handle.result) + # Confirm there are expected number of child completions + assert_equal( + ManyChildrenWorkflow::COUNT, + handle.fetch_history_events.count(&:child_workflow_execution_completed_event_attributes) + ) + end + end end diff --git a/temporalio/test/worker_workflow_test.rb b/temporalio/test/worker_workflow_test.rb index 18b5b82a..b0a7d423 100644 --- a/temporalio/test/worker_workflow_test.rb +++ b/temporalio/test/worker_workflow_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'base64_codec' +require 'gc_utils' require 'net/http' require 'temporalio/client' require 'temporalio/testing' @@ -1912,9 +1913,14 @@ def test_fail_workflow_payload_converter class ConfirmGarbageCollectWorkflow < Temporalio::Workflow::Definition @initialized_count = 0 @finalized_count = 0 + @weak_instance = nil + @strong_instance = nil + @instance_object_id = nil class << self - attr_accessor :initialized_count, :finalized_count + attr_accessor :initialized_count, :finalized_count, + :weak_instance, :strong_instance, :instance_object_id, + :weak_fiber, :fiber_object_id def create_finalizer proc { @finalized_count += 1 } @@ -1923,6 +1929,13 @@ def create_finalizer def initialize self.class.initialized_count += 1 + self.class.weak_instance = WeakRef.new(self) + # Uncomment this to cause test to fail + # self.class.strong_instance = self + self.class.instance_object_id = object_id + self.class.weak_fiber = WeakRef.new(Fiber.current) + self.class.fiber_object_id = Fiber.current.object_id + ObjectSpace.define_finalizer(self, self.class.create_finalizer) end @@ -1932,6 +1945,11 @@ def execute end def test_confirm_garbage_collect + skip('Skipping GC collection confirmation until https://github.com/temporalio/sdk-ruby/issues/334') + + # This test confirms the workflow instance is reliably GC'd when workflow/worker done. To confirm the test fails + # when there is still an instance, uncomment the strong_instance set in the initialize of the workflow. + execute_workflow(ConfirmGarbageCollectWorkflow) do |handle| # Wait until it is started assert_eventually { assert handle.fetch_history_events.any?(&:workflow_task_completed_event_attributes) } @@ -1940,10 +1958,31 @@ def test_confirm_garbage_collect assert_equal 0, ConfirmGarbageCollectWorkflow.finalized_count end - # Now with worker shutdown, GC and confirm finalized - assert_eventually do + # Perform a GC and confirm gone. There are cases in Ruby where dead stack slots leave the item around for a bit, so + # we check repeatedly for a bit (every 200ms for 10s). We can't use assert_eventually, because path doesn't show + # well. + start_time = Time.now + loop do GC.start - assert_equal 1, ConfirmGarbageCollectWorkflow.finalized_count + # Break if the instance is gone + break unless ConfirmGarbageCollectWorkflow.weak_fiber.weakref_alive? + + # If this is last iteration, flunk w/ the path + if Time.now - start_time > 10 + path, cat = GCUtils.find_retaining_path_to(ConfirmGarbageCollectWorkflow.fiber_object_id, max_depth: 12) + msg = GCUtils.annotated_path(path, root_category: cat) + msg += "\nPath:\n#{path.map { |p| " Item: #{p}" }.join("\n")}" + # Also display any Thread/Fiber backtraces that are in the path + path.grep(Thread).each do |thread| + msg += "\nThread trace: #{thread.backtrace.join("\n")}" + end + path.grep(Fiber).each do |fiber| + msg += "\nFiber trace: #{fiber.backtrace.join("\n")}" + end + msg += "\nOrig fiber trace: #{ConfirmGarbageCollectWorkflow.weak_fiber.backtrace.join("\n")}" + flunk msg + end + sleep(0.2) end end From b0b337e1dbc5042bdd2d22d4f65d944664fb7b02 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Mon, 8 Sep 2025 18:13:15 -0700 Subject: [PATCH 16/31] Remove unnecessary fully qualified import names --- temporalio/lib/temporalio/envconfig.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index f5fa682f..f5561cd8 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -83,7 +83,7 @@ def to_h def to_tls_options return false if disabled - Temporalio::Client::Connection::TLSOptions.new( + Client::Connection::TLSOptions.new( domain: server_name, server_root_ca_cert: read_source(server_root_ca_cert), client_cert: read_source(client_cert), @@ -270,9 +270,9 @@ def self.load( config_file_strict: false, override_env_vars: nil ) - path, data = Temporalio::EnvConfig.source_to_path_and_data(config_source) + path, data = source_to_path_and_data(config_source) - loaded_profiles = Temporalio::Internal::Bridge::EnvConfig.load_client_config( + loaded_profiles = Internal::Bridge::EnvConfig.load_client_config( path, data, disable_file, From 928070c8a7bd45708d9063200d83a2968c869f0f Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 08:29:05 -0700 Subject: [PATCH 17/31] Remove envconfig import in temporalio.rb --- temporalio/lib/temporalio.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/temporalio/lib/temporalio.rb b/temporalio/lib/temporalio.rb index e8e72687..3bef9dcb 100644 --- a/temporalio/lib/temporalio.rb +++ b/temporalio/lib/temporalio.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require 'temporalio/envconfig' require 'temporalio/version' require 'temporalio/versioning_override' From fae411e1a58b65d7703447cc15e019e50151aa50 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 08:37:36 -0700 Subject: [PATCH 18/31] _source_to_path_and_data prefixed with underscore and private doc visibility --- temporalio/lib/temporalio/envconfig.rb | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index f5561cd8..4cb4d3f2 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -14,10 +14,8 @@ module EnvConfig # - String: TOML configuration content # - nil: No configuration source - # Convert a data source to path and data parameters for the bridge - # @param source [Pathname, String, nil] Configuration source - # @return [Array?>] Tuple of [path, data_bytes] - def self.source_to_path_and_data(source) + # @!visibility private + def self._source_to_path_and_data(source) case source when Pathname [source.to_s, nil] @@ -65,7 +63,6 @@ def self.from_h(hash) ) end - # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_h @@ -187,7 +184,7 @@ def self.load( config_file_strict: false, override_env_vars: nil ) - path, data = EnvConfig.source_to_path_and_data(config_source) + path, data = EnvConfig._source_to_path_and_data(config_source) raw_profile = Internal::Bridge::EnvConfig.load_client_connect_config( profile, @@ -202,7 +199,6 @@ def self.load( from_h(raw_profile) end - # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_h @@ -224,7 +220,7 @@ def to_client_connect_options tls: tls&.to_tls_options, rpc_metadata: (grpc_meta if grpc_meta && !grpc_meta.empty?) }.compact - + [positional_args, keyword_args] end end @@ -270,7 +266,7 @@ def self.load( config_file_strict: false, override_env_vars: nil ) - path, data = source_to_path_and_data(config_source) + path, data = EnvConfig._source_to_path_and_data(config_source) loaded_profiles = Internal::Bridge::EnvConfig.load_client_config( path, From 35c2f479302f032d1ba7074aa8f45b6bd4437d98 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 08:51:00 -0700 Subject: [PATCH 19/31] TLS disabled field now optional --- temporalio/lib/temporalio/envconfig.rb | 8 ++++---- temporalio/test/envconfig_test.rb | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index 4cb4d3f2..f287010f 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -31,7 +31,7 @@ def self._source_to_path_and_data(source) # TLS configuration as specified as part of client configuration # # @!attribute [r] disabled - # @return [Boolean] If true, TLS is explicitly disabled + # @return [Boolean, nil] If true, TLS is explicitly disabled; if nil, not specified # @!attribute [r] server_name # @return [String, nil] SNI override # @!attribute [r] server_root_ca_cert @@ -44,7 +44,7 @@ def self._source_to_path_and_data(source) class ClientConfigTLS # Set default values - def initialize(disabled: false, server_name: nil, server_root_ca_cert: nil, client_cert: nil, client_private_key: nil) + def initialize(disabled: nil, server_name: nil, server_root_ca_cert: nil, client_cert: nil, client_private_key: nil) super end @@ -55,7 +55,7 @@ def self.from_h(hash) return nil if hash.nil? || hash.empty? new( - disabled: hash[:disabled] || false, + disabled: hash[:disabled], server_name: hash[:server_name], server_root_ca_cert: hash_to_source(hash[:server_ca_cert]), client_cert: hash_to_source(hash[:client_cert]), @@ -67,7 +67,7 @@ def self.from_h(hash) # @return [Hash] Dictionary representation def to_h { - disabled: disabled ? disabled : nil, + disabled: disabled, server_name: server_name, server_ca_cert: server_root_ca_cert ? source_to_hash(server_root_ca_cert) : nil, client_cert: client_cert ? source_to_hash(client_cert) : nil, diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index d16edfb8..e2b9beff 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -558,12 +558,13 @@ def test_client_config_profile_to_from_dict profile_hash = profile.to_h - # Check hash representation. Note that disabled=false is not in the hash. + # Check hash representation. disabled=false is now included since it was explicitly set. expected_hash = { address: 'some-address', namespace: 'some-namespace', api_key: 'some-api-key', tls: { + disabled: false, server_name: 'some-server-name', server_ca_cert: { data: 'ca-cert-data' }, client_cert: { path: '/path/to/client.crt' }, From 5af7ceb6a70977d544dc7f95f257710a1e6099ef Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 09:11:43 -0700 Subject: [PATCH 20/31] Rubocop fixes --- temporalio/lib/temporalio/envconfig.rb | 66 +++++++++++++++++--------- temporalio/test/envconfig_test.rb | 45 ++++++++---------- 2 files changed, 64 insertions(+), 47 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index f287010f..da61ac1c 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -42,12 +42,11 @@ def self._source_to_path_and_data(source) # @return [Pathname, String, nil] Client key source ClientConfigTLS = Data.define(:disabled, :server_name, :server_root_ca_cert, :client_cert, :client_private_key) + # TLS configuration for Temporal client connections. + # + # This class provides methods for creating, serializing, and converting + # TLS configuration objects used by Temporal clients. class ClientConfigTLS - # Set default values - def initialize(disabled: nil, server_name: nil, server_root_ca_cert: nil, client_cert: nil, client_private_key: nil) - super - end - # Create a ClientConfigTLS from a hash # @param hash [Hash, nil] Hash representation # @return [ClientConfigTLS, nil] The TLS configuration or nil if hash is nil/empty @@ -63,6 +62,12 @@ def self.from_h(hash) ) end + # Set default values + def initialize(disabled: nil, server_name: nil, server_root_ca_cert: nil, client_cert: nil, + client_private_key: nil) + super + end + # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_h @@ -90,14 +95,19 @@ def to_tls_options private - def self.hash_to_source(hash) - return nil if hash.nil? + class << self + private + + # Convert hash to source object (Pathname or String) + def hash_to_source(hash) + return nil if hash.nil? - # Always expect a hash with path or data - if hash[:path] - Pathname.new(hash[:path]) - elsif hash[:data] - hash[:data] + # Always expect a hash with path or data + if hash[:path] + Pathname.new(hash[:path]) + elsif hash[:data] + hash[:data] + end end end @@ -147,12 +157,12 @@ def read_source(source) # @return [Hash] gRPC metadata ClientConfigProfile = Data.define(:address, :namespace, :api_key, :tls, :grpc_meta) + # A client configuration profile loaded from environment and files. + # + # This class represents a complete client configuration profile that can be + # loaded from TOML files and environment variables, and converted to client + # connection options. class ClientConfigProfile - # Create a ClientConfigProfile instance with defaults - def initialize(address: nil, namespace: nil, api_key: nil, tls: nil, grpc_meta: {}) - super - end - # Create a ClientConfigProfile from a hash # @param hash [Hash] Hash representation # @return [ClientConfigProfile] The client profile @@ -199,6 +209,11 @@ def self.load( from_h(raw_profile) end + # Create a ClientConfigProfile instance with defaults + def initialize(address: nil, namespace: nil, api_key: nil, tls: nil, grpc_meta: {}) + super + end + # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_h @@ -207,7 +222,7 @@ def to_h namespace: namespace, api_key: api_key, tls: tls&.to_h&.then { |tls_hash| tls_hash.empty? ? nil : tls_hash }, # steep:ignore - grpc_meta: grpc_meta&.empty? ? nil : grpc_meta + grpc_meta: grpc_meta && grpc_meta.empty? ? nil : grpc_meta }.compact end @@ -233,12 +248,12 @@ def to_client_connect_options # @return [Hash] Map of profile name to its corresponding ClientConfigProfile ClientConfig = Data.define(:profiles) + # Container for multiple client configuration profiles. + # + # This class holds a collection of named client profiles loaded from + # configuration sources and provides methods for profile management + # and client connection configuration. class ClientConfig - # Create a ClientConfig instance with defaults - def initialize(profiles: {}) - super - end - # Create a ClientConfig from a hash # @param hash [Hash] Hash representation # @return [ClientConfig] The client configuration @@ -310,6 +325,11 @@ def self.load_client_connect_config( prof.to_client_connect_options end + # Create a ClientConfig instance with defaults + def initialize(profiles: {}) + super + end + # Convert to a hash that can be used for TOML serialization # @return [Hash] Dictionary representation def to_h diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index e2b9beff..24ea83d2 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -152,7 +152,7 @@ def test_load_profile_from_data_env_overrides assert_equal 'env-address', profile.address assert_equal 'env-namespace', profile.namespace - args, kwargs = profile.to_client_connect_options + args, = profile.to_client_connect_options assert_equal 'env-address', args[0] assert_equal 'env-namespace', args[1] end @@ -202,7 +202,7 @@ def test_load_profile_grpc_meta_env_overrides assert_equal 'env-value', profile.grpc_meta['custom-header'] assert_equal 'another-value', profile.grpc_meta['another-header'] - args, kwargs = profile.to_client_connect_options + _, kwargs = profile.to_client_connect_options rpc_metadata = kwargs[:rpc_metadata] refute_nil rpc_metadata assert_equal 'env-value', rpc_metadata['custom-header'] @@ -223,7 +223,7 @@ def test_grpc_metadata_normalization_from_toml refute_includes profile.grpc_meta, 'ANOTHER_HEADER_KEY' refute_includes profile.grpc_meta, 'mixed_Case-header' - args, kwargs = profile.to_client_connect_options + _, kwargs = profile.to_client_connect_options rpc_metadata = kwargs[:rpc_metadata] refute_nil rpc_metadata assert_equal 'custom-value', rpc_metadata['custom-header'] @@ -247,7 +247,7 @@ def test_grpc_metadata_deletion_via_empty_env_value # new-header should be added assert_equal 'new-value', profile.grpc_meta['new-header'] - args, kwargs = profile.to_client_connect_options + _, kwargs = profile.to_client_connect_options rpc_metadata = kwargs[:rpc_metadata] if rpc_metadata && !rpc_metadata.empty? refute_includes rpc_metadata, 'custom-header' @@ -264,7 +264,7 @@ def test_load_profile_disable_env ) assert_equal 'default-address', profile.address - args, kwargs = profile.to_client_connect_options + args, = profile.to_client_connect_options assert_equal 'default-address', args[0] end end @@ -278,7 +278,7 @@ def test_load_profile_disable_file profile = Temporalio::EnvConfig::ClientConfigProfile.load(disable_file: true, override_env_vars: env) assert_equal 'env-address', profile.address - args, kwargs = profile.to_client_connect_options + args, = profile.to_client_connect_options assert_equal 'env-address', args[0] end @@ -289,8 +289,7 @@ def test_load_profiles_no_env_override 'TEMPORAL_ADDRESS' => 'env-address' # This should be ignored for profiles loading } client_config = Temporalio::EnvConfig::ClientConfig.load(override_env_vars: env) - args, kwargs = client_config.profiles['default'].to_client_connect_options - connect_config = { target_host: args[0], namespace: args[1] }.compact.merge(kwargs) + args, = client_config.profiles['default'].to_client_connect_options assert_equal 'default-address', args[0] end end @@ -312,8 +311,7 @@ def test_load_profiles_from_file_all assert_includes client_config.profiles, 'default' assert_includes client_config.profiles, 'custom' # Check that we can convert to a connect config - args, kwargs = client_config.profiles['default'].to_client_connect_options - connect_config = { target_host: args[0], namespace: args[1] }.compact.merge(kwargs) + args, = client_config.profiles['default'].to_client_connect_options assert_equal 'default-address', args[0] end end @@ -321,9 +319,8 @@ def test_load_profiles_from_file_all def test_load_profiles_from_data_all client_config = Temporalio::EnvConfig::ClientConfig.load(config_source: TOML_CONFIG_BASE) assert_equal 2, client_config.profiles.size - args, kwargs = client_config.profiles['custom'].to_client_connect_options - connect_config = { target_host: args[0], namespace: args[1] }.compact.merge(kwargs) - assert_equal 'custom-address', connect_config[:target_host] + args, = client_config.profiles['custom'].to_client_connect_options + assert_equal 'custom-address', args[0] end def test_load_profiles_no_config_file @@ -368,7 +365,7 @@ def test_load_profile_api_key_enables_tls assert_equal 'my-key', profile.api_key refute_nil profile.tls - args, kwargs = profile.to_client_connect_options + _, kwargs = profile.to_client_connect_options refute_nil kwargs[:tls] assert_equal 'my-key', kwargs[:api_key] end @@ -381,7 +378,7 @@ def test_load_profile_tls_options refute_nil profile_disabled.tls assert profile_disabled.tls.disabled # steep:ignore - args_disabled, kwargs_disabled = profile_disabled.to_client_connect_options + _, kwargs_disabled = profile_disabled.to_client_connect_options assert_equal false, kwargs_disabled[:tls] # Test with TLS certs @@ -397,7 +394,7 @@ def test_load_profile_tls_options refute_nil profile_certs.tls.client_private_key # steep:ignore assert_equal 'client-key-data', profile_certs.tls.client_private_key # steep:ignore - args_certs, kwargs_certs = profile_certs.to_client_connect_options + _, kwargs_certs = profile_certs.to_client_connect_options tls_config_certs = kwargs_certs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config_certs assert_equal 'custom-server', tls_config_certs.domain @@ -437,7 +434,7 @@ def test_load_profile_tls_from_paths refute_nil profile.tls.client_private_key # steep:ignore assert_equal Pathname.new(client_key_path), profile.tls.client_private_key # steep:ignore - args, kwargs = profile.to_client_connect_options + _, kwargs = profile.to_client_connect_options tls_config = kwargs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config assert_equal 'custom-server', tls_config.domain @@ -604,9 +601,9 @@ def test_client_config_to_from_dict grpc_meta: { 'some-header' => 'some-value' } ) config = Temporalio::EnvConfig::ClientConfig.new(profiles: { - 'default' => profile1, - 'custom' => profile2 - }) + 'default' => profile1, + 'custom' => profile2 + }) config_hash = config.to_h @@ -641,7 +638,7 @@ def test_read_source_from_string_content address: 'localhost:1234', tls: Temporalio::EnvConfig::ClientConfigTLS.new(client_cert: 'string-as-cert-content') ) - args, kwargs = profile.to_client_connect_options + _, kwargs = profile.to_client_connect_options tls_config = kwargs[:tls] assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config assert_equal 'string-as-cert-content', tls_config.client_cert @@ -654,7 +651,7 @@ def test_read_source_from_string_content def test_load_client_connect_config_convenience_api with_temp_config_file(TOML_CONFIG_BASE) do |config_file| # Test default profile with file - args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args, = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( config_source: Pathname.new(config_file) ) assert_equal 'default-address', args[0] @@ -662,7 +659,7 @@ def test_load_client_connect_config_convenience_api # Test with environment overrides env = { 'TEMPORAL_NAMESPACE' => 'env-override-namespace' } - args_with_env, kwargs_with_env = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args_with_env, = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( config_source: Pathname.new(config_file), override_env_vars: env ) @@ -829,7 +826,7 @@ def test_e2e_environment_overrides_client_connection override_env_vars: env_overrides ) - args, kwargs = profile.to_client_connect_options + args, = profile.to_client_connect_options # Create client with environment overrides client = Temporalio::Client.connect( From 7f2f777d27cc2b67338e5c1cb8c9ec3423f73469 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 09:17:57 -0700 Subject: [PATCH 21/31] Steep fixes --- temporalio/lib/temporalio/envconfig.rb | 2 +- temporalio/sig/temporalio/envconfig.rbs | 26 ++++++++++++------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/envconfig.rb index da61ac1c..e1190c4a 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/envconfig.rb @@ -261,7 +261,7 @@ def self.from_h(hash) profiles = hash.transform_values do |profile_hash| ClientConfigProfile.from_h(profile_hash) end - new(profiles) + new(profiles: profiles) end # Load all client profiles from given sources. diff --git a/temporalio/sig/temporalio/envconfig.rbs b/temporalio/sig/temporalio/envconfig.rbs index 8fdf6f23..fed9a717 100644 --- a/temporalio/sig/temporalio/envconfig.rbs +++ b/temporalio/sig/temporalio/envconfig.rbs @@ -1,27 +1,27 @@ module Temporalio module EnvConfig - def self.source_to_path_and_data: (untyped source) -> [String?, Array[Integer]?] + def self._source_to_path_and_data: (untyped source) -> [String?, Array[Integer]?] class ClientConfigTLS - attr_reader disabled: bool + attr_reader disabled: bool? attr_reader server_name: String? attr_reader server_root_ca_cert: (Pathname | String)? attr_reader client_cert: (Pathname | String)? attr_reader client_private_key: (Pathname | String)? - def self.from_hash: (Hash[untyped, untyped]? hash) -> ClientConfigTLS? + def self.from_h: (Hash[untyped, untyped]? hash) -> ClientConfigTLS? def self.hash_to_source: (Hash[untyped, untyped]? hash) -> (Pathname | String)? def initialize: ( - ?disabled: bool, + ?disabled: bool?, ?server_name: String?, ?server_root_ca_cert: (Pathname | String)?, ?client_cert: (Pathname | String)?, ?client_private_key: (Pathname | String)? ) -> void - def to_hash: -> Hash[Symbol, untyped] - def to_connect_tls_config: -> (Hash[Symbol, untyped] | false) + def to_h: -> Hash[Symbol, untyped] + def to_tls_options: -> (untyped | false) private @@ -36,7 +36,7 @@ module Temporalio attr_reader tls: ClientConfigTLS? attr_reader grpc_meta: Hash[untyped, untyped] - def self.from_hash: (Hash[untyped, untyped] hash) -> ClientConfigProfile + def self.from_h: (Hash[untyped, untyped] hash) -> ClientConfigProfile def self.load: ( ?profile: String?, @@ -55,14 +55,14 @@ module Temporalio ?grpc_meta: Hash[untyped, untyped] ) -> void - def to_hash: -> Hash[Symbol, untyped] - def to_client_connect_options: -> Hash[Symbol, untyped] + def to_h: -> Hash[Symbol, untyped] + def to_client_connect_options: -> [Array[untyped], Hash[Symbol, untyped]] end class ClientConfig attr_reader profiles: Hash[String, ClientConfigProfile] - def self.from_hash: (Hash[untyped, untyped] hash) -> ClientConfig + def self.from_h: (Hash[untyped, untyped] hash) -> ClientConfig def self.load: ( ?config_source: (Pathname | String)?, @@ -78,10 +78,10 @@ module Temporalio ?disable_env: bool, ?config_file_strict: bool, ?override_env_vars: Hash[String, String]? - ) -> Hash[Symbol, untyped] + ) -> [Array[untyped], Hash[Symbol, untyped]] - def initialize: (Hash[String, ClientConfigProfile] profiles) -> void - def to_hash: -> Hash[String, Hash[Symbol, untyped]] + def initialize: (?profiles: Hash[String, ClientConfigProfile]) -> void + def to_h: -> Hash[String, Hash[Symbol, untyped]] end end end \ No newline at end of file From 4a276af1992964434859ab6cc005bdea2a7dc673 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 09:25:36 -0700 Subject: [PATCH 22/31] Renamed envconfig.rb -> env_config.rb Renamed to_tls_options -> to_client_tls_options --- temporalio/lib/temporalio/{envconfig.rb => env_config.rb} | 4 ++-- temporalio/sig/temporalio/envconfig.rbs | 2 +- temporalio/test/envconfig_test.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename temporalio/lib/temporalio/{envconfig.rb => env_config.rb} (99%) diff --git a/temporalio/lib/temporalio/envconfig.rb b/temporalio/lib/temporalio/env_config.rb similarity index 99% rename from temporalio/lib/temporalio/envconfig.rb rename to temporalio/lib/temporalio/env_config.rb index e1190c4a..7be8ca1e 100644 --- a/temporalio/lib/temporalio/envconfig.rb +++ b/temporalio/lib/temporalio/env_config.rb @@ -82,7 +82,7 @@ def to_h # Create a TLS configuration for use with connections # @return [Connection::TLSOptions, false] TLS options or false if disabled - def to_tls_options + def to_client_tls_options return false if disabled Client::Connection::TLSOptions.new( @@ -232,7 +232,7 @@ def to_client_connect_options positional_args = [address, namespace].compact keyword_args = { api_key: api_key, - tls: tls&.to_tls_options, + tls: tls&.to_client_tls_options, rpc_metadata: (grpc_meta if grpc_meta && !grpc_meta.empty?) }.compact diff --git a/temporalio/sig/temporalio/envconfig.rbs b/temporalio/sig/temporalio/envconfig.rbs index fed9a717..f258c2e4 100644 --- a/temporalio/sig/temporalio/envconfig.rbs +++ b/temporalio/sig/temporalio/envconfig.rbs @@ -21,7 +21,7 @@ module Temporalio ) -> void def to_h: -> Hash[Symbol, untyped] - def to_tls_options: -> (untyped | false) + def to_client_tls_options: -> (untyped | false) private diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index 24ea83d2..b694bae9 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -3,7 +3,7 @@ require 'fileutils' require 'pathname' require 'temporalio/client' -require 'temporalio/envconfig' +require 'temporalio/env_config' require 'tmpdir' require_relative 'test' From 6fe479e64f05330645b1806a86f82eb64a85d015 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 09:33:04 -0700 Subject: [PATCH 23/31] Pass data string as RString, Rust bridge converts to Vec --- temporalio/ext/src/envconfig.rs | 16 +++++++++++----- temporalio/lib/temporalio/env_config.rb | 2 +- temporalio/sig/temporalio/envconfig.rbs | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/temporalio/ext/src/envconfig.rs b/temporalio/ext/src/envconfig.rs index 8eff2181..79939f0f 100644 --- a/temporalio/ext/src/envconfig.rs +++ b/temporalio/ext/src/envconfig.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use magnus::{Error, RHash, Ruby, class, function, prelude::*, scan_args}; +use magnus::{Error, RHash, RString, Ruby, class, function, prelude::*, scan_args}; use temporal_sdk_core_api::envconfig::{ ClientConfig as CoreClientConfig, ClientConfigCodec, ClientConfigProfile as CoreClientConfigProfile, ClientConfigTLS as CoreClientConfigTLS, @@ -155,7 +155,7 @@ fn load_client_connect_config_inner( fn load_client_config(args: &[magnus::Value]) -> Result { let ruby = Ruby::get().expect("Not in Ruby thread"); let args = scan_args::scan_args::< - (Option, Option>, bool, bool), + (Option, Option, bool, bool), (Option>,), (), (), @@ -167,7 +167,10 @@ fn load_client_config(args: &[magnus::Value]) -> Result { let config_source = match (path, data) { (Some(p), None) => Some(DataSource::Path(p)), - (None, Some(d)) => Some(DataSource::Data(d)), + (None, Some(d)) => { + let bytes = unsafe { d.as_slice().to_vec() }; + Some(DataSource::Data(bytes)) + }, (None, None) => None, (Some(_), Some(_)) => { return Err(error!( @@ -192,7 +195,7 @@ fn load_client_connect_config(args: &[magnus::Value]) -> Result { ( Option, Option, - Option>, + Option, bool, bool, bool, @@ -208,7 +211,10 @@ fn load_client_connect_config(args: &[magnus::Value]) -> Result { let config_source = match (path, data) { (Some(p), None) => Some(DataSource::Path(p)), - (None, Some(d)) => Some(DataSource::Data(d)), + (None, Some(d)) => { + let bytes = unsafe { d.as_slice().to_vec() }; + Some(DataSource::Data(bytes)) + }, (None, None) => None, (Some(_), Some(_)) => { return Err(error!( diff --git a/temporalio/lib/temporalio/env_config.rb b/temporalio/lib/temporalio/env_config.rb index 7be8ca1e..7daed133 100644 --- a/temporalio/lib/temporalio/env_config.rb +++ b/temporalio/lib/temporalio/env_config.rb @@ -20,7 +20,7 @@ def self._source_to_path_and_data(source) when Pathname [source.to_s, nil] when String - [nil, source.encode('UTF-8').bytes] + [nil, source] when nil [nil, nil] else diff --git a/temporalio/sig/temporalio/envconfig.rbs b/temporalio/sig/temporalio/envconfig.rbs index f258c2e4..d5a32795 100644 --- a/temporalio/sig/temporalio/envconfig.rbs +++ b/temporalio/sig/temporalio/envconfig.rbs @@ -1,6 +1,6 @@ module Temporalio module EnvConfig - def self._source_to_path_and_data: (untyped source) -> [String?, Array[Integer]?] + def self._source_to_path_and_data: (untyped source) -> [String?, String?] class ClientConfigTLS attr_reader disabled: bool? From 20f28ff83a67782d26446ae4c650ce96fd68c5da Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 09:37:10 -0700 Subject: [PATCH 24/31] Rename load_client_connect_config -> load_client_connect_options --- temporalio/lib/temporalio/env_config.rb | 4 ++-- temporalio/sig/temporalio/envconfig.rbs | 2 +- temporalio/test/envconfig_test.rb | 12 ++++++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/temporalio/lib/temporalio/env_config.rb b/temporalio/lib/temporalio/env_config.rb index 7daed133..8e4393f1 100644 --- a/temporalio/lib/temporalio/env_config.rb +++ b/temporalio/lib/temporalio/env_config.rb @@ -305,8 +305,8 @@ def self.load( # @param disable_env [Boolean] If true, environment variable loading and overriding is disabled # @param config_file_strict [Boolean] If true, will error on unrecognized keys # @param override_env_vars [Hash, nil] Environment variables to use for loading and overrides - # @return [Hash] Hash of keyword arguments for Client.connect - def self.load_client_connect_config( + # @return [Array] Tuple of [positional_args, keyword_args] that can be splatted to Client.connect + def self.load_client_connect_options( profile: nil, config_source: nil, disable_file: false, diff --git a/temporalio/sig/temporalio/envconfig.rbs b/temporalio/sig/temporalio/envconfig.rbs index d5a32795..99b0380f 100644 --- a/temporalio/sig/temporalio/envconfig.rbs +++ b/temporalio/sig/temporalio/envconfig.rbs @@ -71,7 +71,7 @@ module Temporalio ?override_env_vars: Hash[String, String]? ) -> ClientConfig - def self.load_client_connect_config: ( + def self.load_client_connect_options: ( ?profile: String?, ?config_source: (Pathname | String)?, ?disable_file: bool, diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb index b694bae9..1c84defe 100644 --- a/temporalio/test/envconfig_test.rb +++ b/temporalio/test/envconfig_test.rb @@ -648,10 +648,10 @@ def test_read_source_from_string_content # INTEGRATION/E2E TESTS (2 tests) # ============================================================================= - def test_load_client_connect_config_convenience_api + def test_load_client_connect_options_convenience_api with_temp_config_file(TOML_CONFIG_BASE) do |config_file| # Test default profile with file - args, = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args, = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( config_source: Pathname.new(config_file) ) assert_equal 'default-address', args[0] @@ -659,7 +659,7 @@ def test_load_client_connect_config_convenience_api # Test with environment overrides env = { 'TEMPORAL_NAMESPACE' => 'env-override-namespace' } - args_with_env, = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args_with_env, = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( config_source: Pathname.new(config_file), override_env_vars: env ) @@ -667,7 +667,7 @@ def test_load_client_connect_config_convenience_api assert_equal 'env-override-namespace', args_with_env[1] # Test with specific profile - args_custom, kwargs_custom = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args_custom, kwargs_custom = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( profile: 'custom', config_source: Pathname.new(config_file) ) @@ -677,7 +677,7 @@ def test_load_client_connect_config_convenience_api end end - def test_load_client_connect_config_e2e_validation + def test_load_client_connect_options_e2e_validation # Test comprehensive end-to-end configuration loading with all features toml_content = <<~TOML [profile.production] @@ -699,7 +699,7 @@ def test_load_client_connect_config_e2e_validation 'TEMPORAL_TLS_SERVER_NAME' => 'override.temporal.com' } - args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_config( + args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( profile: 'production', config_source: toml_content, override_env_vars: env_overrides From dee3b0a0b7bcfb75e4cdac33a8564841e95c647f Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 09:54:25 -0700 Subject: [PATCH 25/31] Format envconfig.rs --- temporalio/ext/src/envconfig.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/temporalio/ext/src/envconfig.rs b/temporalio/ext/src/envconfig.rs index 79939f0f..9352ebce 100644 --- a/temporalio/ext/src/envconfig.rs +++ b/temporalio/ext/src/envconfig.rs @@ -48,12 +48,18 @@ fn tls_to_hash(ruby: &Ruby, tls: &CoreClientConfigTLS) -> Result { hash.aset(ruby.sym_new("client_key"), data_source_to_hash(ruby, v)?)?; } if let Some(v) = &tls.server_ca_cert { - hash.aset(ruby.sym_new("server_ca_cert"), data_source_to_hash(ruby, v)?)?; + hash.aset( + ruby.sym_new("server_ca_cert"), + data_source_to_hash(ruby, v)?, + )?; } if let Some(v) = &tls.server_name { hash.aset(ruby.sym_new("server_name"), ruby.str_new(v))?; } - hash.aset(ruby.sym_new("disable_host_verification"), tls.disable_host_verification)?; + hash.aset( + ruby.sym_new("disable_host_verification"), + tls.disable_host_verification, + )?; Ok(hash) } @@ -170,7 +176,7 @@ fn load_client_config(args: &[magnus::Value]) -> Result { (None, Some(d)) => { let bytes = unsafe { d.as_slice().to_vec() }; Some(DataSource::Data(bytes)) - }, + } (None, None) => None, (Some(_), Some(_)) => { return Err(error!( @@ -214,7 +220,7 @@ fn load_client_connect_config(args: &[magnus::Value]) -> Result { (None, Some(d)) => { let bytes = unsafe { d.as_slice().to_vec() }; Some(DataSource::Data(bytes)) - }, + } (None, None) => None, (Some(_), Some(_)) => { return Err(error!( From 656a5119b5ae3a522a606fd974dc7af522ba1fe2 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 15:25:54 -0700 Subject: [PATCH 26/31] Nits --- temporalio/lib/temporalio/env_config.rb | 39 ++++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/temporalio/lib/temporalio/env_config.rb b/temporalio/lib/temporalio/env_config.rb index 8e4393f1..7b537bc2 100644 --- a/temporalio/lib/temporalio/env_config.rb +++ b/temporalio/lib/temporalio/env_config.rb @@ -8,25 +8,6 @@ module Temporalio module EnvConfig # This module provides utilities to load Temporal client configuration from TOML files # and environment variables. - # - # DataSource types: - # - Pathname: Path to a configuration file - # - String: TOML configuration content - # - nil: No configuration source - - # @!visibility private - def self._source_to_path_and_data(source) - case source - when Pathname - [source.to_s, nil] - when String - [nil, source] - when nil - [nil, nil] - else - raise TypeError, "Must be Pathname, String, or nil, got #{source.class}" - end - end # TLS configuration as specified as part of client configuration # @@ -72,8 +53,8 @@ def initialize(disabled: nil, server_name: nil, server_root_ca_cert: nil, client # @return [Hash] Dictionary representation def to_h { - disabled: disabled, - server_name: server_name, + disabled:, + server_name:, server_ca_cert: server_root_ca_cert ? source_to_hash(server_root_ca_cert) : nil, client_cert: client_cert ? source_to_hash(client_cert) : nil, client_key: client_private_key ? source_to_hash(client_private_key) : nil @@ -336,5 +317,21 @@ def to_h profiles.transform_values(&:to_h) end end + + # @param source [Pathname, String, nil] Configuration source + # @return [Array] Tuple of [path, data] + # @!visibility private + def self._source_to_path_and_data(source) + case source + when Pathname + [source.to_s, nil] + when String + [nil, source] + when nil + [nil, nil] + else + raise TypeError, "Must be Pathname, String, or nil, got #{source.class}" + end + end end end From cde7c6fe6dac2f55bdd1239024ae8e14f6594810 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 15:26:23 -0700 Subject: [PATCH 27/31] Update core submodule --- temporalio/ext/sdk-core | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temporalio/ext/sdk-core b/temporalio/ext/sdk-core index eb74c70c..1807534c 160000 --- a/temporalio/ext/sdk-core +++ b/temporalio/ext/sdk-core @@ -1 +1 @@ -Subproject commit eb74c70c2f4b6d3f56f364b06d6bbc09d01be809 +Subproject commit 1807534c63c64a87716230467f47c3fd36fc9727 From 89e03de36c2dc1b8596cac0127ef475354a09db6 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 15:28:39 -0700 Subject: [PATCH 28/31] Revert irrelevant cancellation changes --- temporalio/lib/temporalio/cancellation.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/temporalio/lib/temporalio/cancellation.rb b/temporalio/lib/temporalio/cancellation.rb index f1e12a46..9b33b204 100644 --- a/temporalio/lib/temporalio/cancellation.rb +++ b/temporalio/lib/temporalio/cancellation.rb @@ -167,8 +167,8 @@ def prepare_cancel(reason:) to_return.values end - def canceled_mutex_synchronize(&block) - Workflow::Unsafe.illegal_call_tracing_disabled { @canceled_mutex.synchronize(&block) } + def canceled_mutex_synchronize(&) + Workflow::Unsafe.illegal_call_tracing_disabled { @canceled_mutex.synchronize(&) } end end end From aeaf17a4e76d9b247acf26c2d2881cd16eb00904 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 16:05:40 -0700 Subject: [PATCH 29/31] Update Cargo.lock --- temporalio/Cargo.lock | 402 +++++++++++++++++------------------------- 1 file changed, 162 insertions(+), 240 deletions(-) diff --git a/temporalio/Cargo.lock b/temporalio/Cargo.lock index 1fc3849c..1ef1433e 100644 --- a/temporalio/Cargo.lock +++ b/temporalio/Cargo.lock @@ -356,21 +356,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bzip2" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", + "libbz2-rs-sys", ] [[package]] @@ -585,21 +575,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.5.0" @@ -611,9 +586,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" +checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" dependencies = [ "anes", "cast", @@ -635,12 +610,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" dependencies = [ "cast", - "itertools 0.10.5", + "itertools 0.13.0", ] [[package]] @@ -842,23 +817,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] @@ -979,6 +954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1176,9 +1152,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "governor" -version = "0.8.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" dependencies = [ "cfg-if", "dashmap", @@ -1186,7 +1162,7 @@ dependencies = [ "futures-timer", "futures-util", "getrandom 0.3.3", - "no-std-compat", + "hashbrown 0.15.5", "nonzero_ext", "parking_lot", "portable-atomic", @@ -1599,15 +1575,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -1673,6 +1640,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.175" @@ -1689,6 +1662,26 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "liblzma" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bf66f4598dc77ff96677c8e763655494f00ff9c1cf79e2eb5bb07bc31f807d" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libredox" version = "0.1.9" @@ -1700,6 +1693,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1730,9 +1732,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" -version = "0.13.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" dependencies = [ "hashbrown 0.15.5", ] @@ -1743,27 +1745,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "magnus" version = "0.7.1" @@ -1878,12 +1859,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -1933,6 +1908,25 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.36.7" @@ -2202,6 +2196,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2612,13 +2612,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 1.0.69", + "thiserror 2.0.16", ] [[package]] @@ -2725,21 +2725,20 @@ dependencies = [ [[package]] name = "rstest" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" dependencies = [ "futures-timer", "futures-util", "rstest_macros", - "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", @@ -2968,15 +2967,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.0" @@ -3152,14 +3142,15 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.33.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", + "objc2-core-foundation", + "objc2-io-kit", "windows", ] @@ -3256,6 +3247,7 @@ dependencies = [ "assert_matches", "async-trait", "bimap", + "bytes", "clap", "console-subscriber", "criterion", @@ -3292,6 +3284,7 @@ dependencies = [ "ringbuf", "rstest", "rustfsm", + "semver", "serde", "serde_json", "siphasher", @@ -3302,7 +3295,6 @@ dependencies = [ "temporal-sdk", "temporal-sdk-core-api", "temporal-sdk-core-protos", - "temporal-sdk-core-test-utils", "thiserror 2.0.16", "tokio", "tokio-stream", @@ -3330,7 +3322,7 @@ dependencies = [ "tempfile", "temporal-sdk-core-protos", "thiserror 2.0.16", - "toml 0.8.23", + "toml", "tonic 0.13.1", "tracing", "tracing-core", @@ -3358,32 +3350,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "temporal-sdk-core-test-utils" -version = "0.1.0" -dependencies = [ - "anyhow", - "assert_matches", - "async-trait", - "bytes", - "futures-util", - "http-body-util", - "hyper", - "hyper-util", - "parking_lot", - "prost", - "rand 0.9.2", - "semver", - "temporal-client", - "temporal-sdk", - "temporal-sdk-core", - "temporal-sdk-core-api", - "temporal-sdk-core-protos", - "tokio", - "tracing", - "url", -] - [[package]] name = "temporalio_bridge" version = "0.1.0" @@ -3589,18 +3555,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - [[package]] name = "toml" version = "0.9.5" @@ -3609,7 +3563,7 @@ checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "indexmap 2.10.0", "serde", - "serde_spanned 1.0.0", + "serde_spanned", "toml_datetime 0.7.0", "toml_parser", "toml_writer", @@ -3621,9 +3575,6 @@ name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] [[package]] name = "toml_datetime" @@ -3641,10 +3592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.10.0", - "serde", - "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_write", "winnow", ] @@ -3657,12 +3605,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "toml_writer" version = "1.0.2" @@ -3881,7 +3823,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 0.9.5", + "toml", ] [[package]] @@ -4155,31 +4097,55 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.57.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ "windows-core", - "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.57.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", - "windows-targets 0.52.6", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.57.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", @@ -4188,9 +4154,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.57.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", @@ -4203,22 +4169,32 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" -version = "0.1.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-strings" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-targets 0.48.5", + "windows-link", ] [[package]] @@ -4248,21 +4224,6 @@ dependencies = [ "windows-targets 0.53.3", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4297,10 +4258,13 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-threading" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] [[package]] name = "windows_aarch64_gnullvm" @@ -4314,12 +4278,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4332,12 +4290,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4362,12 +4314,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -4380,12 +4326,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4398,12 +4338,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -4416,12 +4350,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4468,15 +4396,6 @@ dependencies = [ "rustix", ] -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "yoke" version = "0.8.0" @@ -4597,34 +4516,37 @@ dependencies = [ [[package]] name = "zip" -version = "2.4.2" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "aes", "arbitrary", "bzip2", "constant_time_eq", "crc32fast", - "crossbeam-utils", "deflate64", - "displaydoc", "flate2", "getrandom 0.3.3", "hmac", "indexmap 2.10.0", - "lzma-rs", + "liblzma", "memchr", "pbkdf2", + "ppmd-rust", "sha1", - "thiserror 2.0.16", "time", - "xz2", "zeroize", "zopfli", "zstd", ] +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + [[package]] name = "zopfli" version = "0.8.2" From 6db2dcc9d3bdbb10468a0b192523da53f03fd97c Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Tue, 9 Sep 2025 17:23:41 -0700 Subject: [PATCH 30/31] Propagate set_headers failure in client bridge --- temporalio/ext/src/client.rs | 7 +++++-- temporalio/lib/temporalio/cancellation.rb | 4 ++-- .../worker/workflow_instance/outbound_implementation.rb | 8 ++++---- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/temporalio/ext/src/client.rs b/temporalio/ext/src/client.rs index 7068bab6..5e032f8d 100644 --- a/temporalio/ext/src/client.rs +++ b/temporalio/ext/src/client.rs @@ -237,8 +237,11 @@ impl Client { self.invoke_rpc(service, callback, call) } - pub fn update_metadata(&self, headers: HashMap) { - self.core.get_client().set_headers(headers); + pub fn update_metadata(&self, headers: HashMap) -> Result<(), Error> { + self.core + .get_client() + .set_headers(headers) + .map_err(|err| error!("Invalid headers: {}", err)) } pub fn update_api_key(&self, api_key: Option) { diff --git a/temporalio/lib/temporalio/cancellation.rb b/temporalio/lib/temporalio/cancellation.rb index 9b33b204..f1e12a46 100644 --- a/temporalio/lib/temporalio/cancellation.rb +++ b/temporalio/lib/temporalio/cancellation.rb @@ -167,8 +167,8 @@ def prepare_cancel(reason:) to_return.values end - def canceled_mutex_synchronize(&) - Workflow::Unsafe.illegal_call_tracing_disabled { @canceled_mutex.synchronize(&) } + def canceled_mutex_synchronize(&block) + Workflow::Unsafe.illegal_call_tracing_disabled { @canceled_mutex.synchronize(&block) } end end end diff --git a/temporalio/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb b/temporalio/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb index a1bf206e..1a23daa9 100644 --- a/temporalio/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +++ b/temporalio/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb @@ -121,7 +121,7 @@ def execute_local_activity(input) end end - def execute_activity_with_local_backoffs(local:, cancellation:, result_hint:, &) + def execute_activity_with_local_backoffs(local:, cancellation:, result_hint:, &block) # We do not even want to schedule if the cancellation is already cancelled. We choose to use canceled # failure instead of wrapping in activity failure which is similar to what other SDKs do, with the accepted # tradeoff that it makes rescue more difficult (hence the presence of Error.canceled? helper). @@ -130,7 +130,7 @@ def execute_activity_with_local_backoffs(local:, cancellation:, result_hint:, &) # This has to be done in a loop for local activity backoff last_local_backoff = nil loop do - result = execute_activity_once(local:, cancellation:, last_local_backoff:, result_hint:, &) + result = execute_activity_once(local:, cancellation:, last_local_backoff:, result_hint:, &block) return result unless result.is_a?(Bridge::Api::ActivityResult::DoBackoff) # @type var result: untyped @@ -142,9 +142,9 @@ def execute_activity_with_local_backoffs(local:, cancellation:, result_hint:, &) end # If this doesn't raise, it returns success | DoBackoff - def execute_activity_once(local:, cancellation:, last_local_backoff:, result_hint:, &) + def execute_activity_once(local:, cancellation:, last_local_backoff:, result_hint:, &block) # Add to pending activities (removed by the resolver) - seq = yield last_local_backoff + seq = block.call(last_local_backoff) @instance.pending_activities[seq] = Fiber.current # Add cancellation hook From edfed0857d97ad7fa64b771deed254a988f8f884 Mon Sep 17 00:00:00 2001 From: Thomas Hardy Date: Wed, 10 Sep 2025 11:55:44 -0700 Subject: [PATCH 31/31] Add experimental warning --- temporalio/lib/temporalio/env_config.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/temporalio/lib/temporalio/env_config.rb b/temporalio/lib/temporalio/env_config.rb index 7b537bc2..6ee88393 100644 --- a/temporalio/lib/temporalio/env_config.rb +++ b/temporalio/lib/temporalio/env_config.rb @@ -5,6 +5,8 @@ module Temporalio # Environment and file-based configuration for Temporal clients + # + # WARNING: Experimental API. module EnvConfig # This module provides utilities to load Temporal client configuration from TOML files # and environment variables.