Skip to content

Commit f2e58c8

Browse files
committed
Keep alive pool size configurable
1 parent 3b7cb83 commit f2e58c8

4 files changed

Lines changed: 62 additions & 4 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ Notes:
4848
- Connections are cached per `(thread, node)`. `Net::HTTP` is not thread-safe, so each thread maintains its own keep-alive socket to each Typesense node, and the existing node round-robin still works.
4949
- A cached connection is dropped automatically when a network error occurs, so retries open a fresh socket. We recommend setting `num_retries` to at least `1` so the gem can recover from a server- or load-balancer-side idle timeout transparently.
5050
- Idle sockets are closed after 30 seconds by default. Override with `keep_alive_idle_timeout_seconds` to match or stay under your load balancer's idle timeout.
51+
- The underlying `net_http_persistent` adapter holds at most `keep_alive_pool_size` sockets per origin (default `1`, which matches the per-`(thread, node)` cache above). The default of `1` is the safe choice for the vast majority of users — because we already cache one Faraday connection per `(thread, node)` and `Net::HTTP` is not thread-safe, a single socket per pool is all the adapter needs. Only raise this if you have a specific reason to keep additional sockets warm per origin (e.g. a non-standard concurrency model layered on top of this client); a larger pool will not increase request throughput on its own.
52+
- `keep_alive_idle_timeout_seconds` and `keep_alive_pool_size` are only valid when `keep_alive_connections: true`; setting them otherwise raises `Typesense::Error::MissingConfiguration`.
5153
- The option defaults to `false`, so upgrading the gem does not change behaviour until you opt in.
5254

5355
## Compatibility

lib/typesense/api_call.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ def initialize(configuration)
2121
@retry_interval_seconds = @configuration.retry_interval_seconds
2222
@keep_alive_connections = @configuration.keep_alive_connections
2323
@keep_alive_idle_timeout_seconds = @configuration.keep_alive_idle_timeout_seconds
24+
@keep_alive_pool_size = @configuration.keep_alive_pool_size
2425

2526
@logger = @configuration.logger
2627

@@ -175,7 +176,10 @@ def build_keep_alive_connection(node)
175176
Faraday.new(url: connection_key(node)) do |f|
176177
f.options.timeout = @connection_timeout_seconds
177178
f.options.open_timeout = @connection_timeout_seconds
178-
f.adapter :net_http_persistent, pool_size: 1 do |http|
179+
# pool_size defaults to 1: we already cache one Faraday connection per
180+
# (thread, node), and Net::HTTP is not thread-safe — so a single socket
181+
# per pool is the safe default. Override only with a specific reason.
182+
f.adapter :net_http_persistent, pool_size: @keep_alive_pool_size do |http|
179183
http.idle_timeout = @keep_alive_idle_timeout_seconds
180184
end
181185
end

lib/typesense/configuration.rb

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
module Typesense
66
class Configuration
7-
attr_accessor :nodes, :nearest_node, :connection_timeout_seconds, :healthcheck_interval_seconds, :num_retries, :retry_interval_seconds, :api_key, :logger, :log_level, :keep_alive_connections, :keep_alive_idle_timeout_seconds
7+
attr_accessor :nodes, :nearest_node, :connection_timeout_seconds, :healthcheck_interval_seconds, :num_retries, :retry_interval_seconds, :api_key, :logger, :log_level, :keep_alive_connections, :keep_alive_idle_timeout_seconds, :keep_alive_pool_size
88

99
def initialize(options = {})
1010
@nodes = options[:nodes] || []
@@ -16,23 +16,26 @@ def initialize(options = {})
1616
@api_key = options[:api_key]
1717
@keep_alive_connections = options.fetch(:keep_alive_connections, false)
1818
@keep_alive_idle_timeout_seconds = options[:keep_alive_idle_timeout_seconds] || 30
19+
@keep_alive_pool_size = options[:keep_alive_pool_size] || 1
1920

2021
@logger = options[:logger] || Logger.new($stdout)
2122
@log_level = options[:log_level] || Logger::WARN
2223
@logger.level = @log_level
2324

2425
show_deprecation_warnings(options)
25-
validate!
26+
validate!(options)
2627
end
2728

28-
def validate!
29+
def validate!(options = {})
2930
if @nodes.nil? ||
3031
@nodes.empty? ||
3132
@nodes.any? { |node| node_missing_parameters?(node) }
3233
raise Error::MissingConfiguration, 'Missing required configuration. Ensure that nodes[][:protocol], nodes[][:host] and nodes[][:port] are set.'
3334
end
3435

3536
raise Error::MissingConfiguration, 'Missing required configuration. Ensure that api_key is set.' if @api_key.nil?
37+
38+
validate_keep_alive_options!(options)
3639
end
3740

3841
private
@@ -41,6 +44,15 @@ def node_missing_parameters?(node)
4144
%i[protocol host port].any? { |attr| node.send(:[], attr).nil? }
4245
end
4346

47+
def validate_keep_alive_options!(options)
48+
return if @keep_alive_connections
49+
50+
dependent_keys = %i[keep_alive_idle_timeout_seconds keep_alive_pool_size].select { |k| options.key?(k) }
51+
return if dependent_keys.empty?
52+
53+
raise Error::MissingConfiguration, "#{dependent_keys.join(' and ')} require keep_alive_connections: true."
54+
end
55+
4456
def show_deprecation_warnings(options)
4557
@logger.warn 'Deprecation warning: timeout_seconds is now renamed to connection_timeout_seconds' unless options[:timeout_seconds].nil?
4658
@logger.warn 'Deprecation warning: master_node is now consolidated to nodes, starting with Typesense Server v0.12' unless options[:master_node].nil?

spec/typesense/api_call_spec.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,24 @@
356356
expect(custom_client.configuration.keep_alive_idle_timeout_seconds).to eq(5)
357357
expect(described_class.new(custom_client.configuration).instance_variable_get(:@keep_alive_idle_timeout_seconds)).to eq(5)
358358
end
359+
360+
it 'defaults the pool size to 1' do
361+
expect(keep_alive_typesense.configuration.keep_alive_pool_size).to eq(1)
362+
end
363+
364+
it 'honours a custom keep_alive_pool_size' do
365+
custom_client = Typesense::Client.new(
366+
api_key: 'abcd',
367+
nodes: typesense.configuration.nodes,
368+
connection_timeout_seconds: 10,
369+
log_level: Logger::ERROR,
370+
keep_alive_connections: true,
371+
keep_alive_pool_size: 5
372+
)
373+
374+
expect(custom_client.configuration.keep_alive_pool_size).to eq(5)
375+
expect(described_class.new(custom_client.configuration).instance_variable_get(:@keep_alive_pool_size)).to eq(5)
376+
end
359377
end
360378

361379
describe 'keep-alive disabled (default)' do
@@ -371,5 +389,27 @@
371389

372390
expect(Thread.current[api_call.instance_variable_get(:@thread_connections_key)]).to be_nil
373391
end
392+
393+
it 'raises when keep_alive_idle_timeout_seconds is set without keep_alive_connections' do
394+
expect do
395+
Typesense::Client.new(
396+
api_key: 'abcd',
397+
nodes: typesense.configuration.nodes,
398+
log_level: Logger::ERROR,
399+
keep_alive_idle_timeout_seconds: 5
400+
)
401+
end.to raise_error(Typesense::Error::MissingConfiguration, /keep_alive_connections: true/)
402+
end
403+
404+
it 'raises when keep_alive_pool_size is set without keep_alive_connections' do
405+
expect do
406+
Typesense::Client.new(
407+
api_key: 'abcd',
408+
nodes: typesense.configuration.nodes,
409+
log_level: Logger::ERROR,
410+
keep_alive_pool_size: 4
411+
)
412+
end.to raise_error(Typesense::Error::MissingConfiguration, /keep_alive_connections: true/)
413+
end
374414
end
375415
end

0 commit comments

Comments
 (0)