{"record":{"id":"f6291c70c31cb9bb","repo":"antiwork/gumroad","slug":"sorry-something-went-wrong-please-try-again-f6291c","errorCode":null,"errorMessage":"Sorry, something went wrong. Please try again.","messagePattern":"Sorry, something went wrong\\. Please try again\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/services/purchase/create_service.rb","lineNumber":27,"sourceCode":"  attr_reader :product, :params, :purchase_params, :gift_params, :buyer\n  attr_accessor :purchase, :gift\n\n  def initialize(product:, params:, buyer: nil)\n    @product = product\n    @params = params\n    @purchase_params = params[:purchase]\n    @gift_params = params[:gift].presence\n    @buyer = buyer\n    @force_new_subscription = !!params[:force_new_subscription]\n  end\n\n  def perform\n    unless @product.allow_parallel_purchases?\n      inventory_semaphore = inventory_lock_client\n      inventory_lock_token = inventory_semaphore.lock\n      if inventory_lock_token.nil?\n        Rails.logger.warn(\"Could not acquire lock for product_inventory semaphore (product id: #{@product.id})\")\n        return nil, \"Sorry, something went wrong. Please try again.\"\n      end\n    end\n\n    begin\n      # create gift if necessary\n      self.gift = create_gift if is_gift?\n\n      # run pre-build validations\n      validate_perceived_price\n      validate_zip_code\n\n      # build primary (non-gift) purchase\n      self.purchase = build_purchase(purchase_params.merge(gift_given: gift))\n      purchase.submitted_pre_discount_price_cents = params[:submitted_pre_discount_price_cents]\n      purchase.once_per_cart_discount_allocation = params[:once_per_cart_discount_allocation]\n      if purchase.once_per_cart_discount_allocation.present?\n        purchase.offer_code = OfferCode.find_by(id: purchase.once_per_cart_discount_allocation[:offer_code_id])\n      end","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/services/purchase/create_service.rb#L9-L45","documentation":"Purchase::CreateService serializes checkouts of products that don't allow parallel purchases with a Redis-backed inventory semaphore (acquisition timeout 50 seconds, INVENTORY_LOCK_ACQUISITION_TIMEOUT). If semaphore.lock returns nil — the per-product lock stayed held for the entire window — perform returns [nil, \"Sorry, something went wrong. Please try again.\"] after a warn log naming the product id. Nothing was charged; this is pure lock contention, not a payment failure.","triggerScenarios":"Two or more concurrent checkouts of a limited-inventory product where an earlier holder keeps the lock for the full 50s (slow charge under load), a previous holder crashed so the lock waits out its TTL, Redis unavailable or slow, or a flash-sale burst exceeding what the semaphore admits.","commonSituations":"Product drops and flash sales on limited products; buyer double-clicks; retry storms after a partial outage; elevated charge-processor latency stretching each holder's critical section.","solutions":["Retry the purchase — the lock releases when the in-flight checkout finishes and the next attempt acquires it.","If it persists, check Redis health and whether the semaphore for the product id in the warn log has leaked (holder crashed without release).","Debounce or disable the buy button client-side to cut duplicate concurrent attempts."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"# Cheap pre-check: fail fast (or queue) when contention is likely\nlocked = inventory_lock_client.lock unless @product.allow_parallel_purchases?\nreturn retry_later if locked.nil? # instead of burning the full 50s window","typeGuard":null,"tryCatchPattern":"# perform returns a tuple, not an exception\npurchase, error = Purchase::CreateService.new(product:, params:, buyer:).perform\nif error == \"Sorry, something went wrong. Please try again.\" && purchase.nil?\n  retry_with_backoff # lock contention is transient; nothing was charged\nend","preventionTips":["Disable the buy button after the first click to cut duplicate lock holders.","Keep the post-lock critical section short so concurrent buyers queue briefly instead of timing out.","Monitor the \"Could not acquire lock for product_inventory semaphore\" warn log for leaked semaphores and Redis degradation."],"tags":["purchase","concurrency","semaphore","redis","inventory","lock-timeout"],"backgroundTag":"lock-acquisition-timeout","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}