Skip to content

Instantly share code, notes, and snippets.

@gerard-morera
Last active September 4, 2017 08:26
Show Gist options
  • Save gerard-morera/bef557d223487824a6ab0daf9a7c7fd2 to your computer and use it in GitHub Desktop.
Save gerard-morera/bef557d223487824a6ab0daf9a7c7fd2 to your computer and use it in GitHub Desktop.
#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