Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Very Hungry Transaction

Daniel Colson
June 08, 2024
53

The Very Hungry Transaction

The story begins with a little database transaction. As the days go by, more and more business requirements cause the transaction to grow in size. We soon discover that it isn't a little transaction anymore, and it now poses a serious risk to our application and business. What went wrong, and how can we fix it?

In this talk we'll witness a database transaction gradually grow into a liability. We'll uncover some common but problematic patterns that can put our data integrity and database health at risk, and then offer strategies for fixing and preventing these patterns.

Daniel Colson

June 08, 2024
Tweet

Transcript

  1. Cat R. Pillar • Job — Developer at BugHub •

    Dislikes — Birds, Software Bugs • Likes — Leaves, Actual Bugs
  2. Monday def submit @order.save! end => BEGIN => INSERT INTO

    orders ... => INSERT INTO order_items ... => INSERT INTO order_items ... ...
  3. Monday def submit @order.save! end => BEGIN => INSERT INTO

    orders ... => INSERT INTO order_items ... => INSERT INTO order_items ... ... => COMMIT / ROLLBACK
  4. “ I was so excited about my order, but it

    was canceled and now the items are not available.
  5. Tuesday => UPDATE products SET inventory = inventory - :order_item_quantity

    WHERE sku = :order_item_sku => UPDATE products ...
  6. Tuesday => UPDATE products SET inventory = inventory - :order_item_quantity

    WHERE sku = :order_item_sku => UPDATE products ... => UPDATE products ...
  7. Tuesday => UPDATE products SET inventory = inventory - :order_item_quantity

    WHERE sku = :order_item_sku => UPDATE products ... => UPDATE products ... => UPDATE products ...
  8. Tuesday => BEGIN => INSERT INTO orders ... => INSERT

    INTO order_items ... => INSERT INTO order_items ... ... => UPDATE products ... => UPDATE products ... ... => COMMIT
  9. Tuesday Lock Contention "# Bug 1 Bug 2 => BEGIN

    => BEGIN => UPDATE ... sku = APL1 => UPDATE ... sku = APL1
  10. Tuesday Lock Contention "# Bug 1 Bug 2 => BEGIN

    => BEGIN => UPDATE ... sku = APL1 => UPDATE ... sku = APL1 "Row locked ... Waiting ...
  11. Tuesday Lock Contention "# Bug 1 Bug 2 => BEGIN

    => BEGIN => UPDATE ... sku = APL1 => UPDATE ... sku = APL1 "Row locked ... Waiting ... "Row locked ... Waiting ...
  12. Tuesday Lock Contention "# Bug 1 Bug 2 => BEGIN

    => BEGIN => UPDATE ... sku = APL1 => UPDATE ... sku = APL1 "Row locked ... Waiting ... "Row locked ... Waiting ... "Row locked ... Waiting ...
  13. Tuesday Lock Contention "# Bug 1 Bug 2 => BEGIN

    => BEGIN => UPDATE ... sku = APL1 => UPDATE ... sku = APL1 "Row locked ... Waiting ... "Row locked ... Waiting ... "Row locked ... Waiting ... => COMMIT "Row locked ...
  14. Tuesday Little Opportunity for Contention => BEGIN => UPDATE products

    ... WHERE sku = APL1 => UPDATE products ... WHERE sku = ORG5 => COMMIT < 1 millisecond
  15. Tuesday Large Opportunity for Contention => BEGIN => UPDATE products

    ... WHERE sku = APL1 => UPDATE products ... WHERE sku = ORG5 => COMMIT > 1 second
  16. Tuesday Large Opportunity for Contention => BEGIN => UPDATE products

    ... WHERE sku = APL1 => UPDATE products ... WHERE sku = ORG5 => COMMIT Slow Queries
  17. Tuesday Large Opportunity for Contention => BEGIN => UPDATE products

    ... WHERE sku = APL1 => UPDATE products ... WHERE sku = PLM3 => UPDATE products ... WHERE sku = BRY4 => UPDATE products ... WHERE sku = PER2 ... (hundreds more) => UPDATE products ... WHERE sku = ORG5 => COMMIT Too Many Queries
  18. Tuesday Large Opportunity for Contention => BEGIN => UPDATE products

    ... WHERE sku = APL1 => UPDATE products ... WHERE sku = PLM3 => UPDATE products ... WHERE sku = BRY4 => UPDATE products ... WHERE sku = PER2 ... (hundreds more) => UPDATE products ... WHERE sku = ORG5 => COMMIT Waiting on Locks
  19. “ I don’t have time to waste. I want my

    orders to submit without delay.
  20. Wednesday class Order < ApplicationRecord after_create :finalize_order def finalize_order #

    Sync with billing platform # Send confirmation email # etc. end end
  21. Wednesday => BEGIN => INSERT INTO orders ... => Enqueue

    Job => Job Executes and Fails => UPDATE products ...
  22. Wednesday => BEGIN => INSERT INTO orders ... => Enqueue

    Job => Job Executes and Fails => UPDATE products ... => Job Retries and Fails
  23. Wednesday => BEGIN => INSERT INTO orders ... => Enqueue

    Job => Job Executes and Fails => UPDATE products ... => Job Retries and Fails => COMMIT
  24. Wednesday => BEGIN => INSERT INTO orders ... => Enqueue

    Job => Job Executes and Fails => UPDATE products ... => Job Retries and Fails => COMMIT => Job Retries and Succeeds
  25. Wednesday => BEGIN => INSERT INTO orders ... => Enqueue

    Job => Job Executes and Fails => UPDATE products ... => Job Retries and Fails => ROLLBACK
  26. Wednesday => BEGIN => INSERT INTO orders ... => Enqueue

    Job => Job Executes and Fails => UPDATE products ... => Job Retries and Fails => ROLLBACK => Job Retries and Fails
  27. Wednesday => BEGIN => INSERT INTO orders ... => Enqueue

    Job => Job Executes and Fails => UPDATE products ... => Job Retries and Fails => ROLLBACK => Job Retries and Fails => Job Retries and Fails
  28. Wednesday => BEGIN => INSERT INTO orders ... => UPDATE

    products ... => COMMIT => Enqueue Job => Job Executes and Succeeds
  29. Thursday => BEGIN => INSERT INTO orders ... => External

    Call => UPDATE products ... => COMMIT
  30. Thursday => BEGIN => INSERT INTO orders ... => UPDATE

    products ... => COMMIT / ROLLBACK Slow External Call
  31. Thursday => BEGIN => INSERT INTO orders ... => UPDATE

    products ... => COMMIT / ROLLBACK Idle
  32. Thursday => BEGIN => INSERT INTO orders ... => UPDATE

    products ... => COMMIT / ROLLBACK Idle Locks Held
  33. Thursday => BEGIN => INSERT INTO orders ... => UPDATE

    products ... => COMMIT / ROLLBACK Idle Connection in Use
  34. Thursday BEGIN Database BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN

    BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN BEGIN
  35. Friday Orders Database => BEGIN => INSERT INTO orders ...

    => COMMIT Products Database => BEGIN => UPDATE products ... => COMMIT Both Commit $
  36. Friday Orders Database => BEGIN => INSERT INTO orders ...

    => ROLLBACK Products Database => BEGIN => UPDATE products ... => ROLLBACK Both Rollback $
  37. Friday Orders Database => BEGIN => INSERT INTO orders ...

    => ROLLBACK Products Database => BEGIN => UPDATE products ... => COMMIT Not Atomic %
  38. Friday Orders Database => BEGIN => INSERT INTO orders ...

    Products Database => BEGIN => UPDATE products ... => COMMIT Idle
  39. Friday Orders Database => BEGIN => INSERT INTO orders ...

    Products Database => BEGIN => UPDATE products ... => COMMIT Idle ❌ Connection Failed
  40. Weekend External Calls within Transactions are a Risk • Data

    Integrity Problems • Cascading Failures
  41. Weekend External Calls within Transactions are a Risk • Data

    Integrity Problems • Cascading Failures • Includes: HTTP requests, emails, jobs, queries to other databases, etc.
  42. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday
  43. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday Small Incremental Changes
  44. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday Defer Until After Commit
  45. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday Defer Until After Commit - Rails 7.2 - Automatically delay Active Job enqueues to after commit #51426
  46. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday Defer Until After Commit - Rails 7.2 - Automatically delay Active Job enqueues to after commit #51426 - Rails 7.2 - Allow to register transaction callbacks outside of a record #51474
  47. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday Identify Other Problematic Transactions
  48. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday Identify Other Problematic Transactions - Database Observability
  49. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday Identify Other Problematic Transactions - Database Observability - Rails 7.1 - Instrument Active Record transactions #49192
  50. def submit Order.transaction do Product.transaction do Chrysalis.transaction do code.not_meant_for_reading! @order.save!

    FulfillmentClient.submit_order(@order) @order.update_product_inventory @product_suggestions = SuggestionService.build_for(@bug) seriously.stop_reading_this! if @order.bug.caterpillar? @bug.very_hungry! basket = Chrysalis::Basket.create!(@bug) PromotionMailer.new_basket(basket).deliver_later @product_suggestions << Chrysalis.default_items_for(@bug) else @bug.book_suggestions.create!( Book.find_by(author: "Eric Carle”, bug: @order.bug) ) end @bug.suggest(@product_suggestions) @bug.friends.each do |friend| @order.share_with(friend) if @bug.sharing_orders? end rescue => e handle_transaction_error(e) ensure nobody.should_have_read_any_of_this end end end end Monday Prevent Further Problematic Transactions
  51. Safe Transactions • Keep transactions short • Fast queries •

    Limit # of queries • No external calls • As little code as possible • Do you really need a transaction?