From 3d97e69f33a390910eb97425f52e63e7461dbaa3 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 13 May 2026 16:51:42 -0400 Subject: [PATCH 1/8] Plumb DNS load balancing options through, defaulting to nil. --- temporalio/ext/src/client.rs | 15 +++++++++- temporalio/lib/temporalio/client.rb | 7 ++++- .../lib/temporalio/client/connection.rb | 30 +++++++++++++++++-- .../lib/temporalio/internal/bridge/client.rb | 7 ++++- temporalio/sig/temporalio/client.rbs | 3 +- .../sig/temporalio/client/connection.rbs | 13 +++++++- .../sig/temporalio/internal/bridge/client.rbs | 16 ++++++++-- temporalio/test/client_test.rb | 23 ++++++++++++++ 8 files changed, 104 insertions(+), 10 deletions(-) diff --git a/temporalio/ext/src/client.rs b/temporalio/ext/src/client.rs index 82036321..9fe952c4 100644 --- a/temporalio/ext/src/client.rs +++ b/temporalio/ext/src/client.rs @@ -2,7 +2,8 @@ use std::{collections::HashMap, future::Future, marker::PhantomData, time::Durat use temporalio_client::{ ClientKeepAliveOptions, ClientTlsOptions, Connection, ConnectionOptions, - HttpConnectProxyOptions, RetryOptions, TlsOptions, errors::ClientConnectError, + DnsLoadBalancingOptions, HttpConnectProxyOptions, RetryOptions, TlsOptions, + errors::ClientConnectError, }; use magnus::{ @@ -181,6 +182,18 @@ impl Client { None }, ) + .dns_load_balancing( + if options.child(id!("http_connect_proxy"))?.is_some() { + None + } else if let Some(dns) = options.child(id!("dns_load_balancing"))? { + let mut opts = DnsLoadBalancingOptions::default(); + opts.resolution_interval = + Duration::from_secs_f64(dns.member(id!("resolution_interval"))?); + Some(opts) + } else { + None + }, + ) .maybe_metrics_meter(metrics_meter) .build(); diff --git a/temporalio/lib/temporalio/client.rb b/temporalio/lib/temporalio/client.rb index 5c8f148e..8ac8156d 100644 --- a/temporalio/lib/temporalio/client.rb +++ b/temporalio/lib/temporalio/client.rb @@ -95,6 +95,9 @@ class ListWorkflowPage; end # rubocop:disable Lint/EmptyClass # @param runtime [Runtime] Runtime for this client. # @param lazy_connect [Boolean] If true, the client will not connect until the first call is attempted or a worker # is created with it. Lazy clients cannot be used for workers if they have not performed a connection. + # @param dns_load_balancing [Connection::DnsLoadBalancingOptions, nil] DNS load balancing options for the connection. + # Default is +nil+ (disabled). Silently disabled when +http_connect_proxy+ is set, since the two are mutually + # exclusive. # # @return [Client] Connected client. # @@ -116,7 +119,8 @@ def self.connect( keep_alive: Connection::KeepAliveOptions.new, # Set to nil to disable http_connect_proxy: nil, runtime: Runtime.default, - lazy_connect: false + lazy_connect: false, + dns_load_balancing: nil ) # Prepare connection. The connection var is needed here so it can be used in callback for plugin. base_connection = nil @@ -162,6 +166,7 @@ def self.connect( http_connect_proxy:, runtime:, lazy_connect:, + dns_load_balancing:, around_connect: # steep:ignore ) diff --git a/temporalio/lib/temporalio/client/connection.rb b/temporalio/lib/temporalio/client/connection.rb index b37a871c..915c9b5d 100644 --- a/temporalio/lib/temporalio/client/connection.rb +++ b/temporalio/lib/temporalio/client/connection.rb @@ -24,7 +24,8 @@ class Connection :keep_alive, :http_connect_proxy, :runtime, - :lazy_connect + :lazy_connect, + :dns_load_balancing ) # Options as returned from {options} for +**to_h+ splat use in {initialize}. See {initialize} for details. @@ -132,6 +133,22 @@ def initialize(interval: 30.0, timeout: 15.0) # @return [String, nil] Pass for HTTP basic auth for the proxy, must be combined with {basic_auth_user}. class HTTPConnectProxyOptions; end # rubocop:disable Lint/EmptyClass + DnsLoadBalancingOptions = Data.define( + :resolution_interval + ) + + # DNS load balancing options for client connections. When set, Core periodically re-resolves the target host's + # DNS records and round-robins requests across the resolved addresses. Mutually exclusive with + # {HTTPConnectProxyOptions} -- DNS load balancing is silently disabled when an HTTP CONNECT proxy is configured. + # + # @!attribute resolution_interval + # @return [Float] How often to re-resolve DNS, in seconds. Default 30.0. + class DnsLoadBalancingOptions + def initialize(resolution_interval: 30.0) + super + end + end + # @return [Options] Frozen options for this client which has the same attributes as {initialize}. Note that if # {api_key=} or {rpc_metadata=} are updated, the options object is replaced with those changes (it is not # mutated in place). @@ -169,6 +186,8 @@ class HTTPConnectProxyOptions; end # rubocop:disable Lint/EmptyClass # @param lazy_connect [Boolean] If true, there is no connection until the first call is attempted or a worker # is created with it. Clients from lazy connections cannot be used for workers if they have not performed a # connection. + # @param dns_load_balancing [DnsLoadBalancingOptions, nil] DNS load balancing options for this connection. Default + # is +nil+ (disabled). Silently disabled when +http_connect_proxy+ is set, since the two are mutually exclusive. # @param around_connect [Proc, nil] If present, this proc accepts two values: options and a block. The block must # be yielded to only once with the options. The block does not return a meaningful value, nor should # around_connect. @@ -185,6 +204,7 @@ def initialize( http_connect_proxy: nil, runtime: Runtime.default, lazy_connect: false, + dns_load_balancing: nil, around_connect: nil ) @options = Options.new( @@ -197,7 +217,8 @@ def initialize( keep_alive:, http_connect_proxy:, runtime:, - lazy_connect: + lazy_connect:, + dns_load_balancing: ).freeze @core_client_mutex = Mutex.new # Create core client now if not lazy, applying around_connect if present @@ -329,6 +350,11 @@ def new_core_client basic_auth_pass: @options.http_connect_proxy.basic_auth_pass ) end + if (dns_load_balancing = @options.dns_load_balancing) + options.dns_load_balancing = Internal::Bridge::Client::DnsLoadBalancingOptions.new( + resolution_interval: dns_load_balancing.resolution_interval + ) + end Internal::Bridge::Client.new(@options.runtime._core_runtime, options) end end diff --git a/temporalio/lib/temporalio/internal/bridge/client.rb b/temporalio/lib/temporalio/internal/bridge/client.rb index a6483b00..01bdfc0e 100644 --- a/temporalio/lib/temporalio/internal/bridge/client.rb +++ b/temporalio/lib/temporalio/internal/bridge/client.rb @@ -16,7 +16,8 @@ class Client :tls, # Optional :rpc_retry, :keep_alive, # Optional - :http_connect_proxy # Optional + :http_connect_proxy, # Optional + :dns_load_balancing # Optional ) TLSOptions = Struct.new( @@ -46,6 +47,10 @@ class Client :basic_auth_pass # Optional ) + DnsLoadBalancingOptions = Struct.new( + :resolution_interval + ) + def self.new(runtime, options) queue = Queue.new async_new(runtime, options, queue) diff --git a/temporalio/sig/temporalio/client.rbs b/temporalio/sig/temporalio/client.rbs index fd7e6761..66c4b73e 100644 --- a/temporalio/sig/temporalio/client.rbs +++ b/temporalio/sig/temporalio/client.rbs @@ -50,7 +50,8 @@ module Temporalio ?keep_alive: Connection::KeepAliveOptions, ?http_connect_proxy: Connection::HTTPConnectProxyOptions?, ?runtime: Runtime, - ?lazy_connect: bool + ?lazy_connect: bool, + ?dns_load_balancing: Connection::DnsLoadBalancingOptions? ) -> Client def self._validate_plugins!: (Array[Plugin] plugins) -> void diff --git a/temporalio/sig/temporalio/client/connection.rbs b/temporalio/sig/temporalio/client/connection.rbs index 9f53b749..083b8c1f 100644 --- a/temporalio/sig/temporalio/client/connection.rbs +++ b/temporalio/sig/temporalio/client/connection.rbs @@ -12,6 +12,7 @@ module Temporalio attr_reader http_connect_proxy: HTTPConnectProxyOptions attr_reader runtime: Runtime attr_reader lazy_connect: bool + attr_reader dns_load_balancing: DnsLoadBalancingOptions? def initialize: ( target_host: String, @@ -23,7 +24,8 @@ module Temporalio keep_alive: KeepAliveOptions, http_connect_proxy: HTTPConnectProxyOptions?, runtime: Runtime, - lazy_connect: bool + lazy_connect: bool, + dns_load_balancing: DnsLoadBalancingOptions? ) -> void def to_h: -> Hash[Symbol, untyped] @@ -85,6 +87,14 @@ module Temporalio ) -> void end + class DnsLoadBalancingOptions + attr_reader resolution_interval: Float + + def initialize: ( + ?resolution_interval: Float + ) -> void + end + attr_reader options: Options # TODO(cretz): Update when generated @@ -106,6 +116,7 @@ module Temporalio ?http_connect_proxy: HTTPConnectProxyOptions?, ?runtime: Runtime, ?lazy_connect: bool, + ?dns_load_balancing: DnsLoadBalancingOptions?, ?around_connect: nil | ^(Options) { (Options) -> void } -> void ) -> void diff --git a/temporalio/sig/temporalio/internal/bridge/client.rbs b/temporalio/sig/temporalio/internal/bridge/client.rbs index c4b81125..7bf929d2 100644 --- a/temporalio/sig/temporalio/internal/bridge/client.rbs +++ b/temporalio/sig/temporalio/internal/bridge/client.rbs @@ -13,7 +13,8 @@ module Temporalio attr_accessor rpc_retry: RPCRetryOptions attr_accessor keep_alive: KeepAliveOptions? attr_accessor http_connect_proxy: HTTPConnectProxyOptions? - + attr_accessor dns_load_balancing: DnsLoadBalancingOptions? + def initialize: ( target_host: String, client_name: String, @@ -24,7 +25,8 @@ module Temporalio ?tls: TLSOptions?, rpc_retry: RPCRetryOptions, ?keep_alive: KeepAliveOptions?, - ?http_connect_proxy: HTTPConnectProxyOptions? + ?http_connect_proxy: HTTPConnectProxyOptions?, + ?dns_load_balancing: DnsLoadBalancingOptions? ) -> void end @@ -74,7 +76,7 @@ module Temporalio attr_accessor target_host: String attr_accessor basic_auth_user: String? attr_accessor basic_auth_pass: String? - + def initialize: ( target_host: String, basic_auth_user: String?, @@ -82,6 +84,14 @@ module Temporalio ) -> void end + class DnsLoadBalancingOptions + attr_accessor resolution_interval: Float + + def initialize: ( + resolution_interval: Float + ) -> void + end + # Defined in Rust SERVICE_WORKFLOW: Integer diff --git a/temporalio/test/client_test.rb b/temporalio/test/client_test.rb index c4774126..291f354a 100644 --- a/temporalio/test/client_test.rb +++ b/temporalio/test/client_test.rb @@ -27,6 +27,29 @@ def test_lazy_connection assert client.connection.connected? end + def test_dns_load_balancing_default_nil + client = Temporalio::Client.connect( + env.client.connection.target_host, env.client.namespace, lazy_connect: true + ) + assert_nil client.connection.options.dns_load_balancing + end + + def test_dns_load_balancing_custom_preserved + dns_opts = Temporalio::Client::Connection::DnsLoadBalancingOptions.new(resolution_interval: 5.0) + client = Temporalio::Client.connect( + env.client.connection.target_host, env.client.namespace, + lazy_connect: true, dns_load_balancing: dns_opts + ) + stored = client.connection.options.dns_load_balancing + assert_equal dns_opts, stored + assert_in_delta 5.0, stored.resolution_interval # steep:ignore + end + + def test_dns_load_balancing_default_interval + opts = Temporalio::Client::Connection::DnsLoadBalancingOptions.new + assert_in_delta 30.0, opts.resolution_interval + end + class TrackCallsInterceptor include Temporalio::Client::Interceptor From 15de18e71400b23bcd15d40797769965dee93eba Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 13 May 2026 17:16:41 -0400 Subject: [PATCH 2/8] Use ignored localhost address to avoid starting a test server. --- temporalio/test/client_test.rb | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/temporalio/test/client_test.rb b/temporalio/test/client_test.rb index 291f354a..741f4568 100644 --- a/temporalio/test/client_test.rb +++ b/temporalio/test/client_test.rb @@ -28,17 +28,14 @@ def test_lazy_connection end def test_dns_load_balancing_default_nil - client = Temporalio::Client.connect( - env.client.connection.target_host, env.client.namespace, lazy_connect: true - ) + client = Temporalio::Client.connect('localhost:7233', 'default', lazy_connect: true) assert_nil client.connection.options.dns_load_balancing end def test_dns_load_balancing_custom_preserved dns_opts = Temporalio::Client::Connection::DnsLoadBalancingOptions.new(resolution_interval: 5.0) client = Temporalio::Client.connect( - env.client.connection.target_host, env.client.namespace, - lazy_connect: true, dns_load_balancing: dns_opts + 'localhost:7233', 'default', lazy_connect: true, dns_load_balancing: dns_opts ) stored = client.connection.options.dns_load_balancing assert_equal dns_opts, stored From 1e787f8320d4c391e9582868b1ac71ebc04cfdd3 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Wed, 13 May 2026 18:01:03 -0400 Subject: [PATCH 3/8] fmt --- temporalio/lib/temporalio/client.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/temporalio/lib/temporalio/client.rb b/temporalio/lib/temporalio/client.rb index 8ac8156d..8e2d42d8 100644 --- a/temporalio/lib/temporalio/client.rb +++ b/temporalio/lib/temporalio/client.rb @@ -95,9 +95,9 @@ class ListWorkflowPage; end # rubocop:disable Lint/EmptyClass # @param runtime [Runtime] Runtime for this client. # @param lazy_connect [Boolean] If true, the client will not connect until the first call is attempted or a worker # is created with it. Lazy clients cannot be used for workers if they have not performed a connection. - # @param dns_load_balancing [Connection::DnsLoadBalancingOptions, nil] DNS load balancing options for the connection. - # Default is +nil+ (disabled). Silently disabled when +http_connect_proxy+ is set, since the two are mutually - # exclusive. + # @param dns_load_balancing [Connection::DnsLoadBalancingOptions, nil] DNS load balancing options for the + # connection. Default is +nil+ (disabled). Silently disabled when +http_connect_proxy+ is set, since the two are + # mutually exclusive. # # @return [Client] Connected client. # From 766aa908e4e0b3c319ea5fd23f1b22905afb914a Mon Sep 17 00:00:00 2001 From: Gregory Michael Travis Date: Thu, 14 May 2026 10:36:52 -0400 Subject: [PATCH 4/8] Update temporalio/test/client_test.rb Co-authored-by: Chris Olszewski --- temporalio/test/client_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/temporalio/test/client_test.rb b/temporalio/test/client_test.rb index 741f4568..4c4eb35d 100644 --- a/temporalio/test/client_test.rb +++ b/temporalio/test/client_test.rb @@ -39,6 +39,7 @@ def test_dns_load_balancing_custom_preserved ) stored = client.connection.options.dns_load_balancing assert_equal dns_opts, stored + raise 'stored is nil' if stored.nil? assert_in_delta 5.0, stored.resolution_interval # steep:ignore end From 8bd3aae4d275f414cb17d767b1171d95a1a2d786 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 14 May 2026 10:40:50 -0400 Subject: [PATCH 5/8] Remove steep:ignore. --- temporalio/test/client_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/temporalio/test/client_test.rb b/temporalio/test/client_test.rb index 4c4eb35d..fd55cb1a 100644 --- a/temporalio/test/client_test.rb +++ b/temporalio/test/client_test.rb @@ -40,7 +40,7 @@ def test_dns_load_balancing_custom_preserved stored = client.connection.options.dns_load_balancing assert_equal dns_opts, stored raise 'stored is nil' if stored.nil? - assert_in_delta 5.0, stored.resolution_interval # steep:ignore + assert_in_delta 5.0, stored.resolution_interval end def test_dns_load_balancing_default_interval From 3dac2c2f8e4ed05946f6c5ea46664090a2d28013 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 14 May 2026 10:41:28 -0400 Subject: [PATCH 6/8] Warn if DNS load balancing is disabled because of proxying. --- temporalio/ext/src/client.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/temporalio/ext/src/client.rs b/temporalio/ext/src/client.rs index 9fe952c4..a97de43a 100644 --- a/temporalio/ext/src/client.rs +++ b/temporalio/ext/src/client.rs @@ -11,6 +11,7 @@ use magnus::{ scan_args, }; use tonic::{Status, metadata::MetadataKey}; +use tracing::warn; use url::Url; use super::{ROOT_MOD, error, id, new_error}; @@ -184,6 +185,7 @@ impl Client { ) .dns_load_balancing( if options.child(id!("http_connect_proxy"))?.is_some() { + warn!("Disabling DNS load balancing because http_connect_proxy is set"); None } else if let Some(dns) = options.child(id!("dns_load_balancing"))? { let mut opts = DnsLoadBalancingOptions::default(); From bdd9461a7e4cc71dcff4eab7e03465b5dc384604 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 14 May 2026 10:48:17 -0400 Subject: [PATCH 7/8] Rubocop wants an empty line for some reason. --- temporalio/test/client_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/temporalio/test/client_test.rb b/temporalio/test/client_test.rb index fd55cb1a..f1c7af4f 100644 --- a/temporalio/test/client_test.rb +++ b/temporalio/test/client_test.rb @@ -40,6 +40,7 @@ def test_dns_load_balancing_custom_preserved stored = client.connection.options.dns_load_balancing assert_equal dns_opts, stored raise 'stored is nil' if stored.nil? + assert_in_delta 5.0, stored.resolution_interval end From a96a81d34c9c6819d9d8bbb04b6a3d2c66a73ca4 Mon Sep 17 00:00:00 2001 From: Greg Travis Date: Thu, 14 May 2026 10:57:18 -0400 Subject: [PATCH 8/8] rust fmt --- temporalio/ext/src/client.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/temporalio/ext/src/client.rs b/temporalio/ext/src/client.rs index a97de43a..4957df00 100644 --- a/temporalio/ext/src/client.rs +++ b/temporalio/ext/src/client.rs @@ -183,19 +183,17 @@ impl Client { None }, ) - .dns_load_balancing( - if options.child(id!("http_connect_proxy"))?.is_some() { - warn!("Disabling DNS load balancing because http_connect_proxy is set"); - None - } else if let Some(dns) = options.child(id!("dns_load_balancing"))? { - let mut opts = DnsLoadBalancingOptions::default(); - opts.resolution_interval = - Duration::from_secs_f64(dns.member(id!("resolution_interval"))?); - Some(opts) - } else { - None - }, - ) + .dns_load_balancing(if options.child(id!("http_connect_proxy"))?.is_some() { + warn!("Disabling DNS load balancing because http_connect_proxy is set"); + None + } else if let Some(dns) = options.child(id!("dns_load_balancing"))? { + let mut opts = DnsLoadBalancingOptions::default(); + opts.resolution_interval = + Duration::from_secs_f64(dns.member(id!("resolution_interval"))?); + Some(opts) + } else { + None + }) .maybe_metrics_meter(metrics_meter) .build();