mirror of
https://github.com/internetee/registry.git
synced 2025-07-31 15:06:23 +02:00
Merge pull request #2762 from internetee/http-timeout-phone-validation
Http timeout phone validation
This commit is contained in:
commit
696a077f98
5 changed files with 210 additions and 7 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -21,3 +21,6 @@
|
|||
/import
|
||||
ettevotja_rekvisiidid__lihtandmed.csv.zip
|
||||
Dockerfile.dev
|
||||
.cursor/
|
||||
.cursorrules
|
||||
certs/
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
class OrgRegistrantPhoneCheckerJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
include Retryable
|
||||
|
||||
# Constants for API error types
|
||||
API_EXCEPTIONS = [
|
||||
HTTPClient::KeepAliveDisconnected,
|
||||
Net::OpenTimeout,
|
||||
Timeout::Error,
|
||||
Savon::HTTPError,
|
||||
Savon::SOAPFault,
|
||||
Wasabi::Resolver::HTTPError
|
||||
].freeze
|
||||
|
||||
CACHE_EXPIRES_IN = 1.day
|
||||
|
||||
def perform(type: 'bulk', registrant_user_code: nil, spam_delay: 1)
|
||||
puts '??? PERFROMED ???'
|
||||
case type
|
||||
when 'bulk'
|
||||
execute_bulk_checker(spam_delay)
|
||||
|
@ -14,7 +27,7 @@ class OrgRegistrantPhoneCheckerJob < ApplicationJob
|
|||
end
|
||||
|
||||
def execute_bulk_checker(spam_delay)
|
||||
log("Bulk checker started")
|
||||
log('Bulk checker started')
|
||||
|
||||
Contact.where(ident_type: 'org', ident_country_code: 'EE').joins(:registrant_domains).each do |registrant_user|
|
||||
is_phone_number_matching = check_the_registrant_phone_number(registrant_user)
|
||||
|
@ -22,15 +35,16 @@ class OrgRegistrantPhoneCheckerJob < ApplicationJob
|
|||
sleep(spam_delay)
|
||||
end
|
||||
|
||||
log("Bulk checker finished")
|
||||
log('Bulk checker finished')
|
||||
end
|
||||
|
||||
def execute_single_checker(registrant_user_code)
|
||||
registrant_user = Contact.where(ident_type: 'org', ident_country_code: 'EE').joins(:registrant_domains).find_by(code: registrant_user_code)
|
||||
registrant_user = Contact.where(ident_type: 'org', ident_country_code: 'EE')
|
||||
.joins(:registrant_domains)
|
||||
.find_by(code: registrant_user_code)
|
||||
return if registrant_user.nil?
|
||||
|
||||
is_phone_number_matching = check_the_registrant_phone_number(registrant_user)
|
||||
|
||||
call_disclosure_action(is_phone_number_matching, registrant_user)
|
||||
end
|
||||
|
||||
|
@ -74,7 +88,24 @@ class OrgRegistrantPhoneCheckerJob < ApplicationJob
|
|||
end
|
||||
|
||||
def fetch_phone_number_from_company_register(company_code)
|
||||
data = company_register.company_details(registration_number: company_code.to_s)
|
||||
data[0].phone_numbers
|
||||
cache_key = "company_register:#{company_code}:phone_numbers"
|
||||
|
||||
return fetch_from_company_register(company_code) if Rails.env.test? && ENV['SKIP_COMPANY_REGISTER_CACHE']
|
||||
|
||||
Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRES_IN) do
|
||||
fetch_from_company_register(company_code)
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_from_company_register(company_code)
|
||||
with_retry(
|
||||
exceptions: API_EXCEPTIONS,
|
||||
logger: Rails.logger,
|
||||
fallback: -> { log("Failed to get data for company #{company_code}, returning empty array"); [] }
|
||||
) do
|
||||
data = company_register.company_details(registration_number: company_code.to_s)
|
||||
log("Successfully retrieved data for company #{company_code}")
|
||||
data[0].phone_numbers
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
39
app/lib/retryable.rb
Normal file
39
app/lib/retryable.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Module for retrying operations with external APIs
|
||||
module Retryable
|
||||
# Executes a code block with a specified number of retry attempts in case of specific errors
|
||||
# @param max_attempts [Integer] maximum number of attempts (defaults to 3)
|
||||
# @param retry_delay [Integer] delay between attempts in seconds (defaults to 2)
|
||||
# @param exceptions [Array<Class>] exception classes to catch (defaults to all exceptions)
|
||||
# @param logger [Object] logger object (must support info, warn, error methods)
|
||||
# @param fallback [Proc] code block executed if all attempts fail
|
||||
# @return [Object] result of the block execution or fallback
|
||||
def with_retry(
|
||||
max_attempts: 3,
|
||||
retry_delay: 2,
|
||||
exceptions: [StandardError],
|
||||
logger: Rails.logger,
|
||||
fallback: -> { [] }
|
||||
)
|
||||
attempts = 0
|
||||
|
||||
retry_attempt = lambda do
|
||||
attempts += 1
|
||||
yield
|
||||
rescue *exceptions => e
|
||||
logger.warn("Attempt #{attempts}/#{max_attempts} failed with error: #{e.class} - #{e.message}")
|
||||
|
||||
if attempts < max_attempts
|
||||
logger.info("Retrying in #{retry_delay} seconds...")
|
||||
sleep retry_delay
|
||||
retry_attempt.call
|
||||
else
|
||||
logger.error("All attempts exhausted. Last error: #{e.class} - #{e.message}")
|
||||
fallback.call
|
||||
end
|
||||
end
|
||||
|
||||
retry_attempt.call
|
||||
end
|
||||
end
|
|
@ -12,6 +12,13 @@ class OrgRegistrantPhoneCheckerJobTest < ActiveSupport::TestCase
|
|||
ident_country_code: 'EE',
|
||||
ident: '12345678'
|
||||
)
|
||||
|
||||
ENV['SKIP_COMPANY_REGISTER_CACHE'] = 'true'
|
||||
Rails.cache.clear if defined?(Rails.cache)
|
||||
end
|
||||
|
||||
teardown do
|
||||
ENV['SKIP_COMPANY_REGISTER_CACHE'] = nil
|
||||
end
|
||||
|
||||
def test_bulk_checker_processes_all_ee_org_contacts
|
||||
|
@ -106,4 +113,59 @@ class OrgRegistrantPhoneCheckerJobTest < ActiveSupport::TestCase
|
|||
|
||||
CompanyRegister::Client.define_singleton_method(:new, original_new_method)
|
||||
end
|
||||
|
||||
# Test that successfully retries after a connection error
|
||||
def test_retries_and_recovers_from_connection_error
|
||||
call_count = 0
|
||||
|
||||
original_new_method = CompanyRegister::Client.method(:new)
|
||||
CompanyRegister::Client.define_singleton_method(:new) do
|
||||
object = original_new_method.call
|
||||
def object.company_details(registration_number:)
|
||||
parent_call_count = CompanyRegister::Client.class_variable_get(:@@test_call_count)
|
||||
parent_call_count += 1
|
||||
CompanyRegister::Client.class_variable_set(:@@test_call_count, parent_call_count)
|
||||
|
||||
if parent_call_count == 1
|
||||
raise HTTPClient::KeepAliveDisconnected, "Connection error"
|
||||
end
|
||||
|
||||
[OpenStruct.new(phone_numbers: ['+372.555666777'])]
|
||||
end
|
||||
object
|
||||
end
|
||||
|
||||
CompanyRegister::Client.class_variable_set(:@@test_call_count, 0)
|
||||
|
||||
@contact.disclosed_attributes = []
|
||||
@contact.save!
|
||||
OrgRegistrantPhoneCheckerJob.perform_now(type: 'single', registrant_user_code: @contact.code)
|
||||
@contact.reload
|
||||
|
||||
assert @contact.disclosed_attributes.include?('phone')
|
||||
assert_operator CompanyRegister::Client.class_variable_get(:@@test_call_count), :>=, 2
|
||||
|
||||
CompanyRegister::Client.remove_class_variable(:@@test_call_count)
|
||||
CompanyRegister::Client.define_singleton_method(:new, original_new_method)
|
||||
end
|
||||
|
||||
def test_returns_empty_array_when_max_retries_exceeded
|
||||
original_new_method = CompanyRegister::Client.method(:new)
|
||||
CompanyRegister::Client.define_singleton_method(:new) do
|
||||
object = original_new_method.call
|
||||
def object.company_details(registration_number:)
|
||||
raise HTTPClient::KeepAliveDisconnected, "Persistent connection error"
|
||||
end
|
||||
object
|
||||
end
|
||||
|
||||
@contact.disclosed_attributes = ['phone']
|
||||
@contact.save!
|
||||
OrgRegistrantPhoneCheckerJob.perform_now(type: 'single', registrant_user_code: @contact.code)
|
||||
@contact.reload
|
||||
|
||||
assert_not @contact.disclosed_attributes.include?('phone')
|
||||
|
||||
CompanyRegister::Client.define_singleton_method(:new, original_new_method)
|
||||
end
|
||||
end
|
68
test/lib/retryable_test.rb
Normal file
68
test/lib/retryable_test.rb
Normal file
|
@ -0,0 +1,68 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class RetryableTest < ActiveSupport::TestCase
|
||||
class TestClass
|
||||
include Retryable
|
||||
|
||||
attr_reader :call_count
|
||||
|
||||
def initialize
|
||||
@call_count = 0
|
||||
end
|
||||
|
||||
def test_retriable_method(should_fail: false, fail_count: 2)
|
||||
with_retry(
|
||||
max_attempts: 3,
|
||||
retry_delay: 0.1,
|
||||
logger: Rails.logger
|
||||
) do
|
||||
@call_count += 1
|
||||
|
||||
if should_fail && @call_count <= fail_count
|
||||
raise StandardError, "Тестовая ошибка #{@call_count}"
|
||||
end
|
||||
|
||||
'success'
|
||||
end
|
||||
end
|
||||
|
||||
def test_retriable_with_fallback(should_fail: true)
|
||||
with_retry(
|
||||
max_attempts: 2,
|
||||
retry_delay: 0.1,
|
||||
logger: Rails.logger,
|
||||
fallback: -> { 'fallback' }
|
||||
) do
|
||||
@call_count += 1
|
||||
raise StandardError, 'Постоянная ошибка' if should_fail
|
||||
'success'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test 'should retry specified number of times and succeed' do
|
||||
test_object = TestClass.new
|
||||
result = test_object.test_retriable_method(should_fail: true, fail_count: 2)
|
||||
|
||||
assert_equal 3, test_object.call_count
|
||||
assert_equal 'success', result
|
||||
end
|
||||
|
||||
test 'should succeed on first try if no errors' do
|
||||
test_object = TestClass.new
|
||||
result = test_object.test_retriable_method(should_fail: false)
|
||||
|
||||
assert_equal 1, test_object.call_count
|
||||
assert_equal 'success', result
|
||||
end
|
||||
|
||||
test 'should use fallback when all retries fail' do
|
||||
test_object = TestClass.new
|
||||
result = test_object.test_retriable_with_fallback(should_fail: true)
|
||||
|
||||
assert_equal 2, test_object.call_count
|
||||
assert_equal 'fallback', result
|
||||
end
|
||||
end
|
Loading…
Add table
Add a link
Reference in a new issue