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

Rails Concurrency Gotchas

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers. →
Avatar for mtoledo mtoledo
November 07, 2011

Rails Concurrency Gotchas

Avatar for mtoledo

mtoledo

November 07, 2011

Other Decks in Programming

Transcript

  1. “I’m trying to unsubscribe but it says email has already

    been taken”! miércoles 2 de noviembre de 2011
  2. # id :integer(4) # email :string(255) class User < ActiveRecord::Base

    validates_uniqueness_of :email end validates_uniqueness_of miércoles 2 de noviembre de 2011
  3. class AddUniquenessIndexToUserEmail < ActiveRecord::Migration def self.up add_index :users, :email, :unique

    => true end def self.down remove_index :users, :email end end Uniqueness Constraint! miércoles 2 de noviembre de 2011
  4. class Comment < ActiveRecord::Base belongs_to :post validates_presence_of :post end class

    Post < ActiveRecord::Base has_many :comments, :dependent => :destroy end :dependent => :destroy miércoles 2 de noviembre de 2011
  5. create_table :comments do |table| table.integer :post_id, :null => false end

    ActiveRecord::Base.connection.execute <<-EOS ALTER TABLE `comments` ADD CONSTRAINT FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE restrict ON UPDATE cascade EOS Database Foreign Keys! miércoles 2 de noviembre de 2011
  6. class Comment < ActiveRecord::Base after_create :update_comment_count def update_comment_count post =

    Post.find post_id post.comment_count += 1 post.save rescue StaleObjectError retry end end Infinite Loop miércoles 2 de noviembre de 2011
  7. 1 - READ UNCOMMITTED 2 - READ COMMITTED 3 -

    REPEATABLE READ 4 - SERIALIZABLE miércoles 2 de noviembre de 2011
  8. mysql> begin; Query OK, 0 rows affected (0,00 sec) mysql>

    select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) mysql> update posts set title = 'New Title' where lock_version = 0; Query OK, 0 rows affected (0,00 sec) Rows matched: 0 Changed: 0 Warnings: 0 mysql> select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) miércoles 2 de noviembre de 2011
  9. mysql> begin; Query OK, 0 rows affected (0,00 sec) mysql>

    select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) mysql> update posts set title = 'New Title' where lock_version = 0; Query OK, 0 rows affected (0,00 sec) Rows matched: 0 Changed: 0 Warnings: 0 mysql> select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) miércoles 2 de noviembre de 2011
  10. mysql> begin; Query OK, 0 rows affected (0,00 sec) mysql>

    select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) mysql> update posts set title = 'New Title' where lock_version = 0; Query OK, 0 rows affected (0,00 sec) Rows matched: 0 Changed: 0 Warnings: 0 // raises StaleObjectError mysql> select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) miércoles 2 de noviembre de 2011
  11. mysql> begin; Query OK, 0 rows affected (0,00 sec) mysql>

    select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) mysql> update posts set title = 'New Title' where lock_version = 0; Query OK, 0 rows affected (0,00 sec) Rows matched: 0 Changed: 0 Warnings: 0 // raises StaleObjectError mysql> select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) miércoles 2 de noviembre de 2011
  12. mysql> begin; Query OK, 0 rows affected (0,00 sec) mysql>

    select * from posts where id = 1; +----+-------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 0 | +----+-------+--------------+ 1 row in set (0,00 sec) mysql> update posts set title = 'New Title' where lock_version = 0; Query OK, 0 rows affected (0,00 sec) Rows matched: 0 Changed: 0 Warnings: 0 // raises StaleObjectError mysql> select * from posts where id = 1; +----+-----------+--------------+ | id | title | lock_version | +----+-------+--------------+ | 1 | Title | 1 | +----+-------+--------------+ 1 row in set (0,00 sec) miércoles 2 de noviembre de 2011
  13. > u1 = User.first => #<User id: 1, login: "marcos",

    options: {:active=>false} > u1.update_attributes :options => {:active => true} => true > u1.reload => #<User id: 1, login: "marcos", options: {:active=>false} Production Console miércoles 2 de noviembre de 2011
  14. # login :string(255) # options :text class User < ActiveRecord::Base

    serialize :options def activate self.options = {:active => true} self.save end end miércoles 2 de noviembre de 2011
  15. > u1 = User.first => #<User id: 1, login: "marcos",

    options: {:active=>false} > u2 = User.first => #<User id: 1, login: "marcos", options: {:active=>false} > u1.update_attributes :options => {:active => true} => true > u2.update_attributes :login => "mtoledo" => true > u1.reload => #<User id: 1, login: "mtoledo", options: {:active=>false} Dirty Checking miércoles 2 de noviembre de 2011
  16. // u1.update_attributes :options => {:active => true} UPDATE `users` SET

    `options` = '--- \n:active: true\n' WHERE (`users`.`id` = 1) // u2.update_attributes :login => "mtoledo" UPDATE `users` SET `login` = 'mtoledo', `options` = '--- \n:active: false\n' WHERE (`users`.`id` = 1) miércoles 2 de noviembre de 2011
  17. # id :integer(4) # user_id :integer(4) # lock_version :integer(4) #

    options :text class UserOptions < ActiveRecord::Base serialize :options belongs_to :user end miércoles 2 de noviembre de 2011
  18. production.log ERROR 1213 (40001): Deadlock found when trying to get

    lock;try restarting transaction miércoles 2 de noviembre de 2011
  19. No pessimistic locking! class Comment after_create :update_posts_count def update_posts_count self.posts.increment

    :comments_count end end class Posts after_update :update_comments def update_comments comments.each {|c| c.published = self.published; c.save} end end miércoles 2 de noviembre de 2011
  20. SELECT * FOR UPDATE FROM `comments` WHERE `id` = 1

    SELECT * FOR UPDATE FROM `posts` WHERE `id` = 1 SELECT * FOR UPDATE FROM `posts` WHERE `id` = 1 SELECT * FOR UPDATE FROM `comments` WHERE `id` = 1 Transaction 1 Transaction 2 DEADLOCK! locked.. miércoles 2 de noviembre de 2011
  21. UPDATE `comments` SET `title` = “Foo” WHERE `id` = 1

    UPDATE `posts` SET `title` = “bar” WHERE `id` = 1 UPDATE `posts` SET `title` = “bar” WHERE `id` = 1 UPDATE `comments` SET `title` = “foo” WHERE `id` = 1 Transaction 1 Transaction 2 DEADLOCK! locked.. miércoles 2 de noviembre de 2011
  22. No longer deadlocks! class Comment after_create :update_posts_count def update_posts_count self.posts.increment

    :comments_count end end class Posts before_update :update_comments def update_comments comments.each {|c| c.published = self.published; c.save} end end miércoles 2 de noviembre de 2011
  23. class Comment < ActiveRecord::Base def visible? post.published? && self.published? end

    end Avoids updating all comments miércoles 2 de noviembre de 2011
  24. - bluepill - god - monit - you and me

    Killing ruby (MRI) threads miércoles 2 de noviembre de 2011
  25. class Order belongs_to :user after_create :mark_user_as_customer def mark_user_as_customer user.customer =

    true user.save end end Simple Transaction miércoles 2 de noviembre de 2011
  26. What happened to my transaction? > order = Order.first =>

    #<Order id: 1, user_id: 1> > order.user.customer? => false > order.user? => #<User id: 1, login: "marcos", customer: false> miércoles 2 de noviembre de 2011
  27. def transaction yield rescue Exception => e rollback ensure commit

    unless rolledback? end ActiveRecord transaction (version for kids) miércoles 2 de noviembre de 2011
  28. Main thread is rescued def print_if_rescued sleep 10 rescue Exception

    puts 'rescued' ensure puts 'ensured' end print_if_rescued # $ killall ruby # => rescued # => ensured miércoles 2 de noviembre de 2011
  29. Other Threads are NOT! def print_if_rescued sleep 10 rescue Exception

    puts 'rescued' ensure puts 'ensured' end t = Thread.new { print_if_rescued } t.join # $ killall ruby # => ensured miércoles 2 de noviembre de 2011
  30. It commits! def transaction yield # thread killed inside yield

    rescue Exception => e rollback # nothing is raised ensure commit unless rolledback? end miércoles 2 de noviembre de 2011
  31. If you do, don’t use ActiveRecord + Threads + MRI

    miércoles 2 de noviembre de 2011
  32. 5 - ARCECA “Don’t use Thread#kill/raise ” Active Record Certified

    Enterprise Concurrency Architect miércoles 2 de noviembre de 2011
  33. 5 - ARCECA “Don’t use Thread#kill/raise ” Active Record Certified

    Enterprise Concurrency Architect miércoles 2 de noviembre de 2011