Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,31 @@ def fetch_schema(endpoint, if_none_match: nil)
def make_request(endpoint, caller: nil, method: :get, payload: nil, symbolize_keys: false, if_none_match: nil)
log_request_start(method, endpoint, if_none_match)

client = Faraday.new(url: @api_url) do |faraday|
client = build_faraday_client(symbolize_keys)
headers = build_request_headers(caller, if_none_match)

response = client.send(method, endpoint, payload, headers)
log_request_complete(response, endpoint)
response
rescue Faraday::ConnectionFailed => e
handle_connection_failed(endpoint, e)
rescue Faraday::TimeoutError => e
handle_timeout_error(endpoint, e)
end
# rubocop:enable Metrics/ParameterLists

def build_faraday_client(symbolize_keys)
Faraday.new(url: @api_url) do |faraday|
faraday.request :json
faraday.response :json, parser_options: { symbolize_names: symbolize_keys }
faraday.adapter Faraday.default_adapter
faraday.ssl.verify = !ForestAdminAgent::Facades::Container.cache(:debug)
faraday.options.timeout = DEFAULT_TIMEOUT
faraday.options.open_timeout = DEFAULT_OPEN_TIMEOUT
end
end

def build_request_headers(caller, if_none_match)
timestamp = Time.now.utc.iso8601(3)
signature = generate_signature(timestamp)

Expand All @@ -80,12 +96,26 @@ def make_request(endpoint, caller: nil, method: :get, payload: nil, symbolize_ke

headers['forest_caller'] = caller.to_json if caller
headers['If-None-Match'] = %("#{if_none_match}") if if_none_match
headers
end

response = client.send(method, endpoint, payload, headers)
log_request_complete(response, endpoint)
response
def handle_connection_failed(endpoint, error)
ForestAdminAgent::Facades::Container.logger&.log(
'Error',
"[RPC Client] Connection failed to #{@api_url}#{endpoint}: #{error.message}"
)
raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
"RPC connection failed: Unable to connect to #{@api_url}. Please check if the RPC server is running."
end

def handle_timeout_error(endpoint, error)
ForestAdminAgent::Facades::Container.logger&.log(
'Error',
"[RPC Client] Request timeout to #{@api_url}#{endpoint}: #{error.message}"
)
raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
"RPC request timeout: The RPC server at #{@api_url} did not respond in time."
end
# rubocop:enable Metrics/ParameterLists

def generate_signature(timestamp)
OpenSSL::HMAC.hexdigest('SHA256', @auth_secret, timestamp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ def check_schema
log_connection_error(e)
rescue ForestAdminAgent::Http::Exceptions::AuthenticationOpenIdClient => e
log_authentication_error(e)
rescue ForestAdminDatasourceToolkit::Exceptions::ForestException => e
log_rpc_error(e)
rescue StandardError => e
log_unexpected_error(e)
end
Expand Down Expand Up @@ -108,7 +110,8 @@ def fetch_initial_schema_sync

@introspection_schema = nil
rescue Faraday::ConnectionFailed, Faraday::TimeoutError,
ForestAdminAgent::Http::Exceptions::AuthenticationOpenIdClient, StandardError => e
ForestAdminAgent::Http::Exceptions::AuthenticationOpenIdClient,
ForestAdminDatasourceToolkit::Exceptions::ForestException, StandardError => e
handle_initial_fetch_error(e)
end

Expand Down Expand Up @@ -213,6 +216,13 @@ def log_connection_error(error)
)
end

def log_rpc_error(error)
ForestAdminAgent::Facades::Container.logger&.log(
'Warn',
"[Schema Polling] RPC error: #{error.message}"
)
end

def log_authentication_error(error)
ForestAdminAgent::Facades::Container.logger&.log(
'Error',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,82 @@ module Utils
end
end
end

context 'when connection fails' do
let(:faraday_connection) { instance_double(Faraday::Connection) }
let(:timestamp) { '2025-04-01T14:07:02Z' }

before do
allow(Faraday).to receive(:new).and_return(faraday_connection)
allow(Time).to receive(:now).and_return(instance_double(Time, utc: instance_double(Time, iso8601: timestamp)))
end

context 'with Faraday::ConnectionFailed' do
before do
allow(faraday_connection).to receive(:send).and_raise(
Faraday::ConnectionFailed.new('Failed to open TCP connection to localhost:3039')
)
end

it 'logs the connection error' do
expect { rpc_client.call_rpc('/rpc/test', method: :get) }.to raise_error(
ForestAdminDatasourceToolkit::Exceptions::ForestException
)

expect(logger).to have_received(:log).with(
'Error',
%r{\[RPC Client\] Connection failed to http://localhost/rpc/test: Failed to open TCP connection}
)
end

it 'raises ForestException with user-friendly message' do
expect { rpc_client.call_rpc('/rpc/test', method: :get) }.to raise_error(
ForestAdminDatasourceToolkit::Exceptions::ForestException,
%r{RPC connection failed: Unable to connect to http://localhost.*Please check if the RPC server is running}
)
end

it 'handles connection failure in fetch_schema' do
expect { rpc_client.fetch_schema('/rpc/schema') }.to raise_error(
ForestAdminDatasourceToolkit::Exceptions::ForestException,
/RPC connection failed/
)
end
end

context 'with Faraday::TimeoutError' do
before do
allow(faraday_connection).to receive(:send).and_raise(
Faraday::TimeoutError.new('Net::ReadTimeout')
)
end

it 'logs the timeout error' do
expect { rpc_client.call_rpc('/rpc/test', method: :get) }.to raise_error(
ForestAdminDatasourceToolkit::Exceptions::ForestException
)

expect(logger).to have_received(:log).with(
'Error',
%r{\[RPC Client\] Request timeout to http://localhost/rpc/test}
)
end

it 'raises ForestException with user-friendly message' do
expect { rpc_client.call_rpc('/rpc/test', method: :get) }.to raise_error(
ForestAdminDatasourceToolkit::Exceptions::ForestException,
%r{RPC request timeout: The RPC server at http://localhost did not respond in time}
)
end

it 'handles timeout in fetch_schema' do
expect { rpc_client.fetch_schema('/rpc/schema') }.to raise_error(
ForestAdminDatasourceToolkit::Exceptions::ForestException,
/RPC request timeout/
)
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,19 @@ module Utils
expect(logger).to have_received(:log).with('Error', /Authentication error/)
end

it 'handles RPC errors (ForestException) gracefully' do
client = described_class.new(uri, secret) { |schema| callback.call(schema) }
allow(rpc_client).to receive(:fetch_schema).and_raise(
ForestAdminDatasourceToolkit::Exceptions::ForestException,
'RPC connection failed: Unable to connect to http://localhost. Please check if the RPC server is running.'
)

expect { client.check_schema }.not_to raise_error

expect(logger).to have_received(:log).with('Warn', /RPC error.*RPC connection failed/)
expect(callback).not_to have_received(:call)
end

it 'handles unexpected errors gracefully' do
client = described_class.new(uri, secret) { |schema| callback.call(schema) }
allow(rpc_client).to receive(:fetch_schema).and_raise(StandardError, 'Unexpected error')
Expand Down Expand Up @@ -424,6 +437,29 @@ module Utils
expected_etag = Digest::SHA1.hexdigest(JSON.generate(introspection))
expect(client.instance_variable_get(:@cached_etag)).to eq(expected_etag)
end

it 'falls back to introspection_schema on ForestException (RPC wrapped error)' do
client = described_class.new(
uri, secret,
introspection_schema: introspection,
introspection_etag: 'fallback-etag-456'
) { |s| callback.call(s) }

allow(rpc_client).to receive(:fetch_schema).and_raise(
ForestAdminDatasourceToolkit::Exceptions::ForestException,
'RPC connection failed: Unable to connect to http://localhost.'
)

client.start?
client.stop

# Should use the introspection schema as current schema
expect(client.instance_variable_get(:@current_schema)).to eq(introspection)
# Should use the provided etag
expect(client.instance_variable_get(:@cached_etag)).to eq('fallback-etag-456')
# Should log the warning
expect(logger).to have_received(:log).with('Warn', /RPC agent.*unreachable.*ForestException/)
end
end

describe '#trigger_schema_change_callback' do
Expand Down Expand Up @@ -518,6 +554,30 @@ module Utils
# Should have continued polling after the connection error
expect(logger).to have_received(:log).with('Warn', /Connection error/)
end

it 'continues polling after RPC errors (ForestException)' do
# Initial fetch succeeds, first poll fails with ForestException, second poll succeeds
call_count = 0
allow(rpc_client).to receive(:fetch_schema) do
call_count += 1
# First call is initial fetch (succeeds), second is first poll (fails), third succeeds
if call_count == 2
raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
'RPC connection failed: Unable to connect to http://localhost.'
end

schema_response
end

client = described_class.new(uri, secret, polling_interval: 1) { |schema| callback.call(schema) }

client.start?
sleep(3) # Wait for initial fetch + polling
client.stop

# Should have continued polling after the RPC error
expect(logger).to have_received(:log).with('Warn', /RPC error.*RPC connection failed/)
end
end
end
end
Expand Down
Loading