Last active
September 4, 2017 08:26
-
-
Save gerard-morera/bef557d223487824a6ab0daf9a7c7fd2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#The aim of this ticket is to handle Pachinko responses. I will show you three different solutions my code has been | |
#through and will give the reasons about its evolution. | |
#we are facing 4 different ways to handle responses | |
# - When is 200 return pending | |
# - When pachinko returns 500...599 retry the call if allowed or log and fails | |
# - When the client returns an exception retry the call if allowed or log and fails | |
# - When is 400...499 log and fails | |
#Total retries can sum until 3 | |
#FIRST SOLUTION | |
#The flow of this code is slightly complicated due to retrie loggic is a bit tanggled | |
module Points | |
class Granter | |
GRANT_RETRIES_ALLOWED = 3.freeze | |
def initialize(points_origin) | |
@points_origin = points_origin | |
@grant_retries = 0 | |
end | |
def grant | |
return :rsp_not_available_market unless user.market.superpoints | |
response = perform_request! | |
case response.status | |
when 200 | |
:pending | |
when 400...499 | |
log_and_fail!('Unexpected error from Pachinko', response) | |
when 500...599 | |
log_and_fail!('Server error occurred when granting for points', response) | |
end | |
end | |
private | |
def perform_request! | |
response = ::Pachinko::Grant::Client.new.bonus(user.market, points_info, | |
user.wuaki_tokens.last) | |
return response unless grant_retries_allowed? | |
return perform_request! if 500...599.include?(response.status) | |
response | |
rescue Faraday::ClientError, Errno::ECONNREFUSED, Timeout::Error => e | |
retry if grant_retries_allowed? | |
Rollbar.error(e, points_origin: points_origin, response: response.body, | |
status: response.status) | |
fail(e, points_origin: points_origin) | |
end | |
def log_and_fail!(message, response) | |
Rollbar.warning(message, response: response.body, status: response.status, | |
points_origin: points_origin) | |
fail(message) | |
end | |
def points_info | |
{ | |
key: points_origin.public_id, | |
date: points_origin.created_at, | |
bonus: points_origin.amount, | |
points_expiration_date: points_origin.expiration_date.in_time_zone | |
} | |
end | |
def grant_retries_allowed? | |
(self.grant_retries += 1) <= GRANT_RETRIES_ALLOWED | |
end | |
delegate :user, to: :points_origin | |
attr_reader :points_origin | |
attr_accessor :grant_retries | |
end | |
end | |
#SECOND SOLUTION | |
#We are wrapping the request with a block that will handle the retries. This way the logic of perform request and retries is | |
#a bit more untangled. | |
module Points | |
class Granter | |
GRANT_RETRIES_ALLOWED = 3.freeze | |
def initialize(points_origin) | |
@points_origin = points_origin | |
@grant_retries = 0 | |
end | |
def grant | |
return :rsp_not_available_market unless user.market.superpoints | |
case request_with_retries!.status | |
when 200 | |
:pending | |
when 400...499 | |
log_and_fail!('Unexpected error from Pachinko', response) | |
when 500...599 | |
log_and_fail!('Server error occurred when granting for points', response) | |
end | |
end | |
private | |
def request_with_retries! | |
retry_for_temporary_error! { perform_request } | |
end | |
def retry_for_temporary_error!(&block) | |
response = yield(block) | |
return response unless response.status.between?(500, 599) | |
retry_for_temporary_error!(&block) if grant_retries_allowed? | |
response | |
rescue Faraday::ClientError, Errno::ECONNREFUSED, Timeout::Error => e | |
return retry_for_temporary_error!(&block) if grant_retries_allowed? | |
Rollbar.error(e, points_origin: points_origin) | |
fail(e, points_origin: points_origin) | |
end | |
def perform_request | |
::Pachinko::Grant::Client.new.bonus(user, points_info, user.wuaki_tokens.last) | |
end | |
def log_and_fail!(message, response) | |
Rollbar.warning(message, response: response.body, status: response.status, | |
points_origin: points_origin) | |
fail(message) | |
end | |
def points_info | |
{ | |
key: points_origin.public_id, | |
date: points_origin.created_at, | |
bonus: points_origin.amount, | |
points_expiration_date: points_origin.expiration_date.in_time_zone | |
} | |
end | |
def grant_retries_allowed? | |
(@grant_retries += 1) <= GRANT_RETRIES_ALLOWED | |
end | |
delegate :user, to: :points_origin | |
attr_reader :points_origin | |
attr_accessor :grant_retries | |
end | |
end | |
#THIRD SOLUTION (total separation of concerns) | |
# The code here is longer and more fragmented but we succeed separating the different responsabilities if the code to its own | |
#objects. | |
#this code has 3 main objects: | |
# | |
# GrantResponseHandler -> handles the response | |
# ClientGranterWithRetries -> Handles the retries | |
# GrantClientWrapper -> Is a wrapper to pahicko client. Understand when a response is a failure or a success | |
#To me the last code is the best but may look like too fragmented. I wish you give it a look :) | |
module Points | |
class GrantResponseHandler | |
def call(points_origin) | |
return :rsp_not_available_market unless superpoints?(points_origin) | |
response = client_with_retries.call(points_origin) | |
log_and_fail(response) unless response.status == 200 | |
:pending | |
end | |
private | |
def client_with_retries | |
@client_with_retries ||= ClientGranterWithRetries.new | |
end | |
def superpoints?(points_origin) | |
points_origin.user.market.superpoints | |
end | |
def log_and_fail!(response) | |
Rollbar.warning(response: response.body, status: response.status, | |
points_origin: points_origin) | |
fail | |
end | |
end | |
class ClientGranterWithRetries | |
GRANT_RETRIES_ALLOWED = 3.freeze | |
def initialize | |
@retries_allowed = GRANT_RETRIES_ALLOWED | |
end | |
def call(points_origin) | |
response = client(points_origin).call | |
if retriable?(response) | |
retries_allowed -= 1 | |
call(points_origin) | |
end | |
response | |
end | |
private | |
def retriable?(response) | |
!response.success? && retries_allowed > 0 | |
end | |
def client(points_origin) | |
GrantClientWrapper.new(points_origin) | |
end | |
attr_reader :retries_allowed | |
end | |
class GrantClientWrapper | |
def initialize(points_origin) | |
@points_origin = points_origin | |
end | |
def call | |
response = perform_request | |
return FailureResponse.new(response) if response.status.between?(500, 599) | |
SuccessfulResponse.new(response) | |
rescue Faraday::ClientError, Errno::ECONNREFUSED, Timeout::Error => error | |
FailureResponse.new(NullResponse.new(error)) | |
end | |
private | |
def perform_request | |
#why shoul I know about user etc... That should be done in thhe client | |
::Pachinko::Grant::Client.new.bonus(user, points_info, user.wuaki_tokens.last) | |
end | |
def points_info | |
{ | |
key: points_origin.public_id, | |
date: points_origin.created_at, | |
bonus: points_origin.amount, | |
points_expiration_date: points_origin.expiration_date.in_time_zone | |
} | |
end | |
delegate :user, to: :points_origin | |
attr_reader :points_origin | |
end | |
class NullResponse | |
def initialize(error) | |
@error = error | |
end | |
def status | |
:no_response | |
end | |
def body | |
:no_body | |
end | |
alias_method :response, :error | |
attr_reader :error | |
end | |
class SuccessfulResponse | |
def initialize(response) | |
@response = response | |
end | |
def success? | |
true | |
end | |
delegate :status, to: :response | |
attr_reader :response | |
end | |
class FailureResponse | |
def initialize(response) | |
@response = response | |
end | |
def success? | |
false | |
end | |
delegate :status, to: :response | |
attr_reader :response | |
end | |
end | |
#thanks <3 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment