module OneMoreTime

Constants

VERSION

Public Class Methods

start_request!(idempotency_key:, request_path: nil, request_body: nil) click to toggle source
# File lib/one_more_time.rb, line 12
def start_request!(idempotency_key:, request_path: nil, request_body: nil)
  IdempotentRequest.create!(
    idempotency_key: idempotency_key,
    locked_at: Time.current,
    request_path: request_path,
    request_body: request_body,
  )
rescue ActiveRecord::RecordNotUnique
  # Our UNIQUE constraint was violated, so a request with the given idempotency
  # key already exists. Use the highest transaction isolation level to atomically
  # load that request record and mark it as "locked".
  # Similar to Rails create_or_find_by, the race condition here is if another
  # client deleted the request record exactly at this point. For this specific
  # model there is basically no risk of that happening.
  serializable_transaction do
    existing_request = IdempotentRequest.find_by(idempotency_key: idempotency_key, locked_at: nil)
    validate_incoming_request!(existing_request, request_path, request_body)
    existing_request.update!(locked_at: Time.current) unless existing_request.finished?
    existing_request
  end
end

Private Class Methods

serializable_transaction() { || ... } click to toggle source
# File lib/one_more_time.rb, line 42
def serializable_transaction
  ActiveRecord::Base.transaction(isolation: :serializable) do
    yield
  end
rescue ActiveRecord::SerializationFailure
  raise RequestInProgressError
end
validate_incoming_request!(existing_request, request_path, request_body) click to toggle source
# File lib/one_more_time.rb, line 36
def validate_incoming_request!(existing_request, request_path, request_body)
  raise RequestInProgressError if existing_request.blank?
  raise RequestMismatchError if request_path.present? && request_path != existing_request.request_path
  raise RequestMismatchError if request_body.present? && request_body != existing_request.request_body
end